**数据获取(Data Fetching)**是最核心的一环。如果你用传统的axios放在onMounted 里请求,爬虫抓到的依然是一个空壳,SEO 照样没戏。
Nuxt 提供了四个核心工具:useFetch、useAsyncData、$fetch 和useLazyFetch。
useFetch
它是 Nuxt 获取数据最简单的方式,会自动处理服务端渲染序列化(即:在服务器请求一次,把结果塞进 HTML,客户端直接复用,不会在浏览器里重复请求)。
<script setup lang="ts">
interface BaseResponse<T> {
code: number
data: T
msg: string
}
interface ArticleType {
title: string
content: string
}
const {data, pending, error} = await useFetch<BaseResponse<ArticleType>>("http://127.0.0.1:4523/m1/8122291-7879433-default/api/article/detail")
</script>
<template>
<div v-if="pending">加载中...</div>
<div v-else-if="error">出错了:{{ error.message }}</div>
<div v-else>
<h1>{{ data.data.title }}</h1>
<p>{{ data.data.content }}</p>
</div>
</template>
<style scoped>
</style>
配置baseurl
直接写绝对路径不太容易维护,我们可以只写相对路径,通过代理去请求url
const {data, pending, error} = await useFetch<BaseResponse<ArticleType>>("/api/article/detail")
然后在nuxt.config.ts中配置nitro
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
nitro: {
devProxy: {
// 当你请求 /api 时,Nitro 会自动转发到你的 Mock 地址,只在csr下生效
"/api": {
target: "http://127.0.0.1:4523/m1/8122291-7879433-default/api",
changeOrigin: true,
}
},
// 如果是线上环境,使用 routeRules 进行转发 (SSR 模式下非常强),ssr和csr都生效
routeRules: {
'/api/**': { proxy: 'http://127.0.0.1:4523/m1/8122291-7879433-default/api/**' }
}
}
})
带参数
const { data } = await useFetch('/api/article/update', {
method: 'POST',
query: { id: 100 }, // URL 参数
body: {
title: '更新后的标题' // Body 参数
}
})
pick
很多时候后端接口返回的对象非常大(比如包含create_time,update_time,author_email等),但你前端只需要title和content。
const { data } = await useFetch('/api/article', {
pick: ['title', 'content', 'id'] // 只保留这三个字段进入 HTML
})
pick只能选第一层
transform数据转换
对应大部分的api数据都是嵌套结构,code data msg这样的,pick只能选第一层,所以我们得用transform处理数据
const { data } = await useFetch('/api/article', {
transform: (res) => ({
title: res.data.title,
content: res.data.content
})
})
// 此时 HTML 里的 __NUXT_DATA__ 只会存:{ title: "...", content: "..."
refresh函数
- 知识点:
useFetch的第一个参数其实是它的key。 - **常见坑:**如果你在同一个页面多次调用同一个接口,由于
key相同,数据可能不会更新。
const { data, refresh } = await useFetch('/api/article')
// 当点击“刷新”按钮时调用 refresh()
useLazyFetch
useLazyFetch是useFetch的一个变体,它的核心逻辑只有一句话:“先跳页面,再加载数据。”
通常情况下,Nuxt 的useFetch会阻塞路由跳转。也就是说,如果你在setup里用了await useFetch,用户点击链接后,浏览器会“卡”在当前页,直到新页面的数据请求完成后才执行跳转。
useLazyFetch 则打破了这种阻塞。
useLazyFetch等价于useFetch(url, { lazy: true })。
它的行为特征如下:
- 非阻塞跳转: 路由会立即切换,组件会立即挂载。
- 前台异步: 数据在后台抓取,抓取过程中页面已经展示出来了。
- **响应式:**它返回的
pending属性会从true变为false,你可以据此展示加载动画(Loading)。
如果你有一个页面数据量很大(比如复杂的报表),接口可能需要 2 秒才能返回。
- 用
useFetch****: 用户点击后,页面卡住 2 秒没反应,用户以为点错了。 - 用
useLazyFetch****: 页面秒开,用户看到一个转圈圈或骨架屏,心理预期更舒服。
<script setup>
const { pending, data: posts } = useLazyFetch('/api/posts')
</script>
<template>
<div v-if="pending">
加载中... (或者显示骨架屏)
</div>
<div v-else>
<div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</div>
</template>
注意:
使用useLazyFetch时,你必须处理**“数据不存在”** 的状态。
- **处理空数据:**因为组件是立即渲染的,此时
data.value为null。如果你在模板里直接访问data.user.name而不加v-if或可选链(?.),页面会直接报错挂掉。 - **Watch 监听:**如果你需要根据获取到的数据做进一步处理,通常需要配合
watch来监听data的变化,因为在setup执行的那一刻,数据还没回来。
const { data } = useLazyFetch('/api/user')
// ❌ 这样做拿不到数据
console.log(data.value)
// ✅ 这样做才能在数据回来时处理
watch(data, (newData) => {
if (newData) {
console.log('数据到货了:', newData)
}
})
总结:该选哪一个?
| 需求 | 推荐工具 |
|---|---|
| SEO 至上,数据必须在首屏渲染出来 | useFetch |
| 追求极致跳转速度,允许先白屏/显示 Loading | useLazyFetch |
| 数据请求极快(< 100ms) | useFetch (用户无感知) |
| 数据请求很慢(> 500ms) | useLazyFetch (避免页面假死) |
如果你需要这个请求在csr阶段,可以加一个server:false
const {data, pending, status, error, refresh} = useLazyFetch<userType>("/api/user/await", {
server: false,
})
useAsyncData
useAsyncData是一个底层的数据获取工具。它不限制你获取数据的方式,你可以用它包裹任何返回 Promise 的逻辑(比如数据库查询、调用第三方 SDK 或甚至是定时器)。
语法结构
const { data, pending, error, refresh } = await useAsyncData(
'key-unique', // 唯一的键,用于确保跨请求的数据缓存
() => $fetch('https://api.example.com/data') // 异步逻辑
)
一般用它处理复杂的聚合逻辑
如果你需要在一个 Hook 里完成多个操作,比如:
- 先从 API A 获取 ID。
- 再用 ID 去 API B 获取详情。
- 最后返回合并后的结果。
const { data } = await useAsyncData('composite-data', async () => {
const user = await $fetch('/api/user')
const posts = await $fetch(`/api/posts/${user.id}`)
return { user, posts }
})
核心差异对比
| 特性 | useFetch | useAsyncData |
|---|---|---|
| 本质 | 专为 HTTP 请求设计。 | 通用的异步包装器。 |
| 便利性 | 高。自动生成 key,直接传入 URL 即可。 | 中。需要手动提供 key(除非你不在乎缓存)。 |
| 灵活度 | 低。只能处理基于 URL 的请求。 | 高。可以执行任何 Promise 逻辑。 |
| 使用场景 | 90% 的标准 API 调用。 | 复杂逻辑、多个请求合并、调用外部 SDK(如 Firebase)。 |
$fetch
简单来说,只要这个请求不是为了在页面一打开时就把内容渲染出来,而是为了响应某个动作,那就该$fetch 上场了。
用法类似axios,我们可以把它的使用场景归纳为**“非页面初始化”**的所有情况。
注意:不要在
setup顶层直接写$fetch。这会导致 SSR 和客户端各跑一次请求!
- 提交数据(POST / PUT / DELETE)
这是$fetch 最经典的操作。比如用户注册、发表评论或删除订单。这些操作不需要 SSR(服务器渲染),只需要在点击时执行一次。
const submitForm = async () => {
const response = await $fetch('/api/register', {
method: 'POST',
body: { username: 'Gemini', password: '123' }
})
// 处理注册成功逻辑
}
- 在中间件(Middleware)或插件(Plugins)中
如果你需要在路由跳转时验证权限,或者在应用启动时初始化一些数据,由于这些地方不在 Vue 组件的setup生命周期内,useFetch无法工作,必须使用$fetch。
// middleware/auth.js
export default defineNuxtRouteMiddleware(async (to, from) => {
const user = await $fetch('/api/check-auth')
if (!user.isLoggedIn) {
return navigateTo('/login')
}
})
- Server API 内部的相互调用
在 Nuxt 的 server/api 目录下编写后端接口时,如果你想在一个接口里调用另一个接口,你也应该使用 $fetch(Nuxt 会智能地进行内部调用,而不产生实际的 HTTP 开销)。
文件上传和下载
$fetch是基于ofetch构建的,它非常智能。处理文件上传时,它会自动识别FormData并设置正确的Content-Type;处理下载时,则需要结合二进制流处理。
文件上传
<script setup lang="ts">
const uploadAvatar = async (event: Event) => {
const el = event.target as HTMLInputElement
const file = el.files?.[0]
if (!file) return
// 1. 构建 FormData
const formData = new FormData()
formData.append('file', file)
formData.append('type', 'avatar') // 还可以附带其他字段
try {
// 2. 使用 $fetch 发送
// 注意:不要手动设置 Content-Type,浏览器会自动处理并加上 boundary
const res = await $fetch('/api/upload', {
method: 'POST',
body: formData,
headers: { Authorization: `Bearer ${token}` }, // 如果你的上传接口需要鉴权,别忘了加 header
// 如果需要监听进度(仅限客户端)
onResponse({ response }) {
console.log('上传成功')
}
})
alert('上传成功!')
} catch (err) {
console.error('上传失败', err)
}
}
</script>
<template>
<input type="file" @change="uploadAvatar" />
</template>
文件下载
如果后端接口直接吐出文件二进制流,你需要将响应类型设为blob。
const downloadInvoice = async (id: string) => {
try {
// 1. 请求时指定 responseType 为 'blob'
const response = await $fetch<Blob>(`/api/download/${id}`, {
responseType: 'blob'
})
// 2. 创建一个临时的 URL 对象
const url = window.URL.createObjectURL(response)
const link = document.createElement('a')
link.href = url
// 3. 设置文件名
link.setAttribute('download', `invoice-${id}.pdf`)
document.body.appendChild(link)
// 4. 模拟点击并移除
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url) // 释放内存
} catch (err) {
alert('下载失败')
}
}
获取原始响应
如果你需要获取原始响应的数据
比如判断的http状态码,http响应头,有2种方法
- onResponse属性
- $fe
<script setup lang="ts">
const {data} = await useFetch("/api/user", {
onResponse: (res) => {
const response = res.response
console.log(response.status, response._data, response.headers)
}
})
function getResponse(){
$fetch("/api/user", {
onResponse: (res) => {
const response = res.response
console.log(response.status, response._data, response.headers)
}
})
}
async function getResponse1(){
const response = await $fetch.raw("/api/user")
console.log(response.status, response._data, response.headers)
}
</script>
<template>
<div>原始响应
<button @click="getResponse">获取响应</button>
<button @click="getResponse1">获取响应1</button>
</div>
</template>
<style scoped>
</style>