nuxt / 前端 · 2025-12-30 0

Nuxt生命周期(从useAsyncData 的案例解析)

useAsyncData 在生命周期中的位置,以及从服务器端到客户端的完整流程

  1. 解释 useAsyncData 的作用
  2. 结合 Nuxt 4 的生命周期详细说明执行流程
  3. 结合 Vue 的生命周期
  4. 展示从 SSR 到水合的完整过程

代码流程我们可以看到:

  1. 在 index.vue 中使用了 useAsyncData 来获取首页数据
  2. 在 products/[id].vue 中也使用了 useAsyncData 来获取商品详情
  3. useAppSSR.ts 中封装了 useSSRData,内部使用了 useAsyncData

现在详细解释:

  1. useAsyncData 在哪个生命周期阶段执行
  2. 从服务器端到客户端的完整流程
  3. 与 Vue 生命周期的对应关系

整理后一个完整的生命周期图解如下。

useAsyncData 完整生命周期解析

一、useAsyncData 的核心作用

// useAsyncData - Nuxt 4 的核心数据获取 API
const { data, pending, error, refresh, clear } = await useAsyncData(
  'unique-key',           // 缓存键,用于数据去重
  async () => {            // 数据获取函数
    const response = await $fetch('/api/data')
    return response
  },
  {
    server: true,          // 是否在服务端执行(默认 true)
    client: true,          // 是否在客户端执行(默认 true)
    transform: (data) => data, // 数据转换
    default: () => null    // 默认值
  }
)

二、完整生命周期流程图

┌─────────────────────────────────────────────────────────────────┐
│                    Nuxt 4 + Vue 生命周期完整流程                │
└─────────────────────────────────────────────────────────────────┘

【阶段 1:服务端渲染】
│
├─ 1. 服务器启动
│   └─ Nuxt 应用初始化
│
├─ 2. 收到 HTTP 请求 (request)
│   └─ 解析路由
│
├─ 3. Nuxt 钩子
│   ├─ app:beforeMount (Nuxt 挂载前)
│   ├─ page:start (页面开始)
│   └─ ━━▶ useAsyncData 在此执行 ━━━
│           │
│           ├─ 检查缓存 (基于 key)
│           ├─ 执行数据获取函数
│           ├─ 调用 transform 转换数据
│           ├─ 存储到 payload
│           └─ 返回 { data, pending: false }
│
├─ 4. Vue 组件实例化
│   └─ beforeCreate (Vue)
│
├─ 5. setup() 执行
│   └─ useAsyncData 数据已经可用
│
├─ 6. Nuxt 钩子
│   ├─ app:mounted (Nuxt 挂载)
│   └─ page:finish (页面完成)
│
├─ 7. Vue 组件渲染
│   ├─ beforeMount
│   ├─ mounted
│   └─ 生成 HTML
│
└─ 8. 返回 HTML + Payload
    │
    └─ Payload 包含:
        └─ __NUXT__ = { data: { 'unique-key': [...] } }

【阶段 2:数据传输】
│
└─ HTML + Payload 通过网络发送到客户端
    (Payload 通常在 HTML 中嵌入或单独传输)

【阶段 3:客户端水合】
│
├─ 9. 浏览器接收 HTML + Payload
│   └─ 解析 HTML,页面快速显示
│
├─ 10. 加载 JavaScript
│   └─ 执行客户端代码
│
├─ 11. Vue 组件实例化 (客户端)
│   └─ beforeCreate
│
├─ 12. setup() 执行 (客户端)
│   └─ ━━▶ useAsyncData 在此执行 ━━━
│           │
│           ├─ 检查 Payload 中是否有数据
│           ├─ 有:直接使用,pending = false
│           ├─ 无:如果 server=false,则重新请求
│           └─ 返回响应式数据
│
├─ 13. Nuxt 客户端钩子
│   ├─ app:mounted (客户端挂载)
│   └─ vue:app:mounted
│
├─ 14. Vue 生命周期
│   ├─ beforeMount
│   ├─ mounted ━━━ 水合开始 ━━━
│   └─ DOM 事件绑定
│
└─ 15. 水合完成 (Hydration Done)
    │
    └─ 客户端接管,页面可交互

【阶段 4:客户端交互】
│
├─ 16. 用户交互
│   └─ 点击按钮、输入等
│
├─ 17. 数据更新
│   ├─ refresh() 手动刷新
│   └─ watch 监听参数变化自动刷新
│
├─ 18. useAsyncData 重新执行
│   └─ 仅在客户端执行
│
└─ 19. 更新响应式数据
    └─ 触发 Vue 重新渲染

