在 Vue 3 中,如何使用 Composition API 封装一个具备完整生命周期管理的数据加载 Hook,包括 Loading、Error、Retry 状态?

各位观众老爷,大家好!我是今天的主讲人,一个在代码堆里摸爬滚打多年的老码农。今天咱们聊聊Vue 3 Composition API,用它来封装一个贼好用的数据加载 Hook,让你的组件再也不用为了数据加载的那些破事儿烦恼。

咱们的目标是做一个 Hook,它能优雅地处理数据加载的各个阶段:

  • Loading: 告诉用户 "我正在努力加载数据呢,稍等哈!"
  • Success: 数据加载成功,让组件开心地展示数据。
  • Error: 出错了!告诉用户哪里错了,并提供重试的机会。
  • Retry: 用户点击重试,我们重新发起数据请求。

听起来是不是很棒?咱们这就开始!

第一步:搭建舞台,定义响应式状态

首先,我们需要定义一些响应式状态,用来跟踪数据加载的过程。在 Composition API 中,refreactive 是咱们的好伙伴。

import { ref, reactive, onMounted, onUnmounted } from 'vue';

export function useDataLoader(fetchDataFn) {
  const data = ref(null); // 存储加载到的数据
  const loading = ref(false); // 是否正在加载
  const error = ref(null); // 存储错误信息
  const isMounted = ref(false); // 组件是否挂载

  // 统一状态管理
  const state = reactive({
    loading: false,
    error: null,
    data: null,
    isMounted: false,
  });

  return {
    data,
    loading,
    error,
    isMounted,
    state,
  };
}
  • data: 用来存储加载成功的数据,初始值为 null
  • loading: 表示当前是否正在加载数据,初始值为 false
  • error: 如果加载过程中出现错误,就把错误信息存到这里,初始值为 null
  • isMounted: 组件是否挂载, 初始值为 false。 某些操作需要在组件挂载后才能进行,比如取消未完成的请求。
  • state: 使用reactive进行统一状态管理,将dataloadingerrorisMounted四个状态聚合到一个响应式对象中。

解释:

  • ref: 用于创建响应式的基本类型数据,比如这里的 loading (布尔值)、error (字符串或 null) 和 data (任意类型)。
  • reactive: 用于创建响应式的对象或数组。当对象或数组内部的属性发生变化时,Vue 会自动更新视图。

第二步:编写数据加载逻辑

接下来,我们要编写实际的数据加载逻辑。这里需要传入一个 fetchDataFn 函数,这个函数负责发起数据请求并返回一个 Promise。

import { ref, reactive, onMounted, onUnmounted } from 'vue';

export function useDataLoader(fetchDataFn) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);
  const isMounted = ref(false);

  // 统一状态管理
  const state = reactive({
    loading: false,
    error: null,
    data: null,
    isMounted: false,
  });

  const loadData = async () => {
    state.loading = true;
    loading.value = true;
    state.error = null;
    error.value = null;

    try {
      const result = await fetchDataFn();
      state.data = result;
      data.value = result;
    } catch (err) {
      state.error = err;
      error.value = err;
    } finally {
      state.loading = false;
      loading.value = false;
    }
  };

  return {
    data,
    loading,
    error,
    isMounted,
    state,
    loadData, // 暴露加载数据的函数
  };
}
  • loadData 函数:
    • 首先,设置 loading.valuetrue,表示开始加载数据。
    • 然后,调用传入的 fetchDataFn 函数,并使用 await 等待 Promise 完成。
    • 如果 Promise 成功 resolved,则将结果赋值给 data.value
    • 如果 Promise rejected,则将错误信息赋值给 error.value
    • 最后,无论成功与否,都将 loading.value 设置为 false,表示加载完成。

第三步:添加生命周期钩子

为了在组件挂载后自动加载数据,我们需要使用 onMounted 钩子。同时,为了避免内存泄漏,我们还需要在组件卸载时取消未完成的请求(如果 fetchDataFn 支持取消)。

import { ref, reactive, onMounted, onUnmounted } from 'vue';

