各位观众老爷,大家好!我是今天的主讲人,一个在代码堆里摸爬滚打多年的老码农。今天咱们聊聊Vue 3 Composition API,用它来封装一个贼好用的数据加载 Hook,让你的组件再也不用为了数据加载的那些破事儿烦恼。
咱们的目标是做一个 Hook,它能优雅地处理数据加载的各个阶段:
- Loading: 告诉用户 "我正在努力加载数据呢,稍等哈!"
- Success: 数据加载成功,让组件开心地展示数据。
- Error: 出错了!告诉用户哪里错了,并提供重试的机会。
- Retry: 用户点击重试,我们重新发起数据请求。
听起来是不是很棒?咱们这就开始!
第一步:搭建舞台,定义响应式状态
首先,我们需要定义一些响应式状态,用来跟踪数据加载的过程。在 Composition API 中,ref
和 reactive
是咱们的好伙伴。
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
进行统一状态管理,将data
,loading
,error
,isMounted
四个状态聚合到一个响应式对象中。
解释:
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.value
为true
,表示开始加载数据。 - 然后,调用传入的
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
函数作为参数传递给它。 - 最后,我们在模板中使用
loading
、error
和data
来展示不同的状态。
状态表格
为了更清晰地展示 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 更加强大和灵活。
好了,今天的讲座就到这里。希望大家学有所获,写出更优雅的代码! 感谢各位的观看!