Nuxt: 数据获取 useFetch
最后更新:2026-08-26
Alice 打开 MegaShop 首页,看到的是一片空白——商品数据在客户端才请求,等了 3 秒才渲染出来。Charlie 知道,Nuxt 3 的 useFetch 能在服务端预取数据,HTML 直接包含商品信息,首屏立即可见。
1. 你将学到
- useFetch / useAsyncData:SSR 预取 + 客户端水合完整流程
- $fetch 原始请求 vs useFetch 封装差异
- 请求选项:server/lazy/immediate/dedupe/transform/pick
- 刷新与轮询:refresh() / watch 模式
- MegaShop 商品列表 SSR 预取实战
2. 一个消费者的真实故事
(1) 痛点:白屏等待 3 秒
Alice 用手机打开 MegaShop,看到空白页面转圈 3 秒后商品才出现。在地铁弱网环境下更糟——5 秒白屏。Charlie 分析发现:商品数据在客户端 JS 加载后才 fetch,整个渲染链路是 JS 下载 → 执行 → fetch → 渲染,太慢了。
(2) useFetch SSR 预取的解法
useFetch 在 SSR 阶段就获取数据,HTML 直接包含商品内容:
VUE
<script setup lang="ts">
// Server: fetch data during SSR
// Client: use pre-fetched data, no duplicate request
const { data: products } = await useFetch('/api/products')
</script>
(3) 收益:首屏从 3s 降到 0.8s
SSR 预取后,Alice 打开页面立即看到商品,LCP 从 3s 降到 0.8s,用户体验评分从 52 提升到 94。
3. useFetch 与 useAsyncData
(1) SSR 数据预取到客户端水合时序
sequenceDiagram
participant B as Browser
participant S as Nuxt Server
participant A as API Server
B->>S: GET /products
S->>A: fetch /api/products
A-->>S: JSON data
S->>S: Render HTML with data
S-->>B: HTML + payload (data embedded)
B->>B: Display HTML (instant first paint)
B->>B: Hydrate with payload (no re-fetch)
(2) useFetch vs useAsyncData 对比
| 维度 | useFetch | useAsyncData |
|---|---|---|
| 定位 | 封装了 $fetch 的快捷方法 | 通用数据获取 |
| 请求方式 | 内置 $fetch | 自定义 handler |
| URL 响应式 | ✅ URL 变化自动重新请求 | ❌ 需手动 watch |
| 类型推导 | ✅ 自动推导响应类型 | ⚠️ 需手动指定泛型 |
| 使用场景 | 大多数 API 请求 | 非标准请求/复杂逻辑 |
▶ 示例:useFetch 基础用法
VUE
<!-- pages/products/index.vue -->
<template>
<div>
<h1>Products</h1>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Failed to load products</div>
<div v-else>
<ProductCard v-for="p in products" :key="p.id" :product="p" />
</div>
</div>
</template>
<script setup lang="ts">
const { data: products, pending, error } = await useFetch('/api/products')
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:useAsyncData 自定义 handler
VUE
<script setup lang="ts">
const { data: stats } = await useAsyncData('product-stats', async () => {
// Custom data fetching logic
const [total, featured, onSale] = await Promise.all([
$fetch('/api/products/count'),
$fetch('/api/products/featured'),
$fetch('/api/products/on-sale')
])
return { total, featured, onSale }
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
4. $fetch vs useFetch
(1) 核心差异对比
| 维度 | $fetch | useFetch |
|---|---|---|
| SSR 数据传递 | ❌ 不传递 | ✅ 自动 payload 传递 |
| 响应式 | ❌ 原始数据 | ✅ ref 响应式 |
| 状态管理 | ❌ 无 | ✅ pending/error/refresh |
| 重复请求 | ❌ 可能重复 | ✅ dedupe 去重 |
| 适用场景 | 事件处理/API 路由 | 页面数据获取 |
▶ 示例:$fetch 用于事件处理
VUE
<script setup lang="ts">
// $fetch is for one-off requests (no SSR payload needed)
async function submitOrder() {
const order = await $fetch('/api/orders', {
method: 'POST',
body: { items: cart.value, total: totalPrice.value }
})
navigateTo(`/orders/${order.id}`)
}
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:$fetch 在 server API 中使用
TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
// $fetch is the right choice inside server handlers
const query = getQuery(event)
const products = await $fetch('https://api.supplier.com/products', {
params: { category: query.category }
})
return products
})
输出:
TEXT
📖 仅展示
// 执行成功
5. 请求选项详解
(1) 核心选项速查
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| server | boolean | true | 是否在服务端执行 |
| lazy | boolean | false | 是否延迟加载(不阻塞导航) |
| immediate | boolean | true | 是否立即执行 |
| dedupe | string | 'cancel' | 重复请求策略 |
| transform | function | - | 转换响应数据 |
| pick | array | - | 只提取指定字段 |
| default | function | - | 数据加载前的默认值 |
▶ 示例:lazy 模式(不阻塞导航)
VUE
<template>
<div>
<h1>Product Recommendations</h1>
<!-- lazy: data loads in background, page renders immediately -->
<div v-if="pending">Loading recommendations...</div>
<div v-else>
<ProductCard v-for="p in recommendations" :key="p.id" :product="p" />
</div>
</div>
</template>
<script setup lang="ts">
const { data: recommendations, pending } = await useFetch('/api/recommendations', {
lazy: true
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:transform 与 pick
VUE
<script setup lang="ts">
// Transform: process data before storing
const { data: products } = await useFetch('/api/products', {
transform: (data: any[]) => {
return data.map(p => ({
...p,
formattedPrice: new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD'
}).format(p.price)
}))
}
})
// Pick: only extract specific fields (reduce payload size)
const { data: productNames } = await useFetch('/api/products', {
pick: ['id', 'name', 'price']
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:server: false(纯客户端获取)
VUE
<script setup lang="ts">
// Skip SSR fetching - only fetch on client side
const { data: userWishlist } = await useFetch('/api/wishlist', {
server: false,
default: () => [] // Default value before data loads
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
6. 刷新与轮询
(1) 刷新策略对比
| 策略 | 方式 | 场景 | 频率 |
|---|---|---|---|
| 手动刷新 | refresh() | 用户操作后 | 按需 |
| Watch 自动刷新 | watch 选项 | 参数变化时 | 参数变化时 |
| 轮询 | setInterval + refresh | 实时数据 | 定时 |
| 实时 | WebSocket | 极端实时 | 推送 |
▶ 示例:手动刷新
VUE
<template>
<div>
<h1>Products</h1>
<button @click="refresh()" :disabled="pending">
{{ pending ? 'Refreshing...' : 'Refresh' }}
</button>
<ProductCard v-for="p in products" :key="p.id" :product="p" />
</div>
</template>
<script setup lang="ts">
const { data: products, pending, refresh } = await useFetch('/api/products')
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Watch 参数变化自动刷新
VUE
<template>
<div>
<select v-model="selectedCategory">
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<ProductCard v-for="p in products" :key="p.id" :product="p" />
</div>
</template>
<script setup lang="ts">
const selectedCategory = ref('all')
// Auto-refetch when selectedCategory changes
const { data: products } = await useFetch('/api/products', {
query: { category: selectedCategory },
watch: [selectedCategory]
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:轮询实时库存
VUE
<script setup lang="ts">
const { data: stock, refresh } = await useFetch('/api/stock/live', {
server: false // Client-only polling
})
// Poll every 30 seconds
const pollInterval = setInterval(() => {
refresh()
}, 30000)
onUnmounted(() => clearInterval(pollInterval))
</script>
输出:
TEXT
📖 仅展示
// 执行成功
7. 综合示例:MegaShop 商品列表 SSR 预取
VUE
<!-- pages/products/index.vue -->
<template>
<div class="product-list-page">
<h1>MegaShop Products</h1>
<!-- Filters -->
<div class="filters">
<select v-model="filters.category" @change="applyFilters">
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="home">Home & Garden</option>
</select>
<select v-model="filters.sort">
<option value="popular">Most Popular</option>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
</select>
</div>
<!-- Loading state -->
<div v-if="pending" class="loading">Loading products...</div>
<!-- Error state -->
<div v-else-if="error" class="error">
Failed to load products. <button @click="refresh()">Retry</button>
</div>
<!-- Product grid -->
<div v-else class="product-grid">
<ProductCard
v-for="p in products"
:key="p.id"
:product="p"
@add-to-cart="addToCart"
/>
</div>
<!-- Pagination -->
<div class="pagination">
<button :disabled="page <= 1" @click="page--">Previous</button>
<span>Page {{ page }}</span>
<button @click="page++">Next</button>
</div>
</div>
</template>
<script setup lang="ts">
interface Product {
id: number; name: string; price: number; image: string
}
const page = ref(1)
const filters = reactive({
category: '',
sort: 'popular'
})
// SSR pre-fetch with reactive query
const { data: products, pending, error, refresh } = await useFetch<Product[]>('/api/products', {
query: computed(() => ({
page: page.value,
category: filters.category || undefined,
sort: filters.sort
})),
default: () => [],
transform: (data: Product[]) => data.map(p => ({
...p,
formattedPrice: `$${p.price.toLocaleString()} USD`
}))
})
const cart = useState<Product[]>('cart', () => [])
function addToCart(product: Product) {
cart.value.push(product)
}
function applyFilters() {
page.value = 1
refresh()
}
// Watch page change for auto-refresh
watch(page, () => refresh())
</script>
❓ 常见问题
Q useFetch 会导致客户端重复请求吗?
A 不会。Nuxt 3 的 payload 机制会将 SSR 获取的数据序列化到 HTML 中,客户端水合时直接使用,不会重复请求。
Q 什么时候用 $fetch 而不是 useFetch?
A 在事件处理函数中(如按钮点击提交表单)、server API 内部调用时用 $fetch。页面初始数据获取用 useFetch。
Q useFetch 的 key 是怎么生成的?
A 默认用 URL + 请求选项自动生成唯一 key。如果需要手动控制(如多个请求共享缓存),可以传 key 参数。
Q lazy 和 server: false 有什么区别?
A lazy 仍然在 SSR 执行请求,只是不阻塞导航(页面先渲染再等数据)。server: false 完全跳过 SSR 请求,只在客户端获取。
Q 为什么 useFetch 的响应数据是 ref 而不是原始值?
A 因为数据可能变化(refresh/watch),ref 保证响应式。解构时用
toRefs 或直接用 .value 访问。Q dedupe 选项的 cancel 和 defer 有什么区别?
A cancel(默认)取消前一个未完成的请求发起新的。defer 复用前一个请求的结果,不发起新请求。
📖 小节
- useFetch = SSR 预取 + 客户端水合 + 响应式状态,是页面数据获取首选
- $fetch 是原始请求工具,适合事件处理和 server API 内部调用
- lazy 不阻塞导航,server: false 跳过 SSR,transform/pick 优化数据
- refresh() 手动刷新,watch 选项自动跟随参数变化
- MegaShop 用 useFetch + watch 实现商品列表 SSR 预取 + 筛选联动
📝 作业
- 基础题(难度⭐):用 useFetch 获取一个公开 API(如 jsonplaceholder),展示数据列表,验证 SSR 查看源代码中是否包含数据
- 进阶题(难度⭐⭐):实现一个带分页的商品列表,点击下一页时用 watch 自动重新获取数据
- 挑战题(难度⭐⭐⭐):同时实现 SSR 预取核心商品 + lazy 加载推荐商品,对比两种模式下的用户体验差异
---|