export function useDataLoader(fetchDataFn) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);
  const isMounted = ref(false);

  // 统一状态管理
  const state = reactive({
    loading: false,
    error: null,
    data: null,
    isMounted: false,
  });

  let controller = null; // AbortController 实例

  const loadData = async () => {
    state.loading = true;
    loading.value = true;
    state.error = null;
    error.value = null;

    try {
      controller = new AbortController();
      const result = await fetchDataFn({ signal: controller.signal }); // 传递 signal
      state.data = result;
      data.value = result;
    } catch (err) {
      if (err.name === 'AbortError') {
        // 如果请求被取消,则不处理错误
        console.log('请求被取消');
        return;
      }
      state.error = err;
      error.value = err;
    } finally {
      state.loading = false;
      loading.value = false;
      controller = null; // 清空 controller
    }
  };

  onMounted(() => {
    state.isMounted = true;
    isMounted.value = true;
    loadData();
  });

  onUnmounted(() => {
    state.isMounted = false;
    isMounted.value = false;
    if (controller) {
      controller.abort(); // 取消请求
    }
  });

  return {
    data,
    loading,
    error,
    isMounted,
    state,
    loadData,
  };
}
  • onMounted
    • 在组件挂载后,调用 loadData 函数开始加载数据。
  • onUnmounted
    • 在组件卸载前,如果 controller 存在,则调用 controller.abort() 取消请求。

注意:

  • 这里使用了 AbortController 来取消请求。你需要确保你的 fetchDataFn 函数支持 AbortSignal。例如,你可以使用 fetch API 并将 signal 传递给它。
  • catch 块中,我们需要检查错误是否是 AbortError。如果是,则说明请求是被取消的,我们不需要处理这个错误。
  • 每次发起请求时,创建一个新的 AbortController 实例,并在请求完成后清空 controller

第四步:添加重试机制

有时候,数据加载失败可能是因为网络问题或者服务器暂时不可用。在这种情况下,提供一个重试按钮让用户手动重试是非常友好的。

import { ref, reactive, onMounted, onUnmounted } from 'vue';

export function useDataLoader(fetchDataFn) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);
  const isMounted = ref(false);

  // 统一状态管理
  const state = reactive({
    loading: false,
    error: null,
    data: null,
    isMounted: false,
  });

  let controller = null; // AbortController 实例

  const loadData = async () => {
    state.loading = true;
    loading.value = true;
    state.error = null;
    error.value = null;

    try {
      controller = new AbortController();
      const result = await fetchDataFn({ signal: controller.signal }); // 传递 signal
      state.data = result;
      data.value = result;
    } catch (err) {
      if (err.name === 'AbortError') {
        // 如果请求被取消,则不处理错误
        console.log('请求被取消');
        return;
      }
      state.error = err;
      error.value = err;
    } finally {
      state.loading = false;
      loading.value = false;
      controller = null; // 清空 controller
    }
  };

  onMounted(() => {
    state.isMounted = true;
    isMounted.value = true;
    loadData();
  });

  onUnmounted(() => {
    state.isMounted = false;
    isMounted.value = false;
    if (controller) {
      controller.abort(); // 取消请求
    }
  });

  const retry = () => {
    loadData();
  };

  return {
    data,
    loading,
    error,
    isMounted,
    state,
    loadData,
    retry, // 暴露重试函数
  };
}
  • retry 函数:
    • 直接调用 loadData 函数重新加载数据。

第五步:封装成通用 Hook

现在,我们已经有了一个功能完善的数据加载 Hook。为了让它更通用,我们可以添加一些配置选项,例如:

  • immediate: 是否在组件挂载后立即加载数据,默认为 true
  • onError: 一个回调函数,用于处理错误。
import { ref, reactive, onMounted, onUnmounted } from 'vue';

