数据获取与 API 调用
深度解析 Nuxt 框架中的数据获取机制,包括客户端和服务端数据获取方式、API 路由开发,以及性能优化最佳实践
概述
数据获取是现代 Web 应用开发中的核心环节,Nuxt 提供了一套完整而强大的数据获取解决方案。本文将深度解析 Nuxt 框架中的数据获取机制,从基础的客户端请求到复杂的服务端渲染数据获取,帮助中高级前端开发者掌握高效的数据管理策略。
🎯 核心目标
- 掌握 Nuxt 多种数据获取方式的特点和使用场景
- 理解服务端渲染中的数据获取机制和性能优化
- 学会构建高效的 API 路由和数据缓存策略
- 掌握错误处理和重试机制的最佳实践
💡 核心特性
- 多样化数据获取: 支持
$fetch
、useFetch
、useLazyFetch
等多种数据获取方式 - 服务端优化: 内置数据预渲染、缓存机制和错误处理
- 类型安全: 完整的 TypeScript 支持,提供类型推断和验证
- 性能优化: 自动的数据缓存、去重和懒加载机制
架构思想: Nuxt 的数据获取遵循 "同构渲染" 原则,同一套代码既能在服务端运行进行预渲染,也能在客户端运行提供交互体验。
数据获取方式
$fetch 全局方法
$fetch
是 Nuxt 提供的全局数据获取方法,基于 ofetch
库构建,提供了现代化的 Promise-based API。
基础用法
// 简单的 GET 请求
const data = await $fetch('/api/users')
// 带参数的 POST 请求
const result = await $fetch('/api/users', {
method: 'POST',
body: {
name: 'John Doe',
email: 'john@example.com'
}
})
// 带查询参数的请求
const users = await $fetch('/api/users', {
query: {
page: 1,
limit: 10,
status: 'active'
}
})
高级配置
// 完整配置示例
const response = await $fetch<UserResponse>('/api/users', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(userData),
query: { include: 'profile' },
timeout: 5000,
retry: 3,
retryDelay: 1000,
onRequest({ request, options }) {
// 请求拦截器
console.log('发送请求:', request)
},
onResponse({ request, response }) {
// 响应拦截器
console.log('收到响应:', response.status)
},
onRequestError({ request, error }) {
// 请求错误处理
console.error('请求失败:', error)
}
})
类型安全实践
// 定义响应类型
interface User {
id: number
name: string
email: string
profile?: {
avatar: string
bio: string
}
}
interface ApiResponse<T> {
data: T
message: string
code: number
}
// 使用类型注解
const users = await $fetch<ApiResponse<User[]>>('/api/users')
const user = await $fetch<ApiResponse<User>>(`/api/users/${userId}`)
// 类型守卫函数
function isValidUser(data: any): data is User {
return data &&
typeof data.id === 'number' &&
typeof data.name === 'string' &&
typeof data.email === 'string'
}
// 使用类型守卫
const userData = await $fetch('/api/user/123')
if (isValidUser(userData.data)) {
// TypeScript 现在知道 userData.data 是 User 类型
console.log(userData.data.name)
}
最佳实践: $fetch
适用于客户端的动态数据获取,特别是用户交互触发的 API 调用。对于页面初始化数据,推荐使用 useFetch
或 useAsyncData
。
useFetch 组合式 API
useFetch
是 Nuxt 提供的核心数据获取组合函数,专为 Vue 组件设计,提供响应式数据管理和自动的服务端渲染支持。
基础使用
<script setup lang="ts">
// 基础用法
const { data: users, pending, error, refresh } = await useFetch('/api/users')
// 带类型注解
const { data: user, pending: userLoading } = await useFetch<User>(`/api/users/${userId}`)
// 条件获取
const { data: posts } = await useFetch('/api/posts', {
query: {
category: route.query.category,
page: route.query.page || 1
}
})
</script>
<template>
<div>
<div v-if="pending">加载中...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<UserCard v-for="user in users" :key="user.id" :user="user" />
<button @click="refresh()">刷新数据</button>
</div>
</div>
</template>
高级配置选项
<script setup lang="ts">
const route = useRoute()
const { data, pending, error, refresh } = await useFetch('/api/posts', {
// 查询参数
query: computed(() => ({
category: route.query.category,
page: route.query.page || 1,
search: route.query.search
})),
// 请求配置
method: 'GET',
headers: {
'Authorization': `Bearer ${useAuthStore().token}`
},
// 缓存配置
key: 'posts-list', // 缓存键名
server: true, // 是否在服务端执行
lazy: false, // 是否延迟加载
default: () => [], // 默认值
// 数据转换
transform: (data: any) => {
return data.posts.map((post: any) => ({
id: post.id,
title: post.title,
slug: post.slug,
publishedAt: new Date(post.published_at)
}))
},
// 响应拦截
onResponse({ response }) {
console.log('响应状态:', response.status)
},
// 错误处理
onResponseError({ error }) {
console.error('API 错误:', error)
}
})
// 监听路由变化自动重新获取
watch(() => route.query, () => {
refresh()
})
</script>
响应式数据管理
<script setup lang="ts">
// 响应式查询参数
const searchParams = reactive({
keyword: '',
category: '',
sortBy: 'created_at',
order: 'desc'
})
// 基于响应式参数的数据获取
const { data: searchResults, pending: searching } = await useFetch('/api/search', {
query: searchParams,
key: 'search-results',
watch: false, // 禁用自动监听,手动控制
})
// 手动触发搜索
const handleSearch = () => {
refresh()
}
// 监听特定字段变化
watchDebounced(
() => searchParams.keyword,
() => {
if (searchParams.keyword.length >= 2) {
refresh()
}
},
{ debounce: 300 }
)
</script>
性能优化: useFetch
会自动缓存数据并在服务端预渲染,避免客户端的重复请求。合理使用 key
参数可以实现更精细的缓存控制。
useLazyFetch 延迟加载
useLazyFetch
提供非阻塞的数据获取方式,特别适用于非关键数据的加载,可以提升页面的首屏渲染性能。
基础概念
<script setup lang="ts">
// 立即返回,不阻塞渲染
const {
data: comments,
pending: loadingComments,
error: commentsError
} = useLazyFetch('/api/posts/123/comments')
// 关键数据使用 useFetch(阻塞渲染)
const { data: post } = await useFetch('/api/posts/123')
// 非关键数据使用 useLazyFetch(非阻塞)
const { data: relatedPosts } = useLazyFetch('/api/posts/123/related')
const { data: userStats } = useLazyFetch('/api/users/stats')
</script>
<template>
<div>
<!-- 关键内容立即显示 -->
<article>
<h1>{{ post.title }}</h1>
<div v-html="post.content"></div>
</article>
<!-- 非关键内容延迟加载 -->
<aside>
<div v-if="loadingComments">加载评论中...</div>
<CommentList v-else-if="comments" :comments="comments" />
<div v-if="relatedPosts">
<h3>相关文章</h3>
<PostCard v-for="post in relatedPosts" :key="post.id" :post="post" />
</div>
</aside>
</div>
</template>
条件延迟加载
<script setup lang="ts">
const user = useAuthUser()
const showAdvanced = ref(false)
// 只有在需要时才加载高级数据
const { data: advancedData, pending: loadingAdvanced } = useLazyFetch('/api/advanced-stats', {
server: false, // 仅在客户端执行
default: () => null,
// 条件执行
execute: () => showAdvanced.value && user.value?.role === 'admin'
})
// 切换显示状态
const toggleAdvanced = () => {
showAdvanced.value = !showAdvanced.value
if (showAdvanced.value && !advancedData.value) {
// 手动触发数据获取
refresh()
}
}
</script>
分页延迟加载
<script setup lang="ts">
const page = ref(1)
const allPosts = ref<Post[]>([])
// 分页数据延迟加载
const { data: newPosts, pending: loadingMore } = useLazyFetch('/api/posts', {
query: computed(() => ({ page: page.value })),
key: computed(() => `posts-page-${page.value}`),
transform: (data: any) => data.posts,
onResponse({ response }) {
if (response.ok && response._data.posts.length > 0) {
// 追加新数据到现有列表
allPosts.value.push(...response._data.posts)
}
}
})
// 加载更多
const loadMore = () => {
page.value++
}
// 监听数据变化
watch(newPosts, (posts) => {
if (posts && page.value === 1) {
// 首页数据,替换现有数据
allPosts.value = posts
}
})
</script>
asyncData 与 fetch 对比
在 Nuxt 2 中,asyncData
和 fetch
是主要的数据获取方式。Nuxt 3 进行了重大改进,但理解它们的差异有助于迁移和优化。
Nuxt 2 vs Nuxt 3 对比
特性 | Nuxt 2 asyncData | Nuxt 2 fetch | Nuxt 3 useFetch |
---|---|---|---|
执行时机 | 页面组件渲染前 | 组件渲染前/后 | 灵活配置 |
数据合并 | 自动合并到 data | 手动管理 | 响应式引用 |
错误处理 | 页面级错误 | 组件级错误 | 细粒度控制 |
类型支持 | 有限 | 有限 | 完整 TypeScript |
缓存机制 | 无内置缓存 | 无内置缓存 | 自动缓存 |
迁移示例
// Nuxt 2 asyncData
export default {
async asyncData({ params, $http }) {
const post = await $http.$get(`/api/posts/${params.id}`)
const comments = await $http.$get(`/api/posts/${params.id}/comments`)
return {
post,
comments
}
}
}
// Nuxt 3 等价实现
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
const { data: comments } = useLazyFetch(`/api/posts/${route.params.id}/comments`)
</script>
// Nuxt 2 fetch
export default {
data() {
return {
posts: [],
loading: false
}
},
async fetch() {
this.loading = true
try {
this.posts = await this.$http.$get('/api/posts')
} finally {
this.loading = false
}
}
}
// Nuxt 3 等价实现
<script setup lang="ts">
const { data: posts, pending: loading } = await useFetch('/api/posts')
</script>
useAsyncData 与 useFetch 深度解析
useAsyncData
是更底层的数据获取组合函数,useFetch
实际上是基于 useAsyncData
构建的高级封装。
useAsyncData 核心机制
<script setup lang="ts">
// useAsyncData 底层用法
const { data, pending, error, refresh } = await useAsyncData('users', async () => {
// 自定义数据获取逻辑
const users = await $fetch('/api/users')
const profiles = await Promise.all(
users.map(user => $fetch(`/api/users/${user.id}/profile`))
)
return users.map((user, index) => ({
...user,
profile: profiles[index]
}))
})
// 复杂的数据处理
const { data: processedData } = await useAsyncData('analytics', async () => {
const [users, orders, products] = await Promise.all([
$fetch('/api/users'),
$fetch('/api/orders'),
$fetch('/api/products')
])
// 复杂的数据处理逻辑
return {
totalUsers: users.length,
totalRevenue: orders.reduce((sum, order) => sum + order.amount, 0),
topProducts: products
.sort((a, b) => b.sales - a.sales)
.slice(0, 5)
}
}, {
default: () => ({
totalUsers: 0,
totalRevenue: 0,
topProducts: []
})
})
</script>
useFetch 实现原理
// useFetch 的简化实现原理
function useFetch(url: string, options: FetchOptions = {}) {
return useAsyncData(options.key || url, () => {
return $fetch(url, options)
}, options)
}
// 自定义 useFetch 增强版
function useEnhancedFetch<T>(url: string, options: EnhancedFetchOptions<T> = {}) {
const {
transform,
validator,
retry = 3,
retryDelay = 1000,
...fetchOptions
} = options
return useAsyncData<T>(
options.key || url,
async () => {
let lastError: Error
for (let i = 0; i <= retry; i++) {
try {
const data = await $fetch(url, fetchOptions)
// 数据验证
if (validator && !validator(data)) {
throw new Error('数据验证失败')
}
// 数据转换
return transform ? transform(data) : data
} catch (error) {
lastError = error as Error
if (i < retry) {
await new Promise(resolve => setTimeout(resolve, retryDelay))
}
}
}
throw lastError!
},
{
...options,
default: options.default || (() => null)
}
)
}
选择策略
// 决策树:何时使用哪种方法
// 1. 简单的 API 调用 → useFetch
const { data: users } = await useFetch('/api/users')
// 2. 需要自定义数据处理逻辑 → useAsyncData
const { data: dashboard } = await useAsyncData('dashboard', async () => {
// 复杂的数据聚合逻辑
return await aggregateDashboardData()
})
// 3. 客户端动态调用 → $fetch
const handleSubmit = async () => {
const result = await $fetch('/api/submit', {
method: 'POST',
body: formData
})
}
// 4. 非阻塞数据 → useLazyFetch
const { data: recommendations } = useLazyFetch('/api/recommendations')
共享预渲染数据机制与性能优化
Nuxt 的数据获取机制包含了复杂的缓存和共享机制,理解这些机制对性能优化至关重要。
数据缓存机制
// 全局缓存配置
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
nitro: {
storage: {
cache: {
driver: 'redis', // 或者 'memory', 'fs'
host: 'localhost',
port: 6379
}
}
}
})
// 组件中的缓存控制
const { data: expensiveData } = await useFetch('/api/expensive-operation', {
key: 'expensive-data',
// 缓存 1 小时
getCachedData: (key) => {
return nuxtApp.ssrContext?.cache?.[key] ?? nuxtApp.payload.data[key]
},
// 自定义缓存策略
server: true,
lazy: false
})
预渲染数据共享
<!-- pages/posts/[id].vue -->
<script setup lang="ts">
// 服务端预渲染的数据会自动序列化到客户端
const { data: post } = await useFetch(`/api/posts/${route.params.id}`, {
key: `post-${route.params.id}` // 确保唯一的缓存键
})
// 子组件可以复用父组件的数据
const { data: author } = await useFetch(`/api/users/${post.value.authorId}`, {
key: `author-${post.value.authorId}`,
// 仅在没有缓存时执行
default: () => null
})
</script>
<template>
<div>
<PostContent :post="post" />
<AuthorInfo :author="author" />
</div>
</template>
性能优化策略
// 1. 数据预取策略
// 在路由切换前预取数据
const router = useRouter()
router.beforeEach(async (to) => {
if (to.name === 'post-detail') {
// 预取文章数据
await $fetch(`/api/posts/${to.params.id}`)
}
})
// 2. 智能缓存失效
const { data: posts, refresh } = await useFetch('/api/posts', {
key: 'posts-list',
// 基于时间戳的缓存失效
getCachedData: (key) => {
const cached = getCachedData(key)
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.data
}
return null
}
})
// 3. 条件数据获取
const user = useAuthUser()
const { data: privateData } = await useFetch('/api/private-data', {
// 只有认证用户才获取
execute: () => !!user.value,
server: false, // 私人数据不在服务端获取
key: `private-${user.value?.id}`
})
// 4. 数据分片加载
const { data: posts } = await useFetch('/api/posts', {
query: {
fields: 'id,title,excerpt', // 只获取必要字段
limit: 20
},
key: 'posts-summary'
})
// 详细数据延迟加载
const loadPostDetail = async (postId: string) => {
return await $fetch(`/api/posts/${postId}`, {
query: { fields: 'content,metadata,tags' }
})
}
性能要点:
- 合理使用缓存键避免重复请求
- 优先加载关键数据,延迟加载非关键数据
- 利用服务端渲染减少客户端请求
- 实现智能的缓存失效策略
服务端数据获取
服务端渲染中的数据获取
在 SSR 环境中,数据获取的执行环境和时机与客户端有显著差异,需要特别注意上下文和生命周期。
SSR 数据获取原理
// 服务端渲染流程
/*
1. 接收客户端请求
2. 执行页面组件的 setup()
3. 运行 useFetch/useAsyncData
4. 等待所有异步数据获取完成
5. 渲染 HTML 并序列化数据
6. 发送完整的 HTML 到客户端
7. 客户端 hydration,复用服务端数据
*/
// pages/posts/index.vue
<script setup lang="ts">
// 在服务端和客户端都会执行
const { data: posts, pending } = await useFetch('/api/posts', {
server: true, // 确保在服务端执行
key: 'posts-list',
// 服务端上下文访问
onRequest({ options }) {
// 添加服务端特有的请求头
if (process.server) {
options.headers = {
...options.headers,
'x-server-request': 'true',
'x-request-id': crypto.randomUUID()
}
}
}
})
// 仅在服务端执行的逻辑
if (process.server) {
console.log('运行在服务端')
// 可以访问服务端特有的 API
const serverOnlyData = await getServerSideData()
}
</script>
上下文数据传递
// 从服务端传递数据到客户端
// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
// 获取请求上下文
const headers = getHeaders(event)
const query = getQuery(event)
// 服务端特有的数据获取
const posts = await getPosts({
userId: headers['x-user-id'],
serverTime: new Date(),
requestId: headers['x-request-id']
})
return {
posts,
meta: {
total: posts.length,
serverTime: new Date().toISOString(),
requestId: headers['x-request-id']
}
}
})
// 页面组件中接收和使用
<script setup lang="ts">
const { data: postsData } = await useFetch('/api/posts')
// 服务端数据会自动序列化到客户端
console.log('服务端时间:', postsData.value.meta.serverTime)
console.log('请求ID:', postsData.value.meta.requestId)
</script>
SSR 特有的数据处理
// composables/useSSRData.ts
export const useSSRData = () => {
const nuxtApp = useNuxtApp()
// 服务端状态管理
const setServerState = (key: string, value: any) => {
if (process.server) {
nuxtApp.ssrContext!.payload.data[key] = value
}
}
const getServerState = (key: string) => {
if (process.server) {
return nuxtApp.ssrContext?.payload?.data[key]
} else {
return nuxtApp.payload.data[key]
}
}
// 服务端数据预处理
const preprocessServerData = async (data: any) => {
if (process.server) {
// 在服务端对数据进行预处理
return {
...data,
processedAt: new Date().toISOString(),
serverGenerated: true
}
}
return data
}
return {
setServerState,
getServerState,
preprocessServerData
}
}
// 使用示例
<script setup lang="ts">
const { preprocessServerData } = useSSRData()
const { data: processedPosts } = await useFetch('/api/posts', {
transform: preprocessServerData,
key: 'processed-posts'
})
</script>
数据缓存机制
Nuxt 提供了多层次的缓存机制,从内存缓存到持久化存储,满足不同的性能需求。
内置缓存策略
// nuxt.config.ts - 全局缓存配置
export default defineNuxtConfig({
nitro: {
// 路由级缓存
routeRules: {
'/': { prerender: true },
'/posts/**': { isr: 60 }, // 60秒 ISR 缓存
'/api/posts': {
headers: { 'Cache-Control': 's-maxage=60' }
}
},
// 存储引擎配置
storage: {
cache: {
driver: 'cloudflare-kv', // 分布式缓存
// driver: 'redis',
// driver: 'memory',
binding: 'CACHE'
}
}
}
})
// 组件级缓存控制
<script setup lang="ts">
const { data: cachedPosts } = await useFetch('/api/posts', {
key: 'posts-cache',
// 自定义缓存逻辑
getCachedData: (key) => {
return getCachedData(key, {
maxAge: 5 * 60 * 1000, // 5分钟缓存
staleWhileRevalidate: 24 * 60 * 60 * 1000 // 24小时 SWR
})
}
})
</script>
智能缓存策略
// composables/useSmartCache.ts
export const useSmartCache = () => {
const storage = useStorage()
// 多层缓存策略
const getWithCache = async <T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions = {}
): Promise<T> => {
const {
ttl = 5 * 60 * 1000, // 默认5分钟
staleTime = 2 * 60 * 1000, // 2分钟内认为是新鲜的
maxStale = 24 * 60 * 60 * 1000 // 最多使用24小时过期数据
} = options
const cacheKey = `cache:${key}`
const cached = await storage.getItem(cacheKey)
if (cached) {
const age = Date.now() - cached.timestamp
// 数据仍然新鲜
if (age < staleTime) {
return cached.data
}
// 数据过期但可用,后台更新
if (age < maxStale) {
// 后台异步更新
Promise.resolve().then(async () => {
try {
const fresh = await fetcher()
await storage.setItem(cacheKey, {
data: fresh,
timestamp: Date.now()
})
} catch (error) {
console.warn('后台缓存更新失败:', error)
}
})
return cached.data
}
}
// 获取新数据并缓存
try {
const fresh = await fetcher()
await storage.setItem(cacheKey, {
data: fresh,
timestamp: Date.now()
})
return fresh
} catch (error) {
// 如果有过期缓存,降级使用
if (cached) {
console.warn('API 请求失败,使用过期缓存:', error)
return cached.data
}
throw error
}
}
// 缓存失效
const invalidateCache = async (pattern: string) => {
const keys = await storage.getKeys(`cache:${pattern}`)
await Promise.all(keys.map(key => storage.removeItem(key)))
}
return {
getWithCache,
invalidateCache
}
}
// 使用智能缓存
<script setup lang="ts">
const { getWithCache } = useSmartCache()
const { data: posts } = await useAsyncData('smart-posts', () =>
getWithCache('posts-list', () => $fetch('/api/posts'), {
ttl: 10 * 60 * 1000, // 10分钟缓存
staleTime: 5 * 60 * 1000 // 5分钟内认为新鲜
})
)
</script>
缓存同步和失效
// stores/cache.ts
export const useCacheStore = defineStore('cache', () => {
const pendingInvalidations = ref<Set<string>>(new Set())
// 全局缓存失效
const invalidatePattern = async (pattern: string) => {
if (pendingInvalidations.value.has(pattern)) return
pendingInvalidations.value.add(pattern)
try {
// 客户端缓存失效
await clearNuxtData(pattern)
// 服务端缓存失效(如果需要)
if (process.client) {
await $fetch('/api/cache/invalidate', {
method: 'POST',
body: { pattern }
})
}
} finally {
pendingInvalidations.value.delete(pattern)
}
}
// 条件性缓存失效
const conditionalInvalidate = async (conditions: CacheInvalidationCondition[]) => {
for (const condition of conditions) {
if (await condition.check()) {
await invalidatePattern(condition.pattern)
}
}
}
return {
invalidatePattern,
conditionalInvalidate
}
})
// 使用示例
<script setup lang="ts">
const cacheStore = useCacheStore()
// 数据更新后失效相关缓存
const updatePost = async (postId: string, data: PostUpdateData) => {
await $fetch(`/api/posts/${postId}`, {
method: 'PUT',
body: data
})
// 失效相关缓存
await cacheStore.invalidatePattern(`post-${postId}`)
await cacheStore.invalidatePattern('posts-list')
await cacheStore.invalidatePattern('posts-*')
}
</script>
错误处理与重试
健壮的错误处理机制是生产环境应用的关键,需要考虑网络异常、服务器错误、数据验证失败等多种场景。
全局错误处理
// plugins/error-handler.client.ts
export default defineNuxtPlugin(() => {
// 全局 API 错误处理
const { $fetch } = useNuxtApp()
// 拦截所有 $fetch 错误
$fetch.create({
onResponseError({ response, error }) {
const errorStore = useErrorStore()
// 根据错误类型分类处理
switch (response.status) {
case 401:
// 未授权,跳转登录
navigateTo('/login')
break
case 403:
// 权限不足
errorStore.setError({
type: 'permission',
message: '权限不足,请联系管理员'
})
break
case 429:
// 请求限制
errorStore.setError({
type: 'rate-limit',
message: '请求过于频繁,请稍后再试'
})
break
case 500:
// 服务器错误
errorStore.setError({
type: 'server',
message: '服务器错误,请稍后重试'
})
break
default:
// 其他错误
errorStore.setError({
type: 'unknown',
message: error.message || '请求失败'
})
}
}
})
})
// stores/error.ts
export const useErrorStore = defineStore('error', () => {
const errors = ref<ErrorInfo[]>([])
const setError = (error: ErrorInfo) => {
errors.value.push({
...error,
id: crypto.randomUUID(),
timestamp: Date.now()
})
// 自动清除错误(5秒后)
setTimeout(() => {
clearError(error.id)
}, 5000)
}
const clearError = (id: string) => {
const index = errors.value.findIndex(e => e.id === id)
if (index > -1) {
errors.value.splice(index, 1)
}
}
return {
errors: readonly(errors),
setError,
clearError
}
})
智能重试机制
// composables/useRetryableFetch.ts
export const useRetryableFetch = <T>(
url: string,
options: RetryableFetchOptions<T> = {}
) => {
const {
retries = 3,
retryDelay = 1000,
retryCondition = (error) => error.status >= 500,
exponentialBackoff = true,
maxDelay = 30000,
onRetry,
...fetchOptions
} = options
return useAsyncData<T>(
options.key || url,
async () => {
let lastError: any
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await $fetch<T>(url, fetchOptions)
} catch (error: any) {
lastError = error
// 检查是否应该重试
if (attempt < retries && retryCondition(error)) {
// 计算延迟时间
let delay = retryDelay
if (exponentialBackoff) {
delay = Math.min(retryDelay * Math.pow(2, attempt), maxDelay)
}
// 触发重试回调
onRetry?.(error, attempt + 1, delay)
// 等待后重试
await new Promise(resolve => setTimeout(resolve, delay))
continue
}
throw error
}
}
throw lastError
},
{
...options,
server: options.server ?? true
}
)
}
// 使用示例
<script setup lang="ts">
const { data: criticalData, error, pending } = useRetryableFetch('/api/critical-data', {
retries: 5,
retryDelay: 2000,
exponentialBackoff: true,
retryCondition: (error) => {
// 只重试服务器错误和网络错误
return error.status >= 500 || error.name === 'NetworkError'
},
onRetry: (error, attempt, delay) => {
console.log(`重试第 ${attempt} 次,延迟 ${delay}ms`, error.message)
}
})
</script>
降级和容错机制
// composables/useFallbackData.ts
export const useFallbackData = <T>(
primaryFetcher: () => Promise<T>,
fallbackStrategies: FallbackStrategy<T>[],
options: FallbackOptions<T> = {}
) => {
const { defaultValue, timeout = 10000 } = options
return useAsyncData<T>(
options.key || 'fallback-data',
async () => {
// 超时控制
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), timeout)
})
try {
// 尝试主要数据源
return await Promise.race([primaryFetcher(), timeoutPromise])
} catch (primaryError) {
console.warn('主要数据源失败:', primaryError)
// 尝试降级策略
for (const strategy of fallbackStrategies) {
try {
const result = await strategy.execute()
console.info(`使用降级策略: ${strategy.name}`)
return result
} catch (fallbackError) {
console.warn(`降级策略 ${strategy.name} 失败:`, fallbackError)
}
}
// 所有策略都失败,返回默认值或抛出错误
if (defaultValue !== undefined) {
console.info('使用默认值')
return defaultValue
}
throw primaryError
}
},
options
)
}
// 使用示例
<script setup lang="ts">
const { data: posts } = useFallbackData(
// 主要数据源
() => $fetch('/api/posts'),
// 降级策略
[
{
name: 'cached-data',
execute: () => getCachedPosts()
},
{
name: 'backup-api',
execute: () => $fetch('/api/backup/posts')
},
{
name: 'local-storage',
execute: () => getPostsFromLocalStorage()
}
],
// 配置
{
key: 'posts-with-fallback',
defaultValue: [],
timeout: 5000
}
)
</script>
容错要点:
- 实现多层次的错误处理机制
- 提供合理的降级策略
- 避免错误级联影响用户体验
- 记录错误信息用于问题排查
API 路由
server/api 目录结构
Nuxt 的 API 路由采用基于文件的路由系统,提供了一套完整的服务端 API 开发解决方案。
基础目录结构
server/
├── api/
│ ├── auth/
│ │ ├── login.post.ts # POST /api/auth/login
│ │ ├── logout.post.ts # POST /api/auth/logout
│ │ ├── refresh.post.ts # POST /api/auth/refresh
│ │ └── profile.get.ts # GET /api/auth/profile
│ ├── users/
│ │ ├── index.get.ts # GET /api/users
│ │ ├── index.post.ts # POST /api/users
│ │ ├── [id].get.ts # GET /api/users/:id
│ │ ├── [id].put.ts # PUT /api/users/:id
│ │ ├── [id].delete.ts # DELETE /api/users/:id
│ │ └── [id]/
│ │ ├── posts.get.ts # GET /api/users/:id/posts
│ │ └── avatar.put.ts # PUT /api/users/:id/avatar
│ ├── posts/
│ │ ├── index.get.ts # GET /api/posts
│ │ ├── index.post.ts # POST /api/posts
│ │ ├── [slug].get.ts # GET /api/posts/:slug
│ │ ├── [slug].put.ts # PUT /api/posts/:slug
│ │ ├── [slug].delete.ts # DELETE /api/posts/:slug
│ │ └── [...path].get.ts # GET /api/posts/** (通配符路由)
│ ├── upload.post.ts # POST /api/upload
│ ├── search.get.ts # GET /api/search
│ └── health.get.ts # GET /api/health
├── middleware/
│ ├── auth.ts # 认证中间件
│ ├── cors.ts # CORS 中间件
│ └── rateLimit.ts # 限流中间件
└── utils/
├── db.ts # 数据库工具
├── validation.ts # 数据验证
└── response.ts # 响应格式化
路由规则详解
// 文件名规则与对应路由
/*
index.get.ts → GET /api/
users.get.ts → GET /api/users
users/index.get.ts → GET /api/users/
users/[id].get.ts → GET /api/users/:id
users/[...path].get.ts → GET /api/users/**
*/
// 支持的 HTTP 方法
// .get.ts → GET
// .post.ts → POST
// .put.ts → PUT
// .patch.ts → PATCH
// .delete.ts → DELETE
// .head.ts → HEAD
// .options.ts → OPTIONS
// 通用处理器(支持所有方法)
// server/api/users.ts
export default defineEventHandler(async (event) => {
const method = getMethod(event)
switch (method) {
case 'GET':
return await getUsers(event)
case 'POST':
return await createUser(event)
case 'PUT':
return await updateUser(event)
case 'DELETE':
return await deleteUser(event)
default:
throw createError({
statusCode: 405,
statusMessage: 'Method Not Allowed'
})
}
})
创建 API 端点
基础 API 端点
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
try {
// 获取查询参数
const query = getQuery(event)
const { page = 1, limit = 10, search, sort } = query
// 数据验证
const validatedQuery = await validateQuery(query, {
page: z.number().int().min(1).optional(),
limit: z.number().int().min(1).max(100).optional(),
search: z.string().optional(),
sort: z.enum(['name', 'email', 'created_at']).optional()
})
// 业务逻辑
const users = await getUsersList({
page: Number(validatedQuery.page),
limit: Number(validatedQuery.limit),
search: validatedQuery.search,
sort: validatedQuery.sort
})
// 统一响应格式
return {
success: true,
data: users.data,
meta: {
total: users.total,
page: Number(validatedQuery.page),
limit: Number(validatedQuery.limit),
totalPages: Math.ceil(users.total / Number(validatedQuery.limit))
}
}
} catch (error) {
// 错误处理
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Internal Server Error'
})
}
})
动态路由参数
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
// 获取路由参数
const userId = getRouterParam(event, 'id')
// 参数验证
if (!userId || isNaN(Number(userId))) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid user ID'
})
}
// 获取用户数据
const user = await getUserById(Number(userId))
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
return {
success: true,
data: user
}
})
// server/api/users/[id]/posts.get.ts
export default defineEventHandler(async (event) => {
const userId = getRouterParam(event, 'id')
const query = getQuery(event)
// 验证用户存在
const user = await getUserById(Number(userId))
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
// 获取用户文章
const posts = await getUserPosts(Number(userId), {
page: Number(query.page) || 1,
limit: Number(query.limit) || 10,
status: query.status
})
return {
success: true,
data: posts
}
})
复杂业务逻辑处理
// server/api/posts/index.post.ts
export default defineEventHandler(async (event) => {
// 检查认证
const user = await requireAuthenticatedUser(event)
// 获取请求体
const body = await readBody(event)
// 数据验证
const validatedData = await validateCreatePost(body)
// 权限检查
if (!hasPermission(user, 'create_post')) {
throw createError({
statusCode: 403,
statusMessage: 'Insufficient permissions'
})
}
// 开始事务
const db = await getDatabase()
const transaction = await db.transaction()
try {
// 创建文章
const post = await transaction.posts.create({
...validatedData,
authorId: user.id,
status: 'draft',
createdAt: new Date()
})
// 处理标签
if (validatedData.tags?.length > 0) {
await transaction.postTags.createMany(
validatedData.tags.map(tag => ({
postId: post.id,
tagId: tag.id
}))
)
}
// 处理附件
if (validatedData.attachments?.length > 0) {
await processAttachments(post.id, validatedData.attachments)
}
// 提交事务
await transaction.commit()
// 异步任务
await Promise.all([
// 发送通知
sendNotification(user.id, 'post_created', { postId: post.id }),
// 更新搜索索引
updateSearchIndex('posts', post),
// 清理缓存
invalidateCache(['posts', `user:${user.id}:posts`])
])
return {
success: true,
data: post,
message: 'Post created successfully'
}
} catch (error) {
// 回滚事务
await transaction.rollback()
// 记录错误
console.error('创建文章失败:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to create post'
})
}
})
请求处理与响应格式
统一请求处理中间件
// server/middleware/request-handler.ts
export default defineEventHandler(async (event) => {
// 只处理 API 路由
if (!event.node.req.url?.startsWith('/api/')) {
return
}
// 设置 CORS 头
setHeaders(event, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
})
// 处理 OPTIONS 请求
if (getMethod(event) === 'OPTIONS') {
event.node.res.statusCode = 200
return ''
}
// 请求日志
const startTime = Date.now()
const requestId = crypto.randomUUID()
// 设置请求上下文
event.context.requestId = requestId
event.context.startTime = startTime
console.log(`[${requestId}] ${getMethod(event)} ${event.node.req.url}`)
// 在响应结束时记录耗时
event.node.res.on('finish', () => {
const duration = Date.now() - startTime
console.log(`[${requestId}] ${event.node.res.statusCode} ${duration}ms`)
})
})
// server/middleware/error-handler.ts
export default defineEventHandler(async (event) => {
try {
// 继续处理其他中间件
return
} catch (error: any) {
// 统一错误处理
const statusCode = error.statusCode || 500
const message = error.message || 'Internal Server Error'
// 记录错误
console.error(`[${event.context.requestId}] 错误:`, error)
// 返回统一错误格式
setResponseStatus(event, statusCode)
return {
success: false,
error: {
code: error.code || 'INTERNAL_ERROR',
message,
requestId: event.context.requestId,
timestamp: new Date().toISOString()
}
}
}
})
响应格式标准化
// server/utils/response.ts
export interface ApiResponse<T = any> {
success: boolean
data?: T
error?: {
code: string
message: string
details?: any
requestId?: string
timestamp?: string
}
meta?: {
total?: number
page?: number
limit?: number
totalPages?: number
hasNext?: boolean
hasPrev?: boolean
}
}
// 成功响应
export const successResponse = <T>(
data: T,
meta?: ApiResponse<T>['meta']
): ApiResponse<T> => ({
success: true,
data,
meta
})
// 错误响应
export const errorResponse = (
code: string,
message: string,
details?: any,
statusCode: number = 500
): never => {
throw createError({
statusCode,
data: {
success: false,
error: {
code,
message,
details,
timestamp: new Date().toISOString()
}
}
})
}
// 分页响应
export const paginatedResponse = <T>(
data: T[],
total: number,
page: number,
limit: number
): ApiResponse<T[]> => ({
success: true,
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
})
// 使用示例
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 10
const { posts, total } = await getPosts({ page, limit })
return paginatedResponse(posts, total, page, limit)
})
高级响应处理
// server/utils/advanced-response.ts
export class ResponseBuilder<T = any> {
private response: Partial<ApiResponse<T>> = { success: true }
constructor(private event: H3Event) {}
data(data: T): this {
this.response.data = data
return this
}
meta(meta: ApiResponse<T>['meta']): this {
this.response.meta = meta
return this
}
cache(maxAge: number): this {
setHeader(this.event, 'Cache-Control', `max-age=${maxAge}`)
return this
}
etag(value: string): this {
setHeader(this.event, 'ETag', value)
return this
}
location(url: string): this {
setHeader(this.event, 'Location', url)
return this
}
status(code: number): this {
setResponseStatus(this.event, code)
return this
}
build(): ApiResponse<T> {
return this.response as ApiResponse<T>
}
}
// 流式响应处理
export const streamResponse = (event: H3Event, generator: AsyncGenerator<any>) => {
setHeader(event, 'Content-Type', 'text/plain')
setHeader(event, 'Transfer-Encoding', 'chunked')
return sendStream(event, generator)
}
// 使用示例
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const postId = getRouterParam(event, 'id')
const post = await getPostById(Number(postId))
if (!post) {
throw createError({
statusCode: 404,
statusMessage: 'Post not found'
})
}
// 生成 ETag
const etag = generateEtag(post)
const clientEtag = getHeader(event, 'if-none-match')
if (clientEtag === etag) {
return new ResponseBuilder(event)
.status(304)
.etag(etag)
.build()
}
return new ResponseBuilder(event)
.data(post)
.cache(300) // 缓存5分钟
.etag(etag)
.build()
})
// 文件上传响应
// server/api/upload.post.ts
export default defineEventHandler(async (event) => {
const files = await readMultipartFormData(event)
const uploadResults = []
for (const file of files || []) {
if (file.type?.startsWith('image/')) {
const result = await uploadFile(file)
uploadResults.push(result)
}
}
return new ResponseBuilder(event)
.status(201)
.data(uploadResults)
.location(`/api/files/${uploadResults[0]?.id}`)
.build()
})
API 设计最佳实践:
- 遵循 RESTful 设计原则
- 实现统一的响应格式
- 提供完整的错误处理机制
- 支持请求验证和安全控制
- 实现合理的缓存策略
总结
本文深度解析了 Nuxt 框架中的数据获取与 API 调用机制,涵盖了从基础的客户端请求到复杂的服务端渲染数据获取的完整生态系统。
核心要点回顾
- 数据获取方式选择
$fetch
:适用于客户端动态调用useFetch
:组件级响应式数据获取的首选useLazyFetch
:非阻塞数据获取,提升首屏性能useAsyncData
:底层 API,适用于复杂数据处理场景
- 服务端渲染优化
- 利用数据预渲染机制减少客户端请求
- 实现多层次缓存策略提升性能
- 构建健壮的错误处理和重试机制
- API 路由开发
- 基于文件的路由系统,直观且可维护
- 统一的请求处理和响应格式
- 完整的中间件和错误处理机制
性能优化建议
- 合理使用缓存键避免重复请求
- 优先加载关键数据,延迟加载非关键数据
- 实现智能的缓存失效策略
- 利用服务端渲染减少客户端网络请求
开发最佳实践
- 始终使用 TypeScript 确保类型安全
- 实现统一的错误处理和用户反馈机制
- 设计合理的降级策略保证可用性
- 遵循 RESTful API 设计原则
通过掌握这些核心技术和最佳实践,中高级前端开发者可以构建出高性能、可维护的现代 Web 应用,充分发挥 Nuxt 框架的技术优势。