Nuxt.js useFetch与useAsyncData #
一、useFetch详解 #
1.1 完整签名 #
typescript
function useFetch<T>(
url: string | Ref<string> | (() => string),
options?: UseFetchOptions<T>
): AsyncData<T, Error>
interface UseFetchOptions<T> {
key?: string
method?: string
query?: Record<string, any>
params?: Record<string, any>
body?: RequestInit['body'] | Record<string, any>
headers?: Record<string, string>
baseURL?: string
server?: boolean
lazy?: boolean
immediate?: boolean
deep?: boolean
dedupe?: boolean | 'cancel' | 'defer'
timeout?: number
retry?: number | false
transform?: (input: T) => T
pick?: string[]
default?: () => T
watch?: WatchSource[]
getCachedData?: (key: string) => T
onResponse?: (context: { response: Response }) => void
onRequest?: (context: { request: Request }) => void
onRequestError?: (context: { request: Request; error: Error }) => void
onResponseError?: (context: { response: Response; error: Error }) => void
}
1.2 响应式URL #
vue
<script setup lang="ts">
const userId = ref(1)
const url = computed(() => `/api/users/${userId.value}`)
const { data } = await useFetch(url)
</script>
1.3 动态查询参数 #
vue
<script setup lang="ts">
const filters = reactive({
status: 'active',
role: '',
search: ''
})
const { data } = await useFetch('/api/users', {
query: filters
})
</script>
1.4 请求拦截 #
vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
onRequest({ request }) {
console.log('请求:', request)
},
onResponse({ response }) {
console.log('响应:', response)
},
onRequestError({ request, error }) {
console.error('请求错误:', error)
},
onResponseError({ response, error }) {
console.error('响应错误:', error)
}
})
</script>
二、useAsyncData详解 #
2.1 完整签名 #
typescript
function useAsyncData<T>(
key: string,
handler: (ctx?: NuxtApp) => Promise<T>,
options?: AsyncDataOptions<T>
): AsyncData<T, Error>
interface AsyncDataOptions<T> {
server?: boolean
lazy?: boolean
immediate?: boolean
deep?: boolean
dedupe?: boolean | 'cancel' | 'defer'
transform?: (input: T) => T
pick?: string[]
default?: () => T
watch?: WatchSource[]
getCachedData?: (key: string) => T
}
2.2 唯一键管理 #
vue
<script setup lang="ts">
const route = useRoute()
const { data } = await useAsyncData(
`article-${route.params.slug}`,
() => $fetch(`/api/articles/${route.params.slug}`),
{
watch: [() => route.params.slug]
}
)
</script>
2.3 复杂数据获取 #
vue
<script setup lang="ts">
const { data } = await useAsyncData(
'dashboard-stats',
async () => {
const [users, posts, analytics] = await Promise.all([
$fetch('/api/users/stats'),
$fetch('/api/posts/stats'),
$fetch('/api/analytics')
])
return {
totalUsers: users.total,
totalPosts: posts.total,
pageViews: analytics.pageViews,
growth: {
users: users.growth,
posts: posts.growth
}
}
}
)
</script>
2.4 条件获取 #
vue
<script setup lang="ts">
const { data } = await useAsyncData(
'conditional-data',
async () => {
if (import.meta.server) {
return await $fetch('/api/server-data')
}
return await $fetch('/api/client-data')
}
)
</script>
三、高级选项 #
3.1 dedupe选项 #
vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
dedupe: 'cancel'
})
</script>
| 值 | 说明 |
|---|---|
true |
跳过重复请求 |
'cancel' |
取消之前的请求 |
'defer' |
等待之前的请求完成 |
3.2 deep选项 #
vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
deep: false
})
</script>
3.3 immediate选项 #
vue
<script setup lang="ts">
const { data, execute } = await useFetch('/api/users', {
immediate: false
})
const loadUsers = () => {
execute()
}
</script>
3.4 watch选项 #
vue
<script setup lang="ts">
const page = ref(1)
const pageSize = ref(10)
const sortBy = ref('name')
const { data } = await useFetch('/api/users', {
query: { page, pageSize, sortBy },
watch: [page, pageSize, sortBy]
})
</script>
四、数据刷新 #
4.1 手动刷新 #
vue
<script setup lang="ts">
const { data, refresh } = await useFetch('/api/users')
const handleRefresh = async () => {
await refresh()
}
</script>
4.2 强制刷新 #
vue
<script setup lang="ts">
const { data, refresh } = await useFetch('/api/users')
const forceRefresh = async () => {
await refresh({ dedupe: false })
}
</script>
4.3 定时刷新 #
vue
<script setup lang="ts">
const { data, refresh } = await useFetch('/api/notifications')
let interval: NodeJS.Interval
onMounted(() => {
interval = setInterval(() => {
refresh()
}, 30000)
})
onUnmounted(() => {
clearInterval(interval)
})
</script>
五、错误处理 #
5.1 基本错误处理 #
vue
<script setup lang="ts">
const { data, error } = await useFetch('/api/users')
if (error.value) {
console.error('请求失败:', error.value)
}
</script>
<template>
<div v-if="error" class="error">
{{ error.message }}
</div>
</template>
5.2 自定义错误处理 #
vue
<script setup lang="ts">
const { data, error } = await useFetch('/api/users', {
onResponseError({ response, error }) {
if (response.status === 401) {
navigateTo('/login')
} else if (response.status === 403) {
showError({ statusCode: 403, message: '没有权限' })
}
}
})
</script>
5.3 重试机制 #
vue
<script setup lang="ts">
const { data, error } = await useFetch('/api/users', {
retry: 3,
retryDelay: 1000,
onRequestError({ error }) {
console.log('请求失败,正在重试...')
}
})
</script>
六、数据转换 #
6.1 transform函数 #
vue
<script setup lang="ts">
interface ApiUser {
id: number
first_name: string
last_name: string
email: string
}
interface User {
id: number
name: string
email: string
}
const { data } = await useFetch<ApiUser[]>('/api/users', {
transform: (users) => users.map(user => ({
id: user.id,
name: `${user.first_name} ${user.last_name}`,
email: user.email
}))
})
</script>
6.2 pick选择字段 #
vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
pick: ['id', 'name', 'email']
})
</script>
6.3 默认值 #
vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
default: () => []
})
</script>
七、缓存策略 #
7.1 自定义缓存 #
vue
<script setup lang="ts">
const cache = new Map()
const { data } = await useFetch('/api/users', {
key: 'users-list',
getCachedData(key) {
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < 60000) {
return cached.data
}
return null
}
})
</script>
7.2 禁用缓存 #
vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
key: `users-${Date.now()}`,
getCachedData: () => null
})
</script>
7.3 缓存失效 #
vue
<script setup lang="ts">
const { data: users } = await useFetch('/api/users', {
key: 'users-list'
})
const deleteUser = async (id: number) => {
await $fetch(`/api/users/${id}`, { method: 'DELETE' })
clearNuxtData('users-list')
await refresh()
}
</script>
八、最佳实践 #
8.1 封装API请求 #
composables/useApi.ts:
typescript
export const useApi = () => {
const config = useRuntimeConfig()
const fetch = async <T>(url: string, options: any = {}) => {
return useFetch<T>(url, {
baseURL: config.public.apiBase,
headers: {
'Authorization': `Bearer ${useCookie('token').value}`
},
...options
})
}
return { fetch }
}
使用:
vue
<script setup lang="ts">
const { fetch } = useApi()
const { data } = await fetch<User[]>('/users')
</script>
8.2 类型安全 #
typescript
interface User {
id: number
name: string
email: string
}
interface ApiResponse<T> {
data: T
total: number
}
const { data } = await useFetch<ApiResponse<User[]>>('/api/users')
8.3 组合使用 #
vue
<script setup lang="ts">
const userId = ref(1)
const { data: user } = await useFetch(() => `/api/users/${userId.value}`)
const { data: posts } = await useFetch(() => `/api/users/${userId.value}/posts`, {
watch: [userId],
immediate: !!user.value
})
</script>
九、完整示例 #
9.1 数据表格组件 #
vue
<script setup lang="ts">
interface User {
id: number
name: string
email: string
role: string
createdAt: string
}
interface Pagination {
page: number
pageSize: number
total: number
totalPages: number
}
interface Sort {
field: string
order: 'asc' | 'desc'
}
const pagination = reactive<Pagination>({
page: 1,
pageSize: 10,
total: 0,
totalPages: 0
})
const sort = reactive<Sort>({
field: 'createdAt',
order: 'desc'
})
const filters = reactive({
search: '',
role: ''
})
const debouncedSearch = refDebounced(toRef(filters, 'search'), 300)
const { data, pending, refresh } = await useFetch<{ data: User[], pagination: Pagination }>(
'/api/users',
{
query: {
page: pagination.page,
pageSize: pagination.pageSize,
sortField: sort.field,
sortOrder: sort.order,
search: debouncedSearch,
role: filters.role
},
watch: [
() => pagination.page,
() => pagination.pageSize,
() => sort.field,
() => sort.order,
debouncedSearch,
() => filters.role
],
transform: (response) => {
pagination.total = response.pagination.total
pagination.totalPages = response.pagination.totalPages
return response
}
}
)
const handleSort = (field: string) => {
if (sort.field === field) {
sort.order = sort.order === 'asc' ? 'desc' : 'asc'
} else {
sort.field = field
sort.order = 'asc'
}
}
const handlePageChange = (page: number) => {
pagination.page = page
}
</script>
<template>
<div>
<div class="filters">
<input
v-model="filters.search"
placeholder="搜索用户..."
/>
<select v-model="filters.role">
<option value="">全部角色</option>
<option value="admin">管理员</option>
<option value="user">用户</option>
</select>
<button @click="refresh">刷新</button>
</div>
<table v-if="!pending">
<thead>
<tr>
<th @click="handleSort('name')">
姓名
<span v-if="sort.field === 'name'">
{{ sort.order === 'asc' ? '↑' : '↓' }}
</span>
</th>
<th>邮箱</th>
<th>角色</th>
<th @click="handleSort('createdAt')">
创建时间
<span v-if="sort.field === 'createdAt'">
{{ sort.order === 'asc' ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="user in data?.data" :key="user.id">
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
<td>{{ user.createdAt }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button
:disabled="pagination.page === 1"
@click="handlePageChange(pagination.page - 1)"
>
上一页
</button>
<span>{{ pagination.page }} / {{ pagination.totalPages }}</span>
<button
:disabled="pagination.page >= pagination.totalPages"
@click="handlePageChange(pagination.page + 1)"
>
下一页
</button>
</div>
</div>
</template>
十、总结 #
本章深入介绍了 useFetch 和 useAsyncData:
- 完整的选项配置
- 响应式 URL 和参数
- 高级选项(dedupe、deep、immediate)
- 数据刷新和错误处理
- 数据转换和缓存策略
- 最佳实践和封装技巧
掌握这些知识可以高效地管理应用数据,下一章我们将学习服务端 API 开发。
最后更新:2026-03-28