各位靓仔靓女,老少爷们,晚上好!今天咱们来唠唠 Vue 3 Composition API 里自定义 Hook 的那些事儿,重点是搞定异步数据加载,还要优雅地管理 loading 和 error 状态。保证你听完之后,腰不酸了,腿不疼了,写代码也更有劲儿了!
开场白:Hook 的魅力与必要性
在Vue 2 的 Options API 里,我们吭哧吭哧地把 data、methods、computed、watch 塞到一个对象里。代码量少的时候还行,一旦组件变得复杂,代码就像一团乱麻,维护起来那叫一个痛苦。这时候,Composition API 就闪亮登场了。它允许我们把相关的逻辑提取出来,放到一个个独立的函数里,也就是 Hook。
Hook 的好处嘛,那是杠杠的:
- 代码复用性爆表: 同一个逻辑,在不同的组件里随便用。
- 可读性大幅提升: 逻辑清晰,一眼就能看明白。
- 维护性蹭蹭上涨: 修改一个地方,影响范围可控。
今天咱们要讲的自定义 Hook,就是利用 Composition API,把异步数据加载的逻辑封装起来,让你的组件专注于展示数据,而不是操心数据怎么来。
正文:打造你的专属异步数据加载 Hook
咱们的目标是创建一个 Hook,它能:
- 发起异步请求(比如用
fetch
或者axios
) - 管理
loading
状态(请求中/请求完成) - 处理
error
状态(请求失败) - 返回数据和状态给组件
先来个简单的框架:
// useAsyncData.js
import { ref, onMounted } from 'vue';
export function useAsyncData(url) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
onMounted(async () => {
// 你的异步请求逻辑
});
return {
data,
loading,
error,
};
}
解释一下:
useAsyncData
:这就是我们的 Hook 函数,接收一个url
参数,表示要请求的地址。data
、loading
、error
:这三个ref
变量用来存储数据、加载状态和错误信息。ref
是 Vue 3 响应式系统的核心,当它们的值发生变化时,使用这个 Hook 的组件会自动更新。onMounted
:Vue 3 的生命周期钩子,类似于 Vue 2 的mounted
。在这里发起异步请求,确保组件挂载后才开始加载数据。
接下来,往 onMounted
里面填充异步请求的逻辑:
// useAsyncData.js (完整版)
import { ref, onMounted } from 'vue';
export function useAsyncData(url) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
loading.value = true;
error.value = null; // 重置错误信息
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
};
onMounted(fetchData);
return {
data,
loading,
error,
refetch: fetchData, // 添加一个手动重新加载数据的函数
};
}
代码解读:
fetchData
:一个异步函数,封装了fetch
请求的逻辑。loading.value = true
:请求开始前,把loading
设置为true
,告诉组件正在加载数据。error.value = null
:请求前重置错误信息,避免显示上次的错误。try...catch...finally
:标准的try...catch
结构,用于捕获请求过程中可能出现的错误。response.ok
:检查 HTTP 状态码,如果不是 200-299 范围内的,就抛出一个错误。response.json()
:把响应体解析成 JSON 格式。error.value = e
:如果请求失败,把错误信息保存到error
变量里。loading.value = false
:请求结束后,无论成功还是失败,都要把loading
设置为false
。refetch: fetchData
:导出一个refetch
函数,允许组件手动重新加载数据。这个在需要手动刷新数据时非常有用。
如何使用这个 Hook?
在你的 Vue 组件里,像这样使用它:
<template>
<div>
<p v-if="loading">Loading...</p>
<p v-if="error">Error: {{ error.message }}</p>
<div v-if="data">
<h1>{{ data.title }}</h1>
<p>{{ data.body }}</p>
</div>
<button @click="refetch" :disabled="loading">Refresh</button>
</div>
</template>
<script>
import { useAsyncData } from './useAsyncData';
import { ref, onMounted } from 'vue';
export default {
setup() {
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1'; // 随便找个 API
const { data, loading, error, refetch } = useAsyncData(apiUrl);
return {
data,
loading,
error,
refetch,
};
},
};
</script>
解释:
import { useAsyncData } from './useAsyncData'
:引入我们自定义的 Hook。const { data, loading, error, refetch } = useAsyncData(apiUrl)
:调用 Hook,并解构返回的值。v-if="loading"
:当loading
为true
时,显示 "Loading…"。v-if="error"
:当error
不为null
时,显示错误信息。v-if="data"
:当data
不为null
时,显示数据。@click="refetch"
:点击按钮时,调用refetch
函数重新加载数据。:disabled="loading"
:当loading
为true
时,禁用按钮,防止用户重复点击。
进阶:更强大的 Hook
上面的 Hook 已经能满足基本的需求了,但是还可以更强大!
- 自定义
initialValue
有时候,我们希望在数据加载之前,先显示一个默认值。可以给 Hook 添加一个 initialValue
参数:
// useAsyncData.js (添加 initialValue)
import { ref, onMounted } from 'vue';
export function useAsyncData(url, initialValue = null) {
const data = ref(initialValue); // 使用 initialValue 初始化 data
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
// ... 省略请求逻辑,和之前一样
};
onMounted(fetchData);
return {
data,
loading,
error,
refetch: fetchData,
};
}
使用方式:
<script>
import { useAsyncData } from './useAsyncData';
export default {
setup() {
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
const { data, loading, error, refetch } = useAsyncData(apiUrl, { title: 'Loading...', body: '' }); // 提供 initialValue
return {
data,
loading,
error,
refetch,
};
},
};
</script>
现在,在数据加载完成之前,会显示 title: 'Loading...', body: ''
。
- 延迟加载 (Lazy Loading)
有时候,我们不希望组件一挂载就立即加载数据,而是希望在特定条件下才加载。可以添加一个 immediate
参数:
// useAsyncData.js (添加 immediate)
import { ref, onMounted, watch } from 'vue';
export function useAsyncData(url, initialValue = null, immediate = true) {
const data = ref(initialValue);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
// ... 省略请求逻辑,和之前一样
};
if (immediate) {
onMounted(fetchData);
}
return {
data,
loading,
error,
refetch: fetchData,
};
}
使用方式:
<template>
<div>
<button @click="loadData" v-if="!data && !loading && !error">Load Data</button>
<p v-if="loading">Loading...</p>
<p v-if="error">Error: {{ error.message }}</p>
<div v-if="data">
<h1>{{ data.title }}</h1>
<p>{{ data.body }}</p>
</div>
</div>
</template>
<script>
import { useAsyncData } from './useAsyncData';
import { ref } from 'vue';
export default {
setup() {
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
const shouldLoad = ref(false);
const { data, loading, error, refetch } = useAsyncData(apiUrl, null, false); // immediate 设置为 false
const loadData = () => {
refetch();
};
return {
data,
loading,
error,
refetch,
loadData,
};
},
};
</script>
现在,组件挂载后不会立即加载数据,而是需要点击 "Load Data" 按钮才会加载。
- 响应式地改变 URL
有时候,我们需要根据用户的操作动态地改变 URL。可以用 watch
监听 URL 的变化,并在 URL 变化时重新加载数据:
// useAsyncData.js (响应式 URL)
import { ref, onMounted, watch } from 'vue';
export function useAsyncData(url, initialValue = null, immediate = true) {
const data = ref(initialValue);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
};
if (immediate) {
onMounted(fetchData);
}
watch(
() => url,
(newUrl) => {
if (newUrl) {
fetchData();
}
}
);
return {
data,
loading,
error,
refetch: fetchData,
};
}
使用方式:
<template>
<div>
<input type="text" v-model="userId" placeholder="Enter user ID">
<p v-if="loading">Loading...</p>
<p v-if="error">Error: {{ error.message }}</p>
<div v-if="data">
<h1>User ID: {{ userId }}</h1>
<p>Name: {{ data.name }}</p>
<p>Email: {{ data.email }}</p>
</div>
</div>
</template>
<script>
import { useAsyncData } from './useAsyncData';
import { ref } from 'vue';
export default {
setup() {
const userId = ref(1);
const apiUrl = ref(`https://jsonplaceholder.typicode.com/users/${userId.value}`); // apiUrl 是 ref
const { data, loading, error, refetch } = useAsyncData(apiUrl);
return {
data,
loading,
error,
userId,
};
},
watch: {
userId(newUserId) {
apiUrl.value = `https://jsonplaceholder.typicode.com/users/${newUserId}`; // 更新 apiUrl
},
},
};
</script>
在这个例子中,apiUrl
是一个 ref
,它的值会随着 userId
的变化而变化。watch
监听 apiUrl
的变化,并在 apiUrl
变化时重新加载数据。
总结
今天我们一起打造了一个强大的异步数据加载 Hook,它可以处理 loading 和 error 状态,支持自定义初始值、延迟加载和响应式 URL。通过这个 Hook,我们可以把异步数据加载的逻辑提取出来,让组件更加简洁、易于维护。
当然,这只是一个基础的 Hook,你可以根据自己的需求进行扩展,比如添加缓存、请求取消等功能。
希望今天的分享对你有所帮助!下次再见!