Nuxt.js 与 Vue SSR

What — 什么是 Nuxt.js

Nuxt.js 是基于 Vue 的全栈框架,提供 SSR(服务端渲染)、SSG(静态生成)、混合渲染等能力,并内置路由、数据获取、SEO 优化等。它是 Vue 生态中 Next.js 的对应物。

渲染模式

模式说明适用场景
SSR服务端实时渲染 HTML动态内容、SEO 敏感页面
SSG构建时生成静态 HTML博客、文档、营销页
SWR静态 + 客户端增量更新内容型网站
CSR纯客户端渲染后台管理、无需 SEO
Hybrid每个路由独立选择渲染模式大型项目不同页面不同策略

与 Next.js 对比

维度Nuxt.jsNext.js
基础框架VueReact
路由基于文件自动生成基于文件自动生成
数据获取useFetch / useAsyncDatafetch in Server Components
布局layouts/ 目录嵌套布局
状态管理useState 内置需第三方库
SSR 框架h3 / NitroNode.js
部署30+ 平台自动适配Vercel 优先

Why — 为什么选择 Nuxt.js

1. 零配置的 Vue SSR

手动配置 Vue SSR 需要处理服务端入口、客户端入口、HTML 模板、路由同步、状态脱水/注水等问题。Nuxt.js 一条命令搞定。

2. 文件即路由

