Nuxt: 数据获取 useFetch

最后更新:2026-08-26

Alice 打开 MegaShop 首页,看到的是一片空白——商品数据在客户端才请求,等了 3 秒才渲染出来。Charlie 知道,Nuxt 3 的 useFetch 能在服务端预取数据,HTML 直接包含商品信息,首屏立即可见。

1. 你将学到


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 数据预取到客户端水合时序

100%
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 复用前一个请求的结果,不发起新请求。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 useFetch 获取一个公开 API(如 jsonplaceholder),展示数据列表,验证 SSR 查看源代码中是否包含数据
  2. 进阶题(难度⭐⭐):实现一个带分页的商品列表,点击下一页时用 watch 自动重新获取数据
  3. 挑战题(难度⭐⭐⭐):同时实现 SSR 预取核心商品 + lazy 加载推荐商品,对比两种模式下的用户体验差异

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