🎯 Nuxt 3 神操作:让爬虫吃同步数据,用户享异步丝滑!

6 阅读3分钟

面试官:你们 Nuxt 项目 SEO 怎么做的?
我:服务端同步请求,保证爬虫能拿到完整 HTML。
面试官:那首屏加载卡顿怎么办?
我:🤯……

SSR 渲染的"鱼与熊掌"之困:

方案SEO用户体验
✅ 异步请求❌ 爬虫只看到 JS 代码✅ 页面秒开
✅ 同步请求✅ 完整 HTML 给爬虫❌ 用户卡到怀疑人生

有没有一种方式:搜索引擎来的时候装乖孩子(同步),用户来的时候当个灵活胖子(异步)?

当然有!而且 Nuxt 3 自带解决方案,一行逻辑就能搞定 👇


🔑 核心武器:import.meta.server + nuxtApp.isHydrating

要实现"看人下菜碟",关键在于判断当前代码在谁的地盘上执行

❌ 为什么不能用路由 from 判断?

很多同学第一反应:用 route.query.from 判断是不是从站内跳转来的。

天真了! 爬虫(Googlebot、百度蜘蛛)访问页面时,压根没有"上一页"的概念,它们就是直接访问。用 from 判断,爬虫会直接被归到"首屏用户"那类,结果你还是区分不了。

所以,我们要判断的不是"有没有上一页",而是 "代码此刻在服务端还是客户端"

✅ 正确姿势:一行代码定乾坤

const shouldWait = import.meta.server || nuxtApp.isHydrating;

这句代码就是整篇文章的灵魂,我们来拆解一下:

  • import.meta.server:Vite 注入的编译时标记。代码在 Node.js 服务端跑时为 true,在浏览器跑时为 false
  • nuxtApp.isHydrating:Nuxt 提供的运行时标记。客户端首屏注水(Hydration)阶段为 true,SPA 跳转后为 false

🚀 完整实现:智能同步/异步切换

<script setup>
const route = useRoute()
const nuxtApp = useNuxtApp()

// 🎯 核心判断逻辑
const shouldWait = import.meta.server || nuxtApp.isHydrating

const { data, pending } = await useAsyncData(
  `data-${route.path}`,
  () => $fetch('/api/your-endpoint'),
  {
    // 爬虫/首屏 → 阻塞等待(同步)
    // 客户端跳转 → 不阻塞(异步)
    lazy: !shouldWait,
    // 确保服务端一定会执行这段代码
    server: true,
  }
)
</script>

<template>
  <div>
    <div v-if="pending" class="skeleton">
      ⏳ 加载中...
    </div>
    <div v-else>
      <h1>{{ data.title }}</h1>
      <p>{{ data.content }}</p>
    </div>
  </div>
</template>

🔬 两种场景模拟

场景 A:搜索引擎爬虫来访 🕷️

爬虫请求 /article/123
       ↓
Nuxt 在 Node.js 环境运行
       ↓
import.meta.server === trueshouldWait === true  →  lazy === false
       ↓
⏸️ 阻塞等待 API 返回数据
       ↓
📦 渲染完整 HTML
       ↓
🚀 发送给爬虫(内含真实数据,SEO 满分!)

场景 B:用户点击链接跳转 🏃

用户点击 <NuxtLink to="/article/456">
       ↓
客户端路由切换,不经过服务端
       ↓
import.meta.server === false
nuxtApp.isHydrating === falseshouldWait === false  →  lazy === true
       ↓
⚡ 页面立即跳转,不等待 API
       ↓
🔄 后台异步请求数据,pending 状态下显示骨架屏
       ↓
📊 数据返回,更新视图(丝滑体验!)

⚠️ 避坑指南:小心 Payload 导致 Hydration 报错

这是最容易翻车的地方,90% 的 Nuxt SSR 报错都跟它有关

原理

Nuxt 在服务端获取数据后,会序列化到 HTML 中:

<script id="__NUXT_DATA__" type="application/json">
  { "data": { "title": "文章标题" } }
</script>

客户端注水时,Nuxt 会从 __NUXT_DATA__ 中读取数据,不会重新请求

什么时候会翻车?

// ❌ 错误写法
const { data } = await useAsyncData('key', () => $fetch('/api'), {
  server: false, // 服务端不跑 → HTML 里没有 payload
  lazy: true,    // 客户端异步请求
})

结果:客户端注水时,发现 datanull,但模板期望有数据 → Hydration Mismatch

正确姿势

// ✅ 正确写法
const { data } = await useAsyncData('key', () => $fetch('/api'), {
  server: true,  // 服务端执行,payload 写入 HTML
  lazy: !import.meta.server, // 仅服务端阻塞
})

🧩 封装成组合式函数(推荐)

每次写一堆判断太累了,直接封装成可复用的 useSmartFetch

// composables/useSmartFetch.ts
export function useSmartFetch<T>(
  key: string,
  url: string,
  options?: { transform?: (data: any) => T }
) {
  const nuxtApp = useNuxtApp()

  // 如果在 hydrating 且 payload 已有数据,无需再次阻塞
  const hasPayload = !!nuxtApp.payload.data[key]
  const shouldBlock = import.meta.server || (nuxtApp.isHydrating && !hasPayload)

  return useAsyncData<T>(
    key,
    () => $fetch(url),
    {
      lazy: !shouldBlock,
      server: true,
      ...options,
    }
  )
}

使用:

<script setup>
const { data, pending } = await useSmartFetch('articles', '/api/articles')
</script>

清爽!一行代码搞定智能同步/异步切换 🎉


📊 场景决策矩阵

场景推荐方案原因
🏠 首页/着陆页强制同步 lazy: falseSEO 是第一优先级
📄 内容详情页智能判断(本文方案)爬虫和用户都要照顾
🔍 搜索列表页智能判断平衡体验
👤 用户中心全异步 lazy: true不需要 SEO
🛠️ 后台管理全异步 lazy: true不需要 SEO

💎 总结

Nuxt 3 处理 SEO + 性能平衡的最佳实践就三句话:

  1. import.meta.server 判断服务端 → 爬虫来了乖乖同步
  2. nuxtApp.isHydrating 判断首屏 → 用户首次访问也不拉胯
  3. 处理好 Payload 防 Hydration 报错 → 服务端必须 server: true