export function useDataLoader(fetchDataFn, options = {}) {
  const { immediate = true, onError } = options;

  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);
  const isMounted = ref(false);

  // 统一状态管理
  const state = reactive({
    loading: false,
    error: null,
    data: null,
    isMounted: false,
  });

  let controller = null; // AbortController 实例

  const loadData = async () => {
    state.loading = true;
    loading.value = true;
    state.error = null;
    error.value = null;

    try {
      controller = new AbortController();
      const result = await fetchDataFn({ signal: controller.signal }); // 传递 signal
      state.data = result;
      data.value = result;
    } catch (err) {
      if (err.name === 'AbortError') {
        // 如果请求被取消,则不处理错误
        console.log('请求被取消');
        return;
      }
      state.error = err;
      error.value = err;
      if (onError) {
        onError(err); // 调用错误处理回调函数
      }
    } finally {
      state.loading = false;
      loading.value = false;
      controller = null; // 清空 controller
    }
  };

  onMounted(() => {
    state.isMounted = true;
    isMounted.value = true;
    if (immediate) {
      loadData();
    }
  });

  onUnmounted(() => {
    state.isMounted = false;
    isMounted.value = false;
    if (controller) {
      controller.abort(); // 取消请求
    }
  });

  const retry = () => {
    loadData();
  };

  return {
    data,
    loading,
    error,
    isMounted,
    state,
    loadData,
    retry,
  };
}
  • options: 一个可选的配置对象。
    • immediate: 是否在组件挂载后立即加载数据,默认为 true
    • onError: 一个回调函数,用于处理错误。

第六步:使用 Hook

现在,我们可以在组件中使用这个 Hook 了。

<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-if="error">
      Error: {{ error.message }}
      <button @click="retry">Retry</button>
    </div>
    <div v-if="data">
      Data: {{ data }}
    </div>
  </div>
</template>

<script setup>
import { useDataLoader } from './useDataLoader';
import { ref } from 'vue';

// 模拟一个异步请求
const fetchData = async () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟成功
      // resolve({ message: 'Hello from API!' });

      // 模拟失败
      reject(new Error('Failed to fetch data'));
    }, 1000);
  });
};

const onError = (error) => {
  console.error('Global error handler:', error);
  // 在这里可以做一些全局的错误处理,比如发送错误报告
};

const { data, loading, error, retry } = useDataLoader(fetchData, { onError });

</script>

解释:

  • 首先,我们导入 useDataLoader Hook。
  • 然后,我们定义一个 fetchData 函数,用于模拟异步请求。
  • 接下来,我们调用 useDataLoader Hook,并将 fetchData 函数作为参数传递给它。
  • 最后,我们在模板中使用 loadingerrordata 来展示不同的状态。

状态表格

为了更清晰地展示 Hook 的状态,我们可以用一个表格来总结一下:

状态 loading error data 说明
Initial false null null 组件挂载后,但尚未开始加载数据。
Loading true null null 正在加载数据。
Success false null 非 null 数据加载成功。
Error false 非 null null 或 之前的数据` 数据加载失败。

总结

通过上面的步骤,我们成功地封装了一个功能完善的数据加载 Hook。它具有以下优点:

  • 代码复用: 可以在多个组件中复用,避免重复编写数据加载逻辑。
  • 状态管理: 集中管理数据加载的状态,使组件代码更清晰。
  • 错误处理: 提供统一的错误处理机制,提高应用的健壮性。
  • 重试机制: 允许用户手动重试,提高用户体验。
  • 生命周期管理: 在组件挂载和卸载时自动加载和取消请求,避免内存泄漏。

这个 Hook 可以大大简化你的 Vue 3 组件开发,让你更专注于业务逻辑。希望这个讲座对你有所帮助!如果有什么问题,欢迎在评论区留言。

扩展

  • 缓存: 可以添加缓存机制,避免重复加载相同的数据。
  • 分页: 可以支持分页加载数据。
  • 节流/防抖: 可以使用节流或防抖来限制数据加载的频率。
  • 自定义 Loading 状态: 可以使用插槽来自定义 Loading 状态的展示。

这些扩展可以使你的 Hook 更加强大和灵活。

好了,今天的讲座就到这里。希望大家学有所获,写出更优雅的代码! 感谢各位的观看!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注