三、useAsyncData 在各阶段的详细执行

阶段 1:服务端渲染 (SSR)

// pages/index.vue (服务端执行流程)

// 1️⃣ Nuxt 接收到请求
// 2️⃣ 执行 useAsyncData
const { data } = await useAsyncData('home-data', async () => {
  // 3️⃣ 检查缓存(基于 key)
  // 4️⃣ 执行数据获取
  const productStore = useProductStore()
  const [featuredProducts, categories] = await Promise.all([
    productStore.fetchFeaturedProducts(),  // ← 这里发 HTTP 请求
    productStore.fetchCategories()
  ])

  // 5️⃣ 返回数据
  return { featuredProducts, categories }
}, {
  server: true  // ← 服务端执行(默认)
})

// 6️⃣ 数据存储到 Payload
__NUXT__ = {
  data: {
    'home-data': {
      featuredProducts: [...],
      categories: [...],
      _v: null,
      _error: null,
      _raw: true
    }
  }
}

// 7️⃣ 组件渲染,生成 HTML
// 8️⃣ 返回 HTML + Payload 给客户端

服务端 Vue 生命周期:

beforeCreate → created → setup() → beforeMount → mounted
               ↑
          useAsyncData 在这里执行
          此时 DOM 还没创建,数据已准备好

阶段 3:客户端水合

// pages/index.vue (客户端执行流程)

// 1️⃣ 浏览器接收 HTML,立即显示内容(无数据获取延迟)
// 2️⃣ 加载并执行 JavaScript
// 3️⃣ 再次执行 useAsyncData
const { data, pending } = await useAsyncData('home-data', async () => {
  // 4️⃣ 检查 Payload
  // 5️⃣ 找到数据!直接返回,不重新请求
  // pending = false

  // 如果 Payload 中没有数据(server: false),才会执行此函数
  return { ... }
}, {
  server: false  // 如果设置为 false,客户端会重新请求
})

// 6️⃣ setup() 继续执行
// 7️⃣ Vue 组件挂载
// 8️⃣ 水合开始
onMounted(() => {
  // 9️⃣ 水合完成
  console.log('水合完成,页面可交互')
})

客户端 Vue 生命周期:

beforeCreate → created → setup() → beforeMount → mounted
               ↑                    ↑
          useAsyncData          水合开始
          从 Payload 恢复数据

四、结合项目代码的实际流程

案例 1:首页数据加载

// apps/nuxt-mall/app/pages/index.vue

// 【服务端执行】
// 1. 服务器接收到 / 请求
const { data } = await useAsyncData('home-data', async () => {
  const productStore = useProductStore()

  // 2. 并行请求多个接口
  const [featuredProducts, categories] = await Promise.all([
    productStore.fetchFeaturedProducts(),  // HTTP 请求 1
    productStore.fetchCategories()         // HTTP 请求 2
  ])

  // 3. 返回数据
  return { featuredProducts, categories }
})

// 4. 数据序列化到 Payload
// 5. 生成 HTML
// 6. 发送到客户端

// 【客户端执行】
// 7. 浏览器接收 HTML,立即显示(数据已在 HTML 中)
// 8. 加载 JS,执行 setup()
// 9. useAsyncData 从 Payload 恢复数据
// 10. 开始水合

// 11. onMounted - 水合完成
onMounted(() => {
  console.log('首页加载完成,可交互')
})

案例 2:动态路由页面

// apps/nuxt-mall/app/pages/products/[id].vue

// 【服务端执行】
const productId = route.params.id  // 从路由获取参数

const { data: productData, pending, error } = await useAsyncData(
  `product-${productId}`,  // ← 唯一键,包含 ID
  async () => {
    const productStore = useProductStore()
    const response = await productStore.fetchProductDetail(Number(productId))
    return { product: response }
  }
)

// 【客户端执行】
// 1. 从 Payload 恢复数据
// 2. 水合组件
// 3. 页面可交互

// 【用户交互】
const addToCart = async () => {
  // 点击按钮,客户端发起请求
  await cartStore.addItem({ ... })
  ElMessage.success('已加入购物车')
}

五、useAsyncData 配置选项详解

await useAsyncData(key, handler, options)

