Nuxt3 封装Http请求客户端及服务端水合解决方案
一、Http请求客户端封装
1.1 核心方案选择
Nuxt3推荐使用内置的$fetch
和useFetch
进行数据请求,替代传统的Axios。$fetch
基于ofetch
库实现,提供了更好的TypeScript支持和内置功能。根据场景不同,可选择以下两种封装方式:
- 服务端渲染(SSR)场景:使用
useFetch
或useAsyncData
,自动处理数据水合 - 客户端交互场景:使用
$fetch
,适合用户触发的异步操作
1.2 完整封装实现
创建请求基础配置(utils/request.ts)
import type { UseFetchOptions } from 'nuxt/app'
import { useNuxtApp } from '#app'
// 响应数据类型定义
export interface IResultData<T> {
code: number
data: T
msg: string
}
// 基础URL配置(从环境变量获取)
const getBaseURL = () => {
const config = useRuntimeConfig()
return process.server ? config.apiServer : config.public.apiBase
}
/**
* 通用请求封装
*/
class HttpRequest {
async request<T = any>(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data?: any,
options?: UseFetchOptions<T>
) {
const nuxtApp = useNuxtApp()
const token = useCookie('token') // 从cookie获取token
// 基础配置
const newOptions: UseFetchOptions<T> = {
baseURL: getBaseURL(),
method,
headers: {
'Content-Type': 'application/json',
...(token.value ? { Authorization: `Bearer ${token.value}` } : {})
},
...options
}
// 根据请求方法处理数据
if (method === 'GET' || method === 'DELETE') {
newOptions.params = data
} else {
newOptions.body = data
}
// 选择合适的请求方法(服务端/客户端)
const fetchMethod = process.server ? useFetch : nuxtApp.$fetch
return new Promise<IResultData<T>>((resolve, reject) => {
fetchMethod(url, newOptions)
.then((res: any) => {
// 处理响应数据
const result = res._data || res.data?.value
if (result.code === 200) {
resolve(result)
} else {
reject(new Error(result.msg || '请求失败'))
}
})
.catch((error: any) => {
// 错误处理
let errorMessage = '服务端错误'
if (error.response?._data?.msg) {
errorMessage = error.response._data.msg
} else if (error.message) {
errorMessage = error.message
}
reject(new Error(errorMessage))
})
})
}
// GET请求
get<T = any>(url: string, params?: any, options?: UseFetchOptions<T>) {
return this.request<T>(url, 'GET', params, options)
}
// POST请求
post<T = any>(url: string, data?: any, options?: UseFetchOptions<T>) {
return this.request<T>(url, 'POST', data, options)
}
// PUT请求
put<T = any>(url: string, data?: any, options?: UseFetchOptions<T>) {
return this.request<T>(url, 'PUT', data, options)
}
// DELETE请求
delete<T = any>(url: string, params?: any, options?: UseFetchOptions<T>) {
return this.request<T>(url, 'DELETE', params, options)
}
}
export default new HttpRequest()
配置环境变量(nuxt.config.ts)
export default defineNuxtConfig({
runtimeConfig: {
// 服务端专用配置
apiServer: process.env.NUXT_API_SERVER || 'https://api.example.com',
// 客户端和服务端共享配置
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
},
// 开发环境代理配置
nitro: {
devProxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
// pathRewrite: { '^/api': '' }
}
}
}
})
创建API调用示例(api/user.ts)
import http from '@/utils/request'
export interface UserInfo {
id: number
name: string
avatar: string
}
export const userApi = {
// 获取用户信息
getUserInfo: (id: number) => {
return http.get<IResultData<UserInfo>>(`/user/${id}`, {
// 配置缓存key,避免水合问题
key: `user_${id}`
})
},
// 更新用户信息
updateUserInfo: (data: Partial<UserInfo>) => {
return http.post<IResultData<UserInfo>>('/user/update', data)
}
}
1.3 请求拦截器实现
通过创建自定义composable实现拦截器功能(composables/useAuthFetch.ts):
import type { UseFetchOptions } from 'nuxt/app'
import { $fetch } from 'ofetch'
export function useAuthFetch<T>(url: string, options: UseFetchOptions<T> = {}) {
const token = useCookie('token')
// 创建自定义fetch实例
const customFetch = $fetch.create({
onRequest({ options }) {
// 请求拦截器:添加认证头
if (token.value) {
options.headers = options.headers || {}
options.headers.Authorization = `Bearer ${token.value}`
}
},
onResponse({ response }) {
// 响应拦截器:处理响应数据
console.log('Response:', response.status, response.url)
},
onResponseError({ response }) {
// 错误处理拦截器
if (response.status === 401) {
// 处理未授权错误,例如重定向到登录页
navigateTo('/login')
}
}
})
// 返回useFetch并使用自定义fetch
return useFetch(url, {
...options,
$fetch: customFetch
})
}
二、服务端水合解决方案
2.1 水合机制概述
Nuxt3的服务端水合(Hydration)是指:
- 服务端渲染(SSR)生成完整HTML页面
- 客户端加载JavaScript并"激活"页面,使静态HTML变为动态DOM
- 确保服务端和客户端数据状态一致
常见问题:Hydration Mismatch(水合不匹配),表现为服务端渲染的HTML与客户端生成的DOM结构不一致。
2.2 水合不匹配的解决方案
方案1:使用唯一key确保缓存一致性
// 错误示例:可能导致水合不匹配
const { data } = await useFetch('/api/user')
// 正确示例:提供唯一key
const { data } = await useFetch('/api/user', {
key: 'user-data' // 固定key确保缓存一致
})
// 动态参数场景:使用参数生成唯一key
const { id } = useRoute().params
const { data } = await useFetch(`/api/user/${id}`, {
key: `user-${id}` // 动态生成key
})
方案2:处理动态数据和浏览器API
<template>
<div>
<!-- 使用client-only包裹客户端特有内容 -->
<client-only>
<div class="client-content">
{{ clientData }}
</div>
</client-only>
</div>
</template>
<script setup>
// 服务端安全的数据获取
const { data: serverData } = await useFetch('/api/data', {
key: 'server-data'
})
// 客户端特有数据
const clientData = ref(null)
// 在onMounted中使用浏览器API
onMounted(() => {
clientData.value = window.someClientAPI()
})
</script>
方案3:使用useState共享状态
// composables/useCounter.ts
export const useCounter = () => {
// 使用useState确保服务端和客户端状态一致
return useState('counter', () => 0)
}
// 在页面中使用
const counter = useCounter()
// 在服务端修改
if (process.server) {
counter.value = 10
}
方案4:处理异步数据加载状态
<template>
<div>
<!-- 处理加载状态 -->
<div v-if="pending">加载中...</div>
<!-- 数据加载完成后渲染 -->
<div v-else-if="data">
{{ data }}
</div>
<!-- 错误处理 -->
<div v-else-if="error">
错误: {{ error.message }}
</div>
</div>
</template>
<script setup>
const { data, pending, error } = await useFetch('/api/data', {
key: 'page-data',
// 设置默认值避免null
default: () => ({})
})
</script>
2.3 高级水合技巧
区分服务端/客户端请求
// composables/useRequest.ts
export const useServerRequest = (url: string, options = {}) => {
// 仅服务端请求
return useFetch(url, {
...options,
server: true, // 强制服务端请求
lazy: false // 阻塞导航直到请求完成
})
}
export const useClientRequest = (url: string, options = {}) => {
// 仅客户端请求
return useFetch(url, {
...options,
server: false, // 强制客户端请求
lazy: true // 不阻塞导航
})
}
使用transform处理响应数据
const { data } = await useFetch('/api/users', {
key: 'users-list',
// 转换数据格式,避免服务端和客户端处理不一致
transform: (input) => {
return input.data.map(user => ({
id: user.id,
name: user.name,
// 统一处理日期格式
createdAt: new Date(user.createdAt).toLocaleString()
}))
}
})
三、完整使用示例
3.1 页面组件中使用
<template>
<div class="user-profile">
<h1>{{ user?.name }}的个人资料</h1>
<div v-if="pending">加载中...</div>
<div v-else-if="error">获取数据失败: {{ error.message }}</div>
<div v-else>
<img :src="user.avatar" alt="头像">
<p>ID: {{ user.id }}</p>
<p>注册时间: {{ user.registeredAt }}</p>
</div>
<button @click="updateProfile">更新资料</button>
</div>
</template>
<script setup lang="ts">
import { userApi } from '@/api/user'
import { useCounter } from '@/composables/useCounter'
// 路由参数
const route = useRoute()
const userId = Number(route.params.id)
// 共享状态
const counter = useCounter()
// 服务端获取用户数据
const { data: userData, pending, error } = await useAuthFetch<IResultData<UserInfo>>(`/user/${userId}`, {
key: `user-${userId}`
})
const user = computed(() => userData?.data || {})
// 客户端交互
const updateProfile = async () => {
try {
const result = await userApi.updateUserInfo({
id: userId,
name: '新名称'
})
// 更新本地数据
if (result.code === 200) {
userData.value.data = result.data
}
} catch (err) {
console.error('更新失败:', err)
}
}
</script>
3.2 处理复杂水合场景
<template>
<div class="dashboard">
<h1>数据仪表盘</h1>
<!-- 使用client-only处理图表等客户端组件 -->
<client-only>
<ChartComponent :data="chartData" />
</client-only>
<!-- 服务端渲染的静态内容 -->
<div class="stats">
<div class="stat-item">
<h3>总用户数</h3>
<p>{{ stats?.totalUsers }}</p>
</div>
<div class="stat-item">
<h3>今日新增</h3>
<p>{{ stats?.todayNew }}</p>
</div>
</div>
</div>
</template>
<script setup>
// 服务端获取统计数据
const { data: stats } = await useFetch('/api/stats', {
key: 'dashboard-stats',
// 自定义缓存策略
getCachedData: (key) => {
// 开发环境不缓存
if (process.dev) return null
return useNuxtApp().payload.data[key]
}
})
// 客户端处理图表数据
const chartData = ref(null)
onMounted(async () => {
// 客户端动态获取详细数据
const result = await $fetch('/api/chart-data', {
params: { period: '7d' }
})
chartData.value = result.data
})
</script>
四、最佳实践总结
4.1 Http请求封装最佳实践
- 统一基础配置:通过环境变量和运行时配置管理API地址
- 区分请求场景:SSR场景用
useFetch
,客户端交互用$fetch
- 完善错误处理:统一处理网络错误、业务错误和权限错误
- 合理使用缓存:通过key参数控制缓存策略,避免重复请求
- 类型安全:使用TypeScript接口定义请求和响应类型
4.2 服务端水合最佳实践
-
避免服务端/客户端差异代码:
- 不在setup中使用浏览器API
- 避免使用随机数或时间相关逻辑
-
处理动态数据:
- 使用唯一key标识请求
- 提供默认值避免null
- 使用transform统一数据处理
-
优化水合性能:
- 减少服务端不必要的数据获取
- 使用lazy模式处理非关键数据
- 合理拆分组件,使用
<client-only>
-
调试水合问题:
- 开启Nuxt开发工具调试
- 使用
nuxt dev --debug
查看详细日志 - 检查DOM结构不一致的组件