pages/
├── index.vue           → /
├── about.vue           → /about
├── users/
│   ├── index.vue       → /users
│   └── [id].vue        → /users/:id
└── blog/
    └── [...slug].vue   → /blog/*

3. 全栈能力

Nuxt 的 Server 目录可以直接写 API 路由,前后端一体。

4. 混合渲染

每个页面可以独立设置渲染模式:首页 SSR、博客 SSG、后台 CSR。

优缺点

  • ✅ 优点:零配置 SSR、文件路由、全栈能力、Vue 生态
  • ❌ 缺点:框架约定重(灵活性低)、调试 SSR 问题较难、社区比 Next.js 小

How — 怎么用

1. 创建项目

npx nuxi@latest init my-app
cd my-app
npm run dev

2. 页面与路由

<!-- pages/index.vue -->
<template>
  <div>
    <h1>Home Page</h1>
    <NuxtLink to="/users">Users</NuxtLink>
  </div>
</template>
<!-- pages/users/[id].vue -->
<template>
  <div>
    <h1>User {{ id }}</h1>
    <p>{{ user?.name }}</p>
    <NuxtLink to="/users">Back</NuxtLink>
  </div>
</template>

<script setup>
const route = useRoute()
const id = route.params.id

const { data: user } = await useFetch(`/api/users/${id}`)
</script>

3. 数据获取

<!-- useFetch:SSR + CSR 统一的数据获取 -->
<template>
  <div v-if="pending">Loading...</div>
  <div v-else>
    <ul>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script setup>
const { data: users, pending, error, refresh } = await useFetch('/api/users', {
  // 请求选项
  method: 'GET',
  headers: { Authorization: `Bearer ${token}` },
  query: { page: 1 },
  // 缓存与去重
  dedupe: 'defer',   // 相同请求只发一次
  // 响应式
  watch: [page],      // page 变化自动重新请求
})
</script>
// useAsyncData:更灵活的异步数据
const { data, pending } = await useAsyncData('key', () => {
  return $fetch('/api/data', { params: { id: 1 } })
})

// useLazyFetch / useLazyAsyncData:不阻塞导航
const { data, pending } = useLazyFetch('/api/users')
// 页面立即显示,数据到达后更新

4. Server API

// server/api/users.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const page = Number(query.page) || 1
  const limit = 20

  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
  })

  return { users, page }
})
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  const user = await db.user.findUnique({ where: { id: Number(id) } })

  if (!user) {
    throw createError({ statusCode: 404, message: 'User not found' })
  }

  return user
})
// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const token = getHeader(event, 'authorization')

  if (!token) {
    throw createError({ statusCode: 401, message: 'Unauthorized' })
  }
})

5. 布局系统

<!-- layouts/default.vue -->
<template>
  <div class="app">
    <header>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/users">Users</NuxtLink>
      </nav>
    </header>
    <main>
      <slot />
    </main>
    <footer>Footer</footer>
  </div>
</template>
<!-- layouts/admin.vue -->
<template>
  <div class="admin-layout">
    <AdminSidebar />
    <main><slot /></main>
  </div>
</template>
<!-- pages/admin/dashboard.vue -->
<template>
  <div>Dashboard</div>
</template>

<script setup>
definePageMeta({ layout: 'admin' })
</script>

6. 混合渲染 — 路由规则

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 首页:SSR + 1 小时缓存
    '/': { prerender: true, swr: 3600 },
    // 博客:构建时静态生成
    '/blog/**': { prerender: true },
    // API:CORS + 缓存
    '/api/**': { cors: true, headers: { 'cache-control': 's-maxage=60' } },
    // 后台:纯客户端渲染
    '/admin/**': { ssr: false },
    // 旧页面重定向
    '/old-page': { redirect: '/new-page' },
  },
})

7. 状态管理

// composables/useCart.ts
export const useCart = () => useState<CartItem[]>('cart', () => [])

// 自动导入,无需手动 import
<!-- 任何组件中直接使用 -->
<script setup>
const cart = useCart()

function addItem(item: CartItem) {
  cart.value.push(item)
}

function removeItem(id: number) {
  cart.value = cart.value.filter(i => i.id !== id)
}
</script>

8. SEO

<script setup>
useHead({
  title: 'My App',
  meta: [
    { name: 'description', content: 'My awesome app' },
    { property: 'og:title', content: 'My App' },
    { property: 'og:description', content: 'My awesome app' },
  ],
})

// 动态 SEO
const { data: article } = await useFetch('/api/article/1')

useHead({
  title: article.value?.title,
  meta: [
    { name: 'description', content: article.value?.summary },
    { property: 'og:image', content: article.value?.cover },
  ],
})
</script>

9. 中间件

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const token = useCookie('token')

  if (!token.value && to.path.startsWith('/admin')) {
    return navigateTo('/login')
  }
})
<!-- 页面中使用 -->
<script setup>
definePageMeta({
  middleware: 'auth',
})
</script>

常见问题与踩坑

问题原因解决方案
Hydration 不匹配服务端和客户端渲染结果不一致避免 Date.now()Math.random() 在模板中直接使用
环境变量不生效客户端无法访问服务端变量使用 runtimeConfig.public
第三方库报错库依赖 window / document<ClientOnly> 包裹或动态导入
SSR 性能差每次请求都完整渲染启用 SWR 缓存 / 静态预渲染
API 跨域服务端请求无跨域问题,客户端有Server API 做代理

最佳实践

  1. 路由规则先行:项目初期定义好每个路由的渲染模式。
  2. Server API 代理:第三方 API 通过 Server API 代理,避免跨域和 Key 泄露。
  3. <ClientOnly> :纯客户端组件用 <ClientOnly> 包裹。
  4. useHead 做 SEO:每个页面设置 title 和 meta。
  5. useState 共享状态:跨组件状态用 useState,SSR 自动脱水/注水。

面试题

1. Nuxt.js 的 SSR 渲染流程是什么?

:(1) 用户请求 URL;(2) Nuxt 服务端匹配路由,执行页面组件的 <script setup>;(3) useFetch / useAsyncData 在服务端执行数据获取;(4) Vue 将组件渲染为 HTML 字符串;(5) 将 HTML + 脱水后的状态(payload)注入 HTML 模板;(6) 返回完整 HTML 给浏览器;(7) 浏览器渲染 HTML(用户可见内容);(8) 加载 JS,Vue 进行 Hydration——将静态 HTML 附加事件监听和响应式;(9) Hydration 完成后,页面变为可交互。关键:useFetch 只执行一次(服务端),客户端直接使用脱水的数据,不重复请求。


2. useFetch 和 $fetch 有什么区别?

useFetch 是 Nuxt 的组合函数,自动处理 SSR 数据获取——服务端获取数据、脱水到 HTML payload、客户端 Hydration 时复用数据不重复请求。它返回响应式引用(datapendingerror)。$fetch 是底层的 fetch 封装(基于 ofetch),每次调用都发起 HTTP 请求,不处理 SSR 脱水/注水,不返回响应式引用。规则:页面数据获取用 useFetch,Server API 内部调用其他 API、事件处理中的请求用 $fetch


3. Nuxt.js 的混合渲染是什么?如何配置?

:混合渲染允许每个路由使用不同的渲染策略,通过 routeRules 配置。例如:首页用 SSG(prerender: true),博客用 SWR(swr: 3600),后台用 CSR(ssr: false),API 路由设置缓存头。这解决了”一刀切”的问题——SEO 敏感页面需要 SSR,后台管理不需要 SSR 却要承担渲染开销。Nuxt 在构建时根据 routeRules 生成对应的 HTML(prerender),运行时根据路由规则决定是返回预渲染 HTML、实时渲染还是 CSR。


4. Nuxt.js 的 Server API 和传统 Express 有什么区别?

:Nuxt Server API 基于 h3(轻量 HTTP 框架),与传统 Express 的区别:(1) 自动路由——server/api/users.ts 自动映射为 /api/users,无需手动 app.get();(2) 类型安全——defineEventHandler 的参数和返回值有 TypeScript 类型;(3) 自动导入——getQuerygetRouterParam 等工具函数自动导入;(4) 部署适配——h3 的 event 对象是跨运行时的,同一份代码可部署到 Node.js、Cloudflare Workers、Vercel Edge 等。Express 只能在 Node.js 运行。


5. Nuxt.js 的 Hydration 不匹配问题怎么排查和解决?

:Hydration 不匹配是指服务端渲染的 HTML 与客户端 Hydration 时重新渲染的 DOM 不一致。原因:(1) 使用了 Date.now()Math.random() 等服务端/客户端结果不同的值;(2) 使用了 window.innerWidth 等浏览器 API;(3) 第三方库在服务端和客户端渲染结果不同。排查:(1) 浏览器控制台的 hydration mismatch 警告;(2) 查看页面源代码(SSR HTML)与 DevTools Elements 面板(Hydration 后 DOM)的差异。解决:(1) 用 <ClientOnly> 包裹客户端专有内容;(2) 在 onMounted 中设置依赖浏览器 API 的值;(3) 用 useState 确保服务端和客户端共享初始值。


6. Nuxt.js 和 Next.js 的核心差异是什么?

:核心差异是底层框架和哲学:(1) 框架——Nuxt 基于 Vue,Next 基于 React;(2) 数据获取——Nuxt 的 useFetch 在 SSR 和 CSR 中行为一致,Next 13+ 的 Server Components 在服务端获取数据后序列化到客户端,客户端组件用 use hook;(3) 全栈——Nuxt 的 Server API 内置(h3),Next 的 API Routes 基于 Node.js;(4) 灵活性——Nuxt 约定更重(目录结构、自动导入),Next 更灵活但配置更多;(5) 部署——Nuxt 通过 Nitro 适配 30+ 平台,Next 深度绑定 Vercel。选型取决于团队技术栈——Vue 团队选 Nuxt,React 团队选 Next。


7. Nuxt.js 的 useState 和 Vue 的 ref 有什么区别?

useState 是 Nuxt 提供的跨 SSR 共享状态,ref 是 Vue 原生的响应式引用。区别:(1) SSR 兼容——useState 的值在服务端渲染后自动脱水到 HTML payload,客户端 Hydration 时自动恢复,确保服务端和客户端初始值一致;ref 在服务端和客户端各创建独立实例,可能导致不匹配;(2) 跨组件共享——useState('key', () => initialValue) 通过 key 标识,所有使用相同 key 的组件共享同一个状态实例;ref 每次调用创建新实例;(3) 适用场景——需要 SSR 共享的全局状态用 useState,组件内部状态用 ref


8. Nuxt.js 项目如何优化首屏加载性能?

:七个优化方向:(1) 预渲染——静态页面用 prerender: true,动态页面用 swr 缓存,避免每次请求都 SSR;(2) 路由代码拆分——Nuxt 自动按页面拆分,确保没有全量 JS;(3) 图片优化——使用 @nuxt/image 自动压缩、懒加载、WebP 格式;(4) 字体优化——@nuxt/fonts 自动下载和 preload;(5) 减少 Hydration 开销——纯展示页面用 <ClientOnly>ssr: false 减少 JS;(6) Payload 压缩——Nuxt 3 自动压缩 SSR payload,确保生产模式启用;(7) CDN 缓存——配合 routeRulesswrheaders 配置,让 CDN 缓存 SSR 输出。


相关链接