options: {
  // === 执行控制 ===
  server: true,       // 是否在服务端执行
  client: true,       // 是否在客户端执行
  lazy: false,        // 是否延迟加载(不阻塞路由导航)

  // === 缓存控制 ===
  key: 'unique-key',  // 缓存键(参数 1)
  getCachedData: (key) => nuxtApp.payload.data[key], // 自定义缓存获取

  // === 数据处理 ===
  transform: (data) => data,           // 数据转换函数
  pick: ['id', 'name'],                // 只保留指定字段
  default: () => ({ items: [] }),      // 默认值

  // === 响应式 ===
  watch: [page, search],               // 监听这些变量变化自动刷新
  deep: false,                         // 深度监听

  // === 错误处理 ===
  dedupe: 'cancel' | 'defer',          // 重复请求策略
}

六、useAsyncData vs useFetch 的区别

特性 useAsyncData useFetch
来源 底层 API 封装了 useAsyncData + $fetch
灵活性 高(自定义 fetcher) 低(仅 HTTP 请求)
典型用途 任意异步数据 API 请求
代码示例 useAsyncData('key', () => db.query()) useFetch('/api/data')
内部实现 直接执行传入的函数 调用 $fetch 并用 useAsyncData 包装
// useFetch 内部实现类似这样
function useFetch(url, options) {
  return useAsyncData(
    options.key || url,
    () => $fetch(url, options)
  )
}

七、最佳实践总结

✅ 正确使用场景

// 1. 页面初始数据(SSR 必需)
const { data } = await useAsyncData('products', fetchProducts)

// 2. SEO 关键数据
const { data } = await useAsyncData('seo-data', fetchSeoData)

// 3. 非请求类的异步操作
const { data } = await useAsyncData('computed', async () => {
  return heavyCalculation()
})

❌ 错误使用场景

// 1. 用户交互后的数据(应使用 $fetch)
// ❌ 错误
const { data } = await useAsyncData('submit', submitForm)
// ✅ 正确
const result = await $fetch('/api/submit', { method: 'POST', body: formData })

// 2. 不需要 SSR 的纯客户端数据
// ❌ 错误
const { data } = await useAsyncData('user-location', () => 
  new Promise(resolve => navigator.geolocation.getCurrentPosition(resolve))
, { server: false })  // 既然是 server: false,为何不用 $fetch 或直接 Promise?
// ✅ 正确
const location = await getUserLocation()

// 3. 频繁变化的数据(如实时股票)
// ❌ 错误
const { data } = await useAsyncData('stock-price', fetchStockPrice)
// ✅ 正确
const stockPrice = ref(null)
setInterval(async () => {
  stockPrice.value = await $fetch('/api/stock-price')
}, 1000)

八、生命周期时间线总结

时间轴
  ↓
服务端:
  ━━━ useAsyncData 执行 ━━━ 数据获取 ━━━ 存储到 Payload
                                              ↓
  HTML 生成 ━━━ 返回 ━━━ 网络 ━━━ 客户端接收
                                              ↓
客户端:
  HTML 显示(无数据获取延迟)
            ↓
  JS 加载 ━━━ 执行 setup()
            ↓
  ━━━ useAsyncData 从 Payload 恢复数据 ━━━
            ↓
  Vue mounted ━━━ 水合开始
            ↓
  水合完成 ━━━ 页面可交互
            ↓
  用户交互 ━━━ 可能触发 refresh() ━━━ useAsyncData 重新执行

九、实际项目中的使用示例

参考项目代码:

// apps/nuxt-mall/app/composables/useAppSSR.ts

export const useSSRData = <T>(key: string, fetcher: () => Promise<T>) => {
  return useAsyncData<T>(key, fetcher, {
    server: true,     // 服务端执行
    client: true,     // 客户端执行(用于水合)
    getCachedData: (key) => nuxtApp.payload.data[key],  // 从 Payload 恢复
    transform: (data: T) => data  // 数据转换
  })
}

// 页面中使用
const { data } = await useSSRData('home-data', async () => {
  // 这个函数会在:
  // 1. 服务端执行一次(生成 HTML)
  // 2. 客户端执行一次(从 Payload 恢复数据,不会重复请求)
  return { products, categories }
})

关键点:

  • useAsyncData 在服务端渲染阶段执行(app:beforeMountsetup()
  • 数据通过 Payload 传递到客户端
  • 客户端水合时,useAsyncData 直接从 Payload 恢复数据(缓存命中),不重新请求
  • 这样实现了首屏秒开,SEO 友好,无需等待客户端数据加载