Nuxt.js 深度解析

从架构到数据流 — 面试级完全指南

Based on Nuxt 3 · 19 Chapters · Interview Ready

本文基于 Nuxt 3(当前主流版本),兼顾 Nuxt 2 的核心差异点。目标是让你读完后,能回答面试中关于 Nuxt 的绝大多数问题。

第一章 Nuxt.js 的定位与核心概念

1.1 Nuxt 是什么

Nuxt.js 是一个基于 Vue.js 的元框架(Meta-Framework)。所谓"元框架",是指它在 Vue(一个 UI 框架)之上,提供了完整的应用工程方案:路由、服务端渲染、数据获取、构建优化、部署适配等开箱即用的能力。类比关系:Vue 之于 Nuxt,如同 React 之于 Next.js,Svelte 之于 SvelteKit。

Nuxt 解决的本质问题是:把一个前端框架(Vue)变成一个全栈应用框架。Vue 本身只关心组件渲染,而 Nuxt 在其上叠加了路由系统、服务端/客户端渲染协调、数据序列化与水合(hydration)、代码分割策略、模块生态等企业级需求。

1.2 Nuxt 3 与 Nuxt 2 的关键差异

维度Nuxt 2Nuxt 3
底层服务器@nuxt/server (Connect)Nitro (h3 + Rollup)
Vue 版本Vue 2Vue 3
构建工具WebpackVite(默认)/ Webpack
响应式Options APIComposition API
语言JavaScriptTypeScript(原生支持)
包管理nuxt 单体包模块化拆分为多个独立包
服务端引擎紧耦合的 Express/Connect与平台无关的 Nitro(可部署到 Cloudflare Workers、Deno Deploy 等)
数据获取asyncData()fetch() (Options API)useAsyncData()useFetch() (Composition API)
状态管理VuexPinia / useState
混合渲染不支持Route Rules(per-route SSR/SPA/ISR)

1.3 核心概念速览

约定优于配置(Convention over Configuration):Nuxt 的核心设计哲学。通过目录结构和文件命名约定来自动生成功能,减少手动配置。例如 pages/ 目录下的文件自动生成路由,server/api/ 下的文件自动注册为 API 端点。

全栈一体:同一个项目中既有前端 Vue 应用,也有后端 Node.js(或 Serverless)服务,共享类型定义和配置。

通用渲染(Universal Rendering):同一套 Vue 组件代码既能在服务端渲染为 HTML,也能在客户端接管交互,Nuxt 负责协调两者之间的状态同步。


第二章 整体架构全景

2.1 架构分层总览

Nuxt 3 的架构可以分为四个核心层:

text
┌─────────────────────────────────────────────────────────┐
│                    开发者代码层                           │
│   pages/  layouts/  components/  server/  plugins/      │
├─────────────────────────────────────────────────────────┤
│                    Nuxt 框架层                           │
│   Nuxt Core · Nuxt Kit · @nuxt/schema                   │
│   自动导入 · 模块系统 · 插件系统 · 中间件系统              │
├─────────────────────────────────────────────────────────┤
│                    引擎层                                │
│   Nitro (Server Engine)  ·  Vite (Build Engine)         │
│   h3 (HTTP Framework)   ·  vue-bundle-renderer         │
├─────────────────────────────────────────────────────────┤
│                    平台/运行时层                          │
│   Node.js · Cloudflare Workers · Deno · Vercel Edge     │
│   AWS Lambda · Netlify Functions · Static Hosting       │
└─────────────────────────────────────────────────────────┘

2.2 核心包关系图

Nuxt 3 本身是一个由多个 npm 包组成的 monorepo(仓库地址 nuxt/nuxt),核心包包括:

nuxt(主入口包)—— 这是用户安装的包,它实际上是一个编排器,内部依赖并协调以下子包:

@nuxt/kit —— 模块和插件开发的工具库。提供了 defineNuxtModuleuseNuxt()addPlugin()addComponent()addServerHandler() 等 API。所有 Nuxt 模块都通过 @nuxt/kit 与 Nuxt 核心交互。

@nuxt/schema —— 定义了 Nuxt 的完整配置 schema(NuxtConfig 类型就是从这里生成的)。它使用 untyped 库从 schema 自动推导 TypeScript 类型,所以用户在 nuxt.config.ts 中获得完整的类型提示。

@nuxt/vite-builder —— 基于 Vite 的构建器,负责 Vue 应用的开发服务器和生产构建。

@nuxt/webpack-builder —— 基于 Webpack 的可选构建器(Nuxt 3 默认使用 Vite)。

@nuxt/vue-renderer —— 负责将 Vue 应用渲染为 HTML(SSR)或生成 SPA 的 HTML 壳。

Nitro 相关包(nitropack —— 独立的 HTTP 服务器引擎,有自己的构建管线,最终可以编译为各种平台的部署产物。

2.3 Nuxt 的生命周期(构建时 vs 运行时)

理解 Nuxt 最关键的一点是区分构建时(Build Time)运行时(Runtime)两个阶段:

构建时(nuxi build / nuxi dev)

  1. 加载 nuxt.config.ts 配置
  2. 初始化 Nuxt 实例(创建 NuxtApp 对象——注意这是构建时的内部对象,不是运行时的)
  3. 安装所有模块(Modules),按顺序执行每个模块的 setup 函数
  4. 扫描项目目录(pages、components、composables、plugins、middleware、server/api 等)
  5. 通过模板引擎生成虚拟文件(.nuxt/ 目录下的文件,如 app.config.mjsroutes.mjsplugins/server.mjs 等)
  6. 构建 Nitro 服务器应用
  7. 构建 Vue 客户端和服务端应用(通过 Vite 或 Webpack)
  8. 输出部署产物(.output/ 目录)

运行时(node .output/server/index.mjs)

  1. Nitro 服务器启动,监听 HTTP 请求
  2. 根据请求 URL 匹配渲染模式(SSR/SPA/ISR/SWR 等,由 Route Rules 决定)
  3. 如果是 SSR:在服务端执行 Vue SSR 渲染,生成 HTML + payload
  4. 返回 HTML 给浏览器
  5. 浏览器加载客户端 JS bundle
  6. Vue 客户端应用挂载(mount),执行水合(hydration),接管 DOM
  7. 客户端路由切换时,通过 payload 或 API 调用获取数据

第三章 构建管线深度剖析

3.1 Nuxt 初始化流程

当你运行 npx nuxi devnpx nuxi build 时,内部发生的事情:

text
用户执行 nuxi dev
       │
       ▼
  CLI 入口 (nuxi)
       │
       ▼
  加载 nuxt.config.ts
  (使用 jiti 执行 TypeScript 配置文件)
       │
       ▼
  创建 Nuxt 实例
  (调用 createNuxt(options))
       │
       ▼
  ┌─────────────────────────┐
  │  Nuxt 实例内部结构       │
  │  - options (完整配置)    │
  │  - hooks (钩子系统)      │
  │  - vfs (虚拟文件系统)    │
  │  - server (Nitro引用)    │
  └─────────────────────────┘
       │
       ▼
  安装所有模块 (installModules)
  按 nuxt.config 中 modules 数组的顺序
  依次调用每个模块的 setup()
       │
       ▼
  执行 Nuxt 钩子
  (modules:done → app:resolve → build:before → ...)
       │
       ▼
  扫描项目目录
  (pages/, components/, composables/, plugins/, middleware/)
       │
       ▼
  生成 .nuxt/ 虚拟文件
  (路由表、插件注册、组件注册等)
       │
       ▼
  启动开发服务器 (Vite dev server + Nitro dev server)
  或执行生产构建 (Vite build + Nitro build)

3.2 虚拟文件生成(.nuxt 目录)

Nuxt 在构建时会生成一个 .nuxt/ 目录,里面是自动生成的代码文件。这些文件是 Nuxt "约定优于配置" 的核心实现手段。关键文件包括:

.nuxt/app.config.mjs —— 合并用户 app.config.ts 和模块提供的配置。

.nuxt/components.plugin.mjs —— 注册所有自动发现的组件(来自 components/ 目录及模块注册的组件),使用 app.component() 全局注册。

.nuxt/imports.d.ts —— 自动导入的类型声明文件。Nuxt 使用 unimport 扫描 composables/ 目录和内置 composables,生成这个文件以提供 TypeScript 支持。

.nuxt/middleware/ —— 中间件注册相关代码。

.nuxt/nuxt.config.mjs —— 运行时的配置对象(注意与构建时的 nuxt.config.ts 不同,这是经过处理和 tree-shaking 后的精简版本)。

.nuxt/plugins/ —— 插件注册代码,分为 client 和 server 两个版本,控制插件的执行顺序和环境。

.nuxt/routes.mjs —— 基于 pages/ 目录结构自动生成的路由配置。这是 vue-router 的路由数组。

.nuxt/types/ —— 自动生成的 TypeScript 类型,包括路由类型、组件类型、布局类型等。

.nuxt/dist/ —— 构建产物(生产模式下)。

3.3 Vite 构建管线

Nuxt 3 默认使用 Vite 作为前端构建工具。构建过程分为两个独立的构建

客户端构建(Client Build)

服务端构建(Server Build)

在开发模式下,Vite 的开发服务器提供 HMR(Hot Module Replacement),Nitro 的开发服务器则负责 SSR 渲染和 API 路由。

3.4 自动导入系统(Auto-imports)

这是 Nuxt 3 最具特色的功能之一,底层依赖 unimportunplugin-auto-import

工作原理

  1. Nuxt 在构建时扫描以下来源的导出:
    • Vue 的核心 API(refcomputedwatchonMounted 等)
    • Vue Router 的 API(useRouteuseRouternavigateTo 等)
    • Nuxt 内置的 composables(useFetchuseAsyncDatauseStateuseHeaduseRuntimeConfig 等)
    • 用户 composables/ 目录下的所有导出
    • 用户 utils/ 目录下的所有导出
    • 模块注册的 composables
  2. 生成一个 import map,记录每个导出的来源文件
  3. 在构建时,通过 Vite/Rollup 插件对用户的源码进行 AST 分析:
    • 发现代码中使用了 ref 但没有 import → 自动在文件头部插入 import { ref } from 'vue'
    • 发现代码中使用了 useMyComposable → 自动插入 import { useMyComposable } from '~/composables/useMyComposable'
  4. 同时生成 .nuxt/types/imports.d.ts 提供 IDE 类型支持

面试关键点:自动导入不是魔法,它是构建时的静态分析 + 代码转换。它不会增加运行时开销,因为最终产物里就是普通的 ES module import。

3.5 组件自动注册

components/ 目录下的 Vue 组件会被自动注册。实现机制:

  1. Nuxt 扫描 components/ 目录(包括子目录和模块注册的组件路径)
  2. 为每个组件生成注册信息(名称、路径、是否异步加载等)
  3. .nuxt/components.plugin.mjs 中,使用 app.component() 全局注册
  4. 对于懒加载组件,使用 defineAsyncComponent 包装,配合 Vite 的代码分割实现按需加载
  5. 组件名称基于文件路径自动生成:components/ui/Button.vue<UiButton />

第四章 Nitro 服务器引擎

Nitro 是 Nuxt 3 最核心的架构创新。它不仅仅是一个服务器——它是一个与平台无关的 HTTP 应用引擎

4.1 Nitro 的设计理念

传统的 Nuxt 2 服务器紧耦合于 Node.js 和 Connect/Express 中间件模型。这导致了一个问题:如果你想部署到 Cloudflare Workers(基于 V8 Isolates,不支持 Node.js API),或者 AWS Lambda(Serverless 模型),需要大量的适配工作。

Nitro 的设计目标是:

4.2 h3:Nitro 的 HTTP 框架

Nitro 底层使用 h3 作为 HTTP 框架。h3(名字来源于 H₂O 的"三氢"——比 H₂O 多一个 H,寓意比传统 HTTP 框架多一点东西)是一个极简的、兼容 Node.js 和 Web Standards 的 HTTP 框架。

h3 的核心数据结构:

typescript
// H3Event —— 封装了每个 HTTP 请求的上下文
interface H3Event {
  // 底层的 Node.js 请求/响应对象(Node.js 环境)
  node: {
    req: IncomingMessage
    res: ServerResponse
  }
  // Web Standards 的 Request/Response(跨平台环境)
  web?: {
    request: Request
  }
  // 请求上下文,可以存储任意数据
  context: Record<string, any>
  // 请求的 Fetch API Request 对象
  _request?: Request
}

关键 API

typescript
// 读取请求体
const body = await readBody(event)       // JSON body
const formData = await readFormData(event) // form data
const rawBody = await readRawBody(event)   // raw buffer

// 读取查询参数
const query = getQuery(event)            // ?key=value → { key: 'value' }

// 读取路由参数
const params = getRouterParams(event)    // /api/user/:id → { id: '...' }

// 设置响应头/状态码
setResponseHeader(event, 'Content-Type', 'application/json')
setResponseStatus(event, 201)

// 发送响应
return { data: '...' }                   // 自动 JSON 序列化
return send(event, 'Hello')             // 发送字符串
return sendStream(event, readableStream) // 流式响应

// 错误处理
throw createError({
  statusCode: 404,
  statusMessage: 'Not Found',
  data: { id: '...' }
})

4.3 Nitro 的服务器目录约定

text
server/
├── api/                    # API 路由(自动注册为 /api/* 端点)
│   ├── hello.ts            # GET /api/hello
│   ├── hello.post.ts       # POST /api/hello(通过文件名后缀指定 HTTP 方法)
│   └── users/
│       ├── index.ts        # GET /api/users
│       ├── [id].ts         # GET /api/users/:id(动态路由参数)
│       └── [id].put.ts     # PUT /api/users/:id
├── routes/                 # 自定义路由(不自动加 /api 前缀)
│   └── health.ts           # GET /health
├── middleware/              # 服务器中间件(在每个请求前执行)
│   ├── auth.ts             # 认证中间件
│   └── logger.ts           # 日志中间件
├── plugins/                # Nitro 插件(服务器启动时执行一次)
│   └── database.ts         # 数据库连接初始化
└── utils/                  # 服务器工具函数(自动导入)
    └── validators.ts

API 路由的处理函数签名

typescript
// server/api/hello.ts
export default defineEventHandler(async (event: H3Event) => {
  // event 是 h3 的事件对象,包含了请求的所有信息

  // 读取查询参数
  const query = getQuery(event)

  // 读取请求体
  const body = await readBody(event)

  // 返回数据(自动序列化为 JSON)
  return {
    message: 'Hello World',
    query
  }
})

中间件的签名

typescript
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // 中间件可以修改 event.context,后续的处理函数可以读取
  event.context.userId = await verifyToken(getHeader(event, 'Authorization'))

  // 如果中间件返回了值,就直接作为响应返回,不再执行后续处理
  // 如果不返回值(return undefined),则继续执行后续处理
})

4.4 Nitro 的构建与部署

Nitro 的构建过程:

  1. 收集处理器(Handlers):扫描 server/api/server/routes/server/middleware/,加上 Nuxt 内置的 SSR 渲染处理器
  2. 构建 Rollup Bundle:使用 Rollup 打包服务器代码
  3. 平台适配:根据 preset 配置选择目标平台的入口模板(Node.js、Cloudflare Workers、AWS Lambda 等)
  4. 静态资源处理:将 public/ 和客户端构建产物复制到输出目录
  5. 生成部署产物:输出到 .output/ 目录

部署产物结构

text
.output/
├── server/
│   ├── index.mjs           # 服务器入口(可直接 node index.mjs 启动)
│   ├── chunks/             # 代码分割后的 chunk 文件
│   └── plugins/            # Nitro 插件
├── public/                 # 静态资源
│   ├── _nuxt/              # 客户端构建产物(JS/CSS/字体等)
│   ├── favicon.ico
│   └── ...
├── nitro.json              # Nitro 元数据
└── package.json            # 精简的 package.json(只包含运行时依赖)

Route Rules(路由规则)—— Nitro 最强大的特性之一:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 这个页面使用 SPA 模式(不 SSR)
    '/admin/**': { ssr: false },

    // 这个页面静态生成(构建时生成 HTML)
    '/blog': { prerender: true },

    // 这个 API 使用 ISR(增量静态再生成),缓存 1 小时
    '/api/popular': { isr: 3600 },

    // 这个页面使用 SWR(stale-while-revalidate),缓存 10 分钟
    '/products/**': { swr: 600 },

    // 这个路径重定向
    '/old-page': { redirect: '/new-page' },

    // 添加自定义响应头
    '/api/**': {
      headers: { 'X-Custom-Header': 'value' }
    }
  }
})

这意味着在同一个 Nuxt 应用中,不同路由可以使用不同的渲染策略。这在实际项目中非常实用:管理后台用 SPA(因为需要登录态),营销页面用静态生成(追求性能和 SEO),数据 API 用 ISR/SWR(平衡性能和实时性)。


第五章 Nuxt 应用层架构

5.1 应用入口与启动流程

Nuxt 客户端应用的启动过程(运行时):

text
浏览器加载 HTML
       │
       ▼
  加载客户端 JS Bundle
       │
       ▼
  执行 Nuxt 入口文件
  (.nuxt/dist/client/app.config.mjs → nuxt/dist/app/)
       │
       ▼
  创建 Vue App 实例
  (createApp() 或 createSSRApp())
       │
       ▼
  创建 NuxtApp 实例
  (运行时 NuxtApp,包含插件、钩子、payload 等)
       │
       ▼
  安装 Vue 插件
  (router、pinia、全局组件等)
       │
       ▼
  执行 Nuxt 插件
  (按顺序执行 plugins/ 目录下的插件)
       │
       ▼
  挂载 Vue App 到 DOM
  (app.mount('#__nuxt'))
       │
       ▼
  水合(Hydration)
  (将服务端渲染的静态 HTML "激活"为可交互的 Vue 应用)

5.2 运行时 NuxtApp 数据结构

这是 Nuxt 3 运行时最核心的数据结构。通过 useNuxtApp() 可以在任何组件或 composable 中访问:

typescript
interface NuxtApp {
  // Vue 应用实例
  vueApp: App<Element>

  // 全局属性(类似 Vue 2 的 Vue.prototype)
  globalName: string   // 默认 'nuxt'

  // 版本信息
  versions: Record<string, string>

  // 钩子系统
  hooks: Hookable<NuxtAppHooks>
  hook: NuxtApp['hooks']['hook']
  callHook: NuxtApp['hooks']['callHook']

  // 运行回调(注册在 app 挂载/渲染时执行的回调)
  _asyncDataPromises: Record<string, Promise<any>>

  // SSR 上下文(仅在服务端存在)
  ssrContext?: NuxtSSRContext

  // Payload(服务端传递给客户端的数据)
  payload: {
    serverRendered: boolean
    data: Record<string, any>
    state: Record<string, any>
    error?: Error | { url: string, statusCode: number, statusMessage: string }
    _errors: Record<string, any>
    [key: string]: any
  }

  // 是否在水合阶段
  isHydrating: boolean

  // 提供/注入(类似 Vue 的 provide/inject,但在 NuxtApp 层面)
  provide: (name: string, value: any) => void

  // 内部属性
  _scope: EffectScope
  _id: number
  _processingPlugin: boolean
}

面试关键点NuxtApp 是每个 Nuxt 请求的核心上下文对象。在服务端,每个请求有独立的 NuxtApp 实例(通过 AsyncLocalStorage 或显式传递来隔离);在客户端,只有一个全局的 NuxtApp 实例。

5.3 NuxtSSRContext 数据结构

在服务端渲染时,每个请求都有一个 NuxtSSRContext 对象,它承载了 SSR 过程中的所有上下文信息:

typescript
interface NuxtSSRContext {
  // 当前请求的 URL
  url: string

  // 请求的事件对象(h3 的 H3Event)
  event: H3Event

  // 请求的来源(IP、Host 等)
  req: IncomingMessage  // Node.js 环境
  res: ServerResponse   // Node.js 环境

  // 渲染结果
  noSSR: boolean          // 是否跳过 SSR
  error?: any             // SSR 过程中的错误

  // 渲染过程中的数据收集
  payload: {
    data: Record<string, any>    // useAsyncData 的缓存
    state: Record<string, any>   // useState 的缓存
    _errors: Record<string, any>
  }

  // 用于 <head> 标签管理(useHead)
  head: HeadEntry[]

  // 组件渲染追踪
  renderedComponents?: Set<string>

  // 岛屿组件相关
  islands: Map<string, any>

  // 渲染出的 HTML
  renderResult?: {
    html: string
  }

  // 缓存和去重
  _asyncDataPromises: Record<string, Promise<any>>

  // Teleports(Vue 的 <Teleport> 组件渲染结果)
  teleports: Record<string, string>
}

5.4 页面、布局与组件系统

页面(Pages)

pages/ 目录下的每个 .vue 文件对应一个路由。Nuxt 使用 vue-router 作为路由引擎,但增加了以下增强:

vue
<!-- pages/index.vue -->
<script setup>
definePageMeta({
  layout: 'default',         // 使用哪个布局
  middleware: ['auth'],      // 使用哪些中间件
  // 路由元信息
  pageType: 'home',
  requiresAuth: false,
  // 过渡动画
  pageTransition: { name: 'page', mode: 'out-in' },
  layoutTransition: { name: 'layout', mode: 'out-in' },
  // keep-alive
  keepalive: true,
  // 自定义滚动行为
  scrollToTop: true
})
</script>

注意definePageMeta 是一个编译器宏(compiler macro),类似于 Vue 的 defineProps。它在构建时被提取出来作为路由元信息,不会出现在组件的 setup 代码中。Nuxt 通过一个 Vite/Rollup 插件在构建时将其提取。

布局(Layouts)

layouts/ 目录定义页面布局模板。布局本质上是带有 <slot /> 的 Vue 组件:

vue
<!-- layouts/default.vue -->
<template>
  <div>
    <AppHeader />
    <main>
      <slot />   <!-- 页面内容在这里渲染 -->
    </main>
    <AppFooter />
  </div>
</template>

页面通过 definePageMeta({ layout: 'default' }) 选择布局。布局切换时会触发过渡动画。

嵌套布局的渲染链

text
<NuxtLayout>            ← 最外层布局
  └── <NuxtPage>       ← 页面组件
        └── <NuxtLayout>  ← 如果页面内部又有布局(嵌套路由场景)
              └── <NuxtPage>  ← 子路由页面

5.5 Nuxt 内置组件

Nuxt 提供了几个核心的内置组件,它们是框架功能的入口点:

<NuxtPage> —— 路由出口,等价于 <RouterView> 但增加了 Nuxt 的增强:自动的页面过渡动画(pageTransition)、自动的 keepalive 支持、将 pageKey 作为 prop 传递,控制组件何时重新渲染。

<NuxtLayout> —— 布局容器,管理布局切换和布局过渡动画。

<NuxtLink> —— 增强的链接组件,基于 <RouterLink> 增加了:

vue
<NuxtLink to="/about" prefetch>关于我们</NuxtLink>
<NuxtLink to="/products" :prefetch="false">产品</NuxtLink>
<NuxtLink to="https://example.com">外部链接</NuxtLink>

<NuxtLoadingIndicator> —— 页面导航进度条。

<NuxtErrorBoundary> —— 错误边界组件,捕获子组件中的错误。

<ClientOnly> —— 只在客户端渲染的内容(跳过 SSR)。

<ServerOnly> / <NuxtIsland> —— 岛屿架构相关组件(后面详述)。

<Suspense>(Vue 原生)—— Nuxt 的页面组件默认被 <Suspense> 包裹,支持异步 setup。


第六章 渲染系统深度剖析

这是面试中最常被问到的部分。

6.1 通用渲染(Universal Rendering)的完整流程

第一次请求(SSR)

text
① 浏览器发送 HTTP 请求 → https://example.com/products

② Nitro 服务器接收请求
   - h3 路由匹配 → 进入 SSR 渲染处理器
   - 检查 Route Rules:该路径是否配置了 SSR?
   - 创建 NuxtSSRContext

③ Vue SSR 渲染
   - 创建 Vue 应用实例(createSSRApp)
   - 安装路由,设置当前 URL
   - 安装所有 Nuxt 插件
   - 执行页面组件的 setup()
     → useAsyncData() / useFetch() 被调用
     → 在服务端发起数据请求
     → 数据存入 NuxtSSRContext.payload.data
   - Vue 渲染器将虚拟 DOM 渲染为 HTML 字符串
     (使用 vue-bundle-renderer 的 renderToString)
   - 收集渲染过程中触发的 <Head> 标签
   - 收集 <Teleport> 内容

④ 组装完整 HTML
   Nitro 将各部分拼接:

   <!DOCTYPE html>
   <html>
   <head>
     <!-- 来自 useHead() 的标签 -->
     <title>Products</title>
     <meta name="description" content="..." />
     <link rel="stylesheet" href="/_nuxt/xxx.css" />
   </head>
   <body>
     <div id="__nuxt">
       <!-- SSR 渲染出的 HTML -->
       <div class="products-page">
         <h1>Products</h1>
         <ul>
           <li>Product 1 - $99</li>
           <li>Product 2 - $199</li>
         </ul>
       </div>
     </div>

     <!-- Payload 注入脚本 -->
     <script>
       window.__NUXT__ = {
         data: { "products-page": { data: [...], pending: false } },
         state: { ... },
         serverRendered: true,
         config: { ... }
       }
     </script>

     <script type="module" src="/_nuxt/entry.js"></script>
   </body>
   </html>

⑤ 返回 HTML 给浏览器

⑥ 浏览器渲染 HTML
   - 用户立即看到页面内容(有内容但不可交互)
   - 同时下载客户端 JS Bundle

⑦ 客户端 JS 执行
   - 创建 Vue 应用实例(createSSRApp)
   - 读取 window.__NUXT__ 中的 payload
   - 使用 payload 中的数据初始化 useAsyncData 缓存
     (避免客户端再次请求相同数据)
   - 执行水合(Hydration):
     Vue 将已有的 DOM 与虚拟 DOM 进行匹配,
     添加事件监听器,激活响应式系统

⑧ 页面变为可交互状态(Interactive)

6.2 水合(Hydration)详解

水合是 SSR 应用中最关键也最容易出问题的环节。

水合的本质:服务端渲染出了"静态"的 HTML(有结构但没有交互能力),水合是 Vue 在客户端"接管"这些 DOM 的过程:

  1. DOM 匹配:Vue 遍历服务端渲染的 DOM 树,与客户端虚拟 DOM 进行对比匹配
  2. 事件绑定:为匹配的 DOM 元素添加事件监听器(click、input 等)
  3. 响应式激活:将 Vue 的响应式系统绑定到 DOM 元素上
  4. 状态同步:从 payload 中恢复 useAsyncDatauseState 的数据

水合不匹配(Hydration Mismatch)

当服务端渲染的 HTML 结构与客户端首次渲染的虚拟 DOM 不一致时,就会发生水合不匹配。常见原因:

解决方案

vue
<!-- 方案1:使用 ClientOnly -->
<ClientOnly>
  <ClientSideOnlyComponent />
</ClientOnly>

<!-- 方案2:使用 onMounted 延迟渲染 -->
<script setup>
const isClient = ref(false)
onMounted(() => { isClient.value = true })
</script>
<template>
  <div v-if="isClient">{{ new Date().toLocaleString() }}</div>
  <div v-else>Loading time...</div>
</template>

<!-- 方案3:使用 useHydration 的友好方式 -->
<script setup>
const nuxtApp = useNuxtApp()
// nuxtApp.isHydrating 为 true 时,说明正在水合
</script>

6.3 客户端导航(Client-Side Navigation)

首次请求之后,用户点击 <NuxtLink> 进行的页面切换不再发起完整的 HTML 请求,而是客户端路由:

text
① 用户点击 <NuxtLink to="/about">

② NuxtLink 拦截点击事件
   - 调用 vue-router 的 push()
   - URL 变为 /about(History API)

③ Nuxt 客户端路由守卫触发
   - 执行全局中间件(middleware/)
   - 执行页面级中间件(definePageMeta 中的 middleware)
   - 如果中间件调用了 navigateTo() 或 abortNavigation(),导航中止

④ 加载目标页面的 JS chunk
   - 如果该 chunk 还没有加载,动态 import()
   - 这就是为什么 Nuxt 页面是自动代码分割的

⑤ 执行目标页面的 setup()
   - useAsyncData() / useFetch() 被调用
   - 如果 payload 中已有该页面的数据(通过 prefetch),直接使用
   - 否则发起 API 请求获取数据

⑥ 渲染新页面组件
   - 触发页面过渡动画(如果有配置)
   - Vue 更新 DOM

⑦ 页面更新完成

6.4 Payload 传递机制

Payload 是 SSR 中服务端向客户端传递数据的核心机制。

服务端

序列化:Nuxt 使用自定义的序列化器(基于 destrsuperjson 的思想)将 payload 序列化为 JavaScript 代码,嵌入到 HTML 的 <script> 标签中。支持的类型包括:Date、Map、Set、RegExp、URL、undefined、BigInt 等。

客户端

Payload 提取优化(Nuxt 3.x):在较新版本的 Nuxt 3 中,payload 不再通过 window.__NUXT__ 全局变量传递,而是通过 <script type="application/json"> 标签或 <script type="module"> 内联模块的方式传递,这允许浏览器以更高效的方式解析。

6.5 其他渲染模式

SPA 模式(Client-Only Rendering)

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: false  // 全局关闭 SSR
})
// 或者通过 Route Rules 对特定路由关闭

SPA 模式下,Nitro 服务器只返回一个空的 HTML 壳(包含 JS bundle 引用),所有渲染在客户端完成。与纯 Vue SPA 的区别是仍然可以使用 Nuxt 的文件路由、插件、模块等功能。

静态站点生成(SSG - Static Site Generation)

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  // 构建时使用 SSR 渲染每个路由,生成静态 HTML
  // 运行时不需要 Node.js 服务器
})
// 执行 npx nuxi generate

nuxi generate 会:构建应用 → 启动临时 Nitro 服务器 → 遍历所有路由 → 对每个路由执行 SSR 渲染 → 将 HTML 和静态资源输出到 .output/public/ 目录 → 部署时只需要一个静态文件服务器。

ISR(Incremental Static Regeneration)

typescript
routeRules: {
  '/blog/**': { isr: 3600 }  // 每小时重新生成
}

页面在首次请求时静态生成并缓存。后续请求直接使用缓存。缓存过期后,下一个请求会触发后台重新生成,同时仍然返回旧的缓存(stale-while-revalidate 模式)。

岛屿架构(Island Architecture)

这是一种混合渲染策略:页面大部分是静态 HTML(不水合),只有标记为"岛屿"的组件会在客户端水合:

vue
<template>
  <div>
    <!-- 这个组件在服务端渲染,不在客户端水合 -->
    <StaticContent />

    <!-- 这个组件在服务端渲染,并在客户端水合(可交互) -->
    <NuxtIsland name="InteractiveChart" :props="{ data }" />
  </div>
</template>

好处:减少客户端 JS 体积和水合时间,提升首屏性能。适合页面中大部分内容是静态的、只有少部分是交互式的场景。


第七章 数据获取系统

7.1 useAsyncData —— 数据获取的基石

useAsyncData 是 Nuxt 3 数据获取的核心 composable。它解决了一个关键问题:如何让同一份数据获取代码在服务端和客户端都能正确运行,且数据能在两端之间同步

typescript
// 签名
function useAsyncData<T>(
  key: string,                          // 唯一标识符
  handler: () => Promise<T>,            // 数据获取函数
  options?: AsyncDataOptions<T>         // 配置项
): AsyncData<T>

function useAsyncData<T>(
  handler: () => Promise<T>,            // key 可以省略(自动基于文件路径生成)
  options?: AsyncDataOptions<T>
): AsyncData<T>

返回值数据结构

typescript
interface AsyncData<T> {
  data: Ref<T | null>           // 数据(响应式引用)
  pending: Ref<boolean>         // 加载状态
  status: Ref<'idle' | 'pending' | 'success' | 'error'>
  error: Ref<Error | null>      // 错误信息
  refresh: () => Promise<void>  // 手动刷新数据
  execute: () => Promise<void>  // 手动执行数据获取
  clear: () => void             // 清除数据
}

配置项

typescript
interface AsyncDataOptions<T> {
  server?: boolean          // 是否在服务端执行(默认 true)
  lazy?: boolean            // 是否懒加载(默认 false)
  default?: () => T | Ref<T>  // 默认值
  transform?: (data: T) => T   // 数据转换函数
  pick?: string[]           // 数据选取函数
  watch?: Ref[]             // 响应式值变化时自动重新获取
  immediate?: boolean       // 是否立即执行(默认 true)
  deep?: boolean            // 深度监听(默认 true)
  dedupe?: 'cancel' | 'defer'  // 去重策略
}

内部工作原理

text
useAsyncData(key, handler) 被调用
       │
       ▼
  检查 NuxtApp._asyncDataPromises[key]
  是否已有相同 key 的进行中请求?
       │
       ├── 有 → 复用已有的 Promise(去重)
       │
       └── 没有 →
              │
              ▼
         检查 NuxtApp.payload.data[key]
         服务端是否已经获取了数据?(SSR payload)
              │
              ├── 有 → 直接使用 payload 中的数据
              │         不调用 handler
              │         data.value = payload.data[key]
              │
              └── 没有 →
                     │
                     ▼
                  检查当前环境
                     │
                     ├── 服务端(SSR)
                     │   → 调用 handler()
                     │   → 结果存入 payload.data[key]
                     │   → 会被序列化到 HTML 中
                     │
                     └── 客户端
                         → 调用 handler()
                         → 结果存入 data.value
                         → 不进入 payload

7.2 useFetch —— useAsyncData 的便捷封装

useFetchuseAsyncData + $fetch 的组合,用于最常见的"调用 API 获取数据"场景:

typescript
// 签名
function useFetch<T>(
  url: string | Ref<string> | (() => string),  // URL(可以是响应式的)
  options?: UseFetchOptions<T>
): AsyncData<T>

useFetch 的 options 扩展了 useAsyncData 的 options,额外支持

typescript
interface UseFetchOptions<T> extends AsyncDataOptions<T> {
  method?: string          // 请求方法
  query?: Record<string, any>   // 查询参数
  params?: Record<string, any>  // URL 路径参数
  body?: any               // 请求体
  headers?: Record<string, string>  // 请求头
  baseURL?: string          // 基础 URL
  onResponse?: (context: { response: Response }) => void
  onResponseError?: (context: { response: Response }) => void
  onRequest?: (context: { request: Request, options: any }) => void
  onRequestError?: (context: { request: Request, options: any, error: Error }) => void
  transform?: (data: any) => T
  pick?: string[]
  timeout?: number
}

示例

typescript
// 基本用法
const { data, pending, error, refresh } = await useFetch('/api/products')

// 带参数
const { data } = await useFetch('/api/products', {
  method: 'POST',
  body: { category: 'electronics' },
  query: { page: 1, limit: 20 }
})

// URL 响应式(URL 变化时自动重新获取)
const id = ref(1)
const { data } = await useFetch(() => `/api/products/${id.value}`)

// watch 触发重新获取
const search = ref('')
const { data } = await useFetch('/api/search', {
  query: { q: search },
  watch: [search]
})

// transform + pick
const { data } = await useFetch('/api/users', {
  transform: (response) => response.data,
  pick: ['name', 'email']
})

7.3 $fetch —— 底层 HTTP 客户端

$fetch 是 Nuxt 3 内置的 HTTP 客户端,基于 ofetch 库。它可以在服务端和客户端统一使用:

typescript
// 在服务端:
// - 如果请求的是本站 API(如 /api/xxx),直接调用 Nitro 处理函数
//   不发起真实的 HTTP 请求(零网络开销!)
// - 如果请求的是外部 URL,发起真实的 HTTP 请求

// 在客户端:
// - 总是发起真实的 HTTP 请求(fetch API)

// 用法
const data = await $fetch('/api/users', {
  method: 'GET',
  query: { page: 1 },
  headers: { Authorization: 'Bearer xxx' }
})

// 注意:$fetch 不会自动处理 SSR 数据同步
// 如果需要 SSR 兼容,应该用 useFetch 或 useAsyncData + $fetch

关键面试题$fetchuseFetch 的区别是什么?

7.4 服务端 API 中的直接调用(避免自调自)

这是一个重要的性能优化点。当 SSR 渲染时,页面组件中的 useFetch('/api/products') 实际上不需要发起真实的 HTTP 请求,因为请求者和被请求者在同一个进程中:

text
┌──────────────────────────────────────┐
│         Nitro 服务器进程              │
│                                      │
│  SSR 渲染器                          │
│    │                                 │
│    ├── useFetch('/api/products')     │
│    │     │                           │
│    │     ▼                           │
│    │   $fetch 检测到是本站 API        │
│    │     │                           │
│    │     ▼                           │
│    │   直接调用 API handler 函数      │
│    │   (不经过网络层,零开销)        │
│    │     │                           │
│    │     ▼                           │
│    │   server/api/products.ts        │
│    │   的 default export 被直接调用   │
│    │                                 │
│  server/api/products.ts              │
│    └── defineEventHandler(...)       │
└──────────────────────────────────────┘

这是通过 h3/Nitro 的 direct call 机制实现的:$fetch 在内部检查请求的 URL 是否匹配已注册的服务器处理器,如果匹配,直接调用处理函数而非发起 HTTP 请求。

7.5 useCookie —— SSR 安全的 Cookie 操作

useCookie 是 Nuxt 内置的 Cookie 读写 composable,解决了 SSR 中 Cookie 读写的核心难题:服务端没有 document.cookie

typescript
// 签名
function useCookie<T = string | null>(
  name: string,
  options?: CookieOptions
): Ref<T | null>

interface CookieOptions {
  path?: string          // 默认 '/'
  domain?: string
  maxAge?: number         // 秒
  expires?: Date
  httpOnly?: boolean
  secure?: boolean
  sameSite?: 'strict' | 'lax' | 'none' | boolean
  default?: () => T       // 默认值
  watch?: boolean | 'shallow'
  readonly?: boolean
}

工作原理

typescript
// 基本用法
const token = useCookie('auth-token')
token.value = 'new-jwt-token'  // 自动写入 Cookie

// 带默认值和类型
const theme = useCookie<'light' | 'dark'>('theme', {
  default: () => 'light',
  maxAge: 60 * 60 * 24 * 365  // 1年
})

// 存储复杂数据(自动 JSON 序列化/反序列化)
const preferences = useCookie('user-prefs', {
  default: () => ({ sidebar: true, compact: false })
})

面试关键点useCookie 在 SSR 中读写的是当前请求的 Cookie,而非浏览器中的 Cookie。这意味着在服务端修改 Cookie 后,同一请求中后续的读取能看到新值,但浏览器要等到收到 Set-Cookie 响应头后才会更新。

7.6 useRequestHeaders 与 useRequestFetch

这两个 composable 解决 SSR 中的一个常见问题:服务端发起的 API 请求不会自动携带客户端的请求头(如 Cookie、Authorization)

typescript
// useRequestHeaders —— 获取当前 SSR 请求的请求头
const headers = useRequestHeaders()                    // 所有请求头
const cookies = useRequestHeaders(['cookie'])           // 只要 cookie 头
const authHeaders = useRequestHeaders(['authorization']) // 只要 auth 头

// useRequestFetch —— 创建一个自动转发 SSR 请求头的 $fetch
const fetch = useRequestFetch()
const { data } = await useAsyncData('user', () =>
  fetch('/api/auth/me')  // 自动携带 SSR 请求的 Cookie
)

为什么需要这个:在 SSR 渲染中,useFetch('/api/auth/me') 默认走 Nitro 的 direct call 机制(直接调用 handler 函数),此时 handler 中的 getRequestHeaders(event) 获取到的是内部调用的请求,而不是原始浏览器请求的 Cookie。useRequestFetch 会将原始请求的关键请求头(尤其是 Cookie)注入到内部调用中,确保认证信息不丢失。

推荐实践

typescript
// composables/useAuth.ts
export const useAuth = () => {
  const fetch = useRequestFetch()

  const { data: user } = await useAsyncData('user', () =>
    fetch('/api/auth/me').catch(() => null)
  )

  return { user, isLoggedIn: computed(() => !!user.value) }
}

7.7 Nitro 存储层(unstorage)

Nitro 内置了 unstorage,提供了一个统一的键值存储抽象,可以对接多种后端:

typescript
// server/api/cache.ts
export default defineEventHandler(async (event) => {
  const storage = useStorage()

  await storage.setItem('cache:key', { data: 'value' })
  const value = await storage.getItem('cache:key')

  await storage.setItem('redis:user:123', userData)

  return value
})
typescript
// nuxt.config.ts —— 配置存储驱动
export default defineNuxtConfig({
  nitro: {
    storage: {
      cache: { driver: 'memory' },
      redis: {
        driver: 'redis',
        host: process.env.REDIS_HOST,
        port: 6379
      },
      files: {
        driver: 'fs',
        base: './data'
      }
    }
  }
})

这在 Serverless 和 Edge 环境中特别有用,因为这些环境通常没有本地文件系统或持久化存储,unstorage 提供了一致的接口来对接 Cloudflare KV、Upstash Redis、AWS DynamoDB 等云存储服务。


第八章 状态管理

8.1 useState —— Nuxt 内置的跨端状态

useState 是 Nuxt 提供的最简单的跨端状态管理工具:

typescript
// 签名
function useState<T>(
  key: string,                  // 唯一标识符
  init?: () => T | Ref<T>      // 初始值工厂函数
): Ref<T>

工作原理:服务端创建响应式引用并存入 payload.state → SSR 完成后序列化到 HTML → 客户端水合时从 payload 恢复 → 后续客户端调用直接使用已有值。

typescript
// composables/useCounter.ts
export const useCounter = () => {
  const count = useState('counter', () => 0)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

8.2 Pinia 集成

Nuxt 3 官方推荐通过 @pinia/nuxt 模块集成 Pinia:

typescript
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const isLoggedIn = computed(() => !!user.value)

  async function login(credentials) {
    user.value = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials
    })
  }

  function logout() {
    user.value = null
    navigateTo('/login')
  }

  return { user, isLoggedIn, login, logout }
})

Pinia 与 SSR 的关系:Pinia 的 store 状态默认不会自动通过 SSR payload 传递。需要通过 Nuxt 的插件或 useState 来桥接。实际上,@pinia/nuxt 模块已经内置了这种 SSR 状态桥接,开发者不需要手动处理。

8.3 useAsyncData 缓存作为状态

在很多场景下,useAsyncData 的缓存本身就是一种状态管理方式。相同 keyuseAsyncData 在多个组件中共享同一份数据:

typescript
// composables/useProducts.ts
export const useProducts = () => {
  return useAsyncData('products', () => $fetch('/api/products'))
}

// 在组件 A 中
const { data: products } = await useProducts()
// 在组件 B 中
const { data: products } = await useProducts()
// 两个组件共享同一份数据,只发起一次请求

第九章 插件系统

9.1 插件的定义与执行顺序

typescript
// plugins/myPlugin.ts

// 方式1:简单函数
export default defineNuxtPlugin((nuxtApp) => {
  // nuxtApp 是运行时的 NuxtApp 实例
  // nuxtApp.vueApp.use(someVuePlugin)
  nuxtApp.provide('myHelper', () => { /* ... */ })
})

// 方式2:对象形式(更精细的控制)
export default defineNuxtPlugin({
  name: 'my-plugin',
  enforce: 'pre',             // 执行时机:'pre' | 'default' | 'post'
  dependsOn: ['other-plugin'],
  async setup(nuxtApp) {
    // 插件逻辑
  },
  hooks: {
    'app:created'(vueApp) { /* ... */ },
    'page:start'() { /* ... */ }
  }
})

// 通过文件名控制环境:
// plugins/myPlugin.client.ts  → 只在客户端执行
// plugins/myPlugin.server.ts  → 只在服务端执行

插件执行顺序

  1. enforce: 'pre' 的插件最先执行
  2. enforce: 'default'(默认)的插件按文件名字母顺序执行
  3. enforce: 'post' 的插件最后执行
  4. 如果有 dependsOn,会等待依赖的插件完成后再执行

9.2 插件可以做什么


第十章 中间件系统

10.1 路由中间件

路由中间件在每次页面导航时执行,用于权限验证、数据预加载、日志等:

typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const authStore = useAuthStore()

  if (!authStore.isLoggedIn && to.meta.requiresAuth) {
    return navigateTo('/login')
  }

  // 不返回值 = 继续导航
  // 返回 navigateTo('/path') = 重定向
  // 返回 abortNavigation() = 中止导航
  // 返回 abortNavigation(error) = 中止并设置错误
})

中间件分类

中间件执行顺序:全局中间件(按文件名字母序) → 命名中间件(按 definePageMeta 中数组的顺序)

执行环境细节:路由中间件同时在服务端和客户端执行。在 SSR 首次请求时,Nuxt 会在服务端执行路由中间件。在后续客户端导航时,中间件在客户端执行。这意味着中间件代码应该避免直接使用浏览器专有 API,或通过 import.meta.client / import.meta.server 做环境判断。

10.2 服务端中间件(Server Middleware)

typescript
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // 对每个 API 请求和 SSR 请求都会执行
  const token = getHeader(event, 'Authorization')
  if (token) {
    event.context.user = await verifyToken(token)
  }
  // 不返回值 = 继续处理
  // 返回值 = 直接响应,不再执行后续处理
})

第十一章 模块系统

11.1 模块的概念与作用

模块是 Nuxt 的扩展机制。一个模块可以在构建时修改 Nuxt 的配置、注册组件、添加插件、注入服务器处理器等。Nuxt 生态的绝大部分功能(Tailwind CSS、Pinia、Content、Image、Auth 等)都是通过模块提供的。

11.2 模块的定义

typescript
// modules/myModule.ts
import { defineNuxtModule, addPlugin, addComponent, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  meta: {
    name: 'my-module',
    configKey: 'myModule',
    compatibility: { nuxt: '>=3.0.0' }
  },
  defaults: {
    apiKey: '',
    enableFeature: true
  },
  async setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)

    // 1. 注册插件
    addPlugin(resolver.resolve('./runtime/plugin'))

    // 2. 注册组件
    addComponent({
      name: 'MyComponent',
      filePath: resolver.resolve('./runtime/MyComponent.vue')
    })

    // 3. 注册 composable(使其可被自动导入)
    nuxt.hook('imports:dirs', (dirs) => {
      dirs.push(resolver.resolve('./runtime/composables'))
    })

    // 4. 注册服务器 API
    addServerHandler({
      route: '/api/my-module/data',
      handler: resolver.resolve('./runtime/server/api/data')
    })

    // 5. 修改 Nuxt 配置
    nuxt.options.css.push('my-module/dist/styles.css')

    // 6. 修改 Vite 配置
    nuxt.hook('vite:extendConfig', (config) => {
      config.resolve.alias['#my-module'] = resolver.resolve('./runtime')
    })

    // 7. 注册 Nitro 插件
    nuxt.hook('nitro:config', (nitroConfig) => {
      nitroConfig.plugins = nitroConfig.plugins || []
      nitroConfig.plugins.push(resolver.resolve('./runtime/server/plugin'))
    })
  }
})

11.3 模块的生命周期钩子

Nuxt 在构建过程中会触发一系列钩子,模块可以监听这些钩子:

text
modules:before          ← 模块安装前
modules:done            ← 所有模块安装完成
app:resolve             ← Vue 应用配置解析完成
app:templates           ← 虚拟文件模板生成前
app:templatesGenerated  ← 虚拟文件生成完成
build:before            ← 构建开始前
build:done              ← 构建完成
pages:extend            ← 路由表生成后(可添加/修改路由)
components:extend       ← 组件列表生成后
imports:dirs            ← 自动导入目录收集
imports:extend          ← 自动导入列表生成后
nitro:config            ← Nitro 配置生成后
nitro:init              ← Nitro 实例创建后
vite:extendConfig       ← Vite 配置生成后
vite:serverCreated      ← Vite 开发服务器创建后

11.4 @nuxt/kit 核心 API 速查

typescript
defineNuxtModule({ meta, defaults, setup })  // 定义模块
addPlugin({ src, mode })                       // 添加插件
addComponent({ name, filePath })               // 添加组件
addServerHandler({ route, handler, method, middleware })  // 添加服务器处理器
addServerPlugin({ src })                       // 添加服务器插件
addRouteMiddleware({ name, path, global })     // 添加路由中间件
extendPages((pages) => { pages.push(...) })    // 修改页面路由
extendViteConfig((config) => { ... })          // 修改 Vite 配置
extendWebpackConfig((config) => { ... })       // 修改 Webpack 配置
addVitePlugin(plugin)                          // 安装 Vite 插件
createResolver(import.meta.url)                // 创建路径解析器
useNuxt()                                      // 获取 Nuxt 实例
useLogger('my-module')                         // 创建日志

第十二章 Nuxt 钩子系统(Hooks)

12.1 NuxtApp 钩子(运行时)

这些钩子在运行时触发,可在插件或组件中监听:

typescript
// 应用生命周期
app:beforeMount         // Vue 应用挂载前
app:mounted             // Vue 应用挂载完成
app:error               // 应用发生错误
app:suspense:resolve    // Suspense 解析完成

// 页面导航
page:start              // 页面导航开始
page:finish             // 页面导航完成
page:loading:start      // 页面加载开始
page:loading:end        // 页面加载结束
page:transition:finish  // 页面过渡动画完成

// 数据获取
asyncData:start         // useAsyncData 开始
asyncData:end           // useAsyncData 结束

// 渲染
app:rendered            // SSR 渲染完成(仅服务端)
app:chunkError          // 加载 JS chunk 失败

// 使用方式
const nuxtApp = useNuxtApp()
nuxtApp.hook('page:finish', () => {
  console.log('Page navigation finished')
})

12.2 Nuxt 钩子(构建时)

这些钩子在构建时触发,可在模块中监听(第十一章已列出)。

12.3 Nitro 钩子(服务器运行时)

typescript
// 请求生命周期
request            // 收到请求
beforeResponse     // 响应发送前
afterResponse      // 响应发送后
error              // 请求处理出错

// 渲染
render:html        // HTML 渲染后(可修改 HTML)
render:response    // 响应准备发送前

// 使用方式(在 Nitro 插件中)
// server/plugins/renderHook.ts
export default defineStritroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    // html 是一个对象,包含 head、bodyAppend 等数组
    html.head.push('<link rel="preconnect" href="https://fonts.googleapis.com" />')
  })
})

第十三章 目录结构与文件约定详解

13.1 完整目录结构

text
my-nuxt-app/
├── .nuxt/                    # 自动生成的虚拟文件(不要手动修改,加入 .gitignore)
├── .output/                  # 生产构建产物
├── app.vue                   # 应用根组件(入口组件)
├── app.config.ts             # 应用配置(可在运行时通过 payload 传递)
├── error.vue                 # 全局错误页面
├── nuxt.config.ts            # Nuxt 配置(构建时)
├── tsconfig.json             # TypeScript 配置
│
├── assets/                   # 静态资源(会被构建工具处理)
│   ├── css/
│   ├── images/
│   └── fonts/
│
├── public/                   # 公共资源(直接复制到输出目录)
│   ├── favicon.ico
│   └── robots.txt
│
├── components/               # Vue 组件(自动注册)
│   ├── AppHeader.vue         # → <AppHeader />
│   ├── AppFooter.vue         # → <AppFooter />
│   ├── ui/
│   │   ├── Button.vue        # → <UiButton />
│   │   └── Input.vue         # → <UiInput />
│   └── base/
│       └── Card.vue          # → <BaseCard />
│
├── composables/              # 组合式函数(自动导入)
│   ├── useAuth.ts
│   ├── useProducts.ts
│   └── useCounter.ts
│
├── utils/                    # 工具函数(自动导入)
│   ├── formatters.ts
│   └── validators.ts
│
├── layouts/                  # 页面布局
│   ├── default.vue
│   ├── auth.vue
│   └── blank.vue
│
├── pages/                    # 路由页面
│   ├── index.vue             # /
│   ├── about.vue             # /about
│   ├── products/
│   │   ├── index.vue         # /products
│   │   ├── [id].vue          # /products/:id(动态路由)
│   │   └── [...slug].vue     # /products/*(通配路由)
│   └── users/
│       ├── index.vue         # /users
│       └── [id]/
│           ├── index.vue     # /users/:id
│           └── settings.vue  # /users/:id/settings(嵌套路由)
│
├── middleware/                # 路由中间件
│   ├── auth.global.ts        # 全局中间件
│   └── admin.ts              # 命名中间件
│
├── plugins/                  # Nuxt 插件
│   ├── vue-plugins.ts
│   ├── analytics.client.ts   # 只在客户端执行
│   └── i18n.ts
│
├── server/                   # 服务器代码(Nitro)
│   ├── api/                  # API 路由
│   ├── routes/               # 自定义路由
│   ├── middleware/            # 服务器中间件
│   ├── plugins/              # Nitro 插件
│   └── utils/                # 服务器工具函数(自动导入)
│
├── stores/                   # Pinia stores
│   └── auth.ts
│
└── types/                    # TypeScript 类型定义
    └── index.d.ts

13.2 动态路由命名约定

文件名路由路径说明
[id].vue/:id单个动态参数
[[id]].vue/:id?可选动态参数
[...slug].vue/*通配(catch-all)路由
[[...slug]].vue/* (可选)可选通配路由
[id]-[name].vue/:id-:name多个参数混合文本

13.3 特殊文件说明

app.vue

应用根组件。如果存在 pages/ 目录,app.vue 中通常需要包含 <NuxtPage />

vue
<!-- app.vue -->
<template>
  <div>
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

error.vue

全局错误页面。当应用发生未捕获的错误时显示。

vue
<!-- error.vue -->
<script setup>
const props = defineProps(['error'])
const handleError = () => clearError({ redirect: '/' })
</script>
<template>
  <div>
    <h1>{{ error.statusCode }}</h1>
    <p>{{ error.message }}</p>
    <button @click="handleError">返回首页</button>
  </div>
</template>

app.config.ts

应用级配置,可在运行时通过 useAppConfig() 访问。与 nuxt.config.ts 的区别:nuxt.config.ts 是构建时配置(修改需要重新构建),app.config.ts 理论上可以在运行时更新。

typescript
// app.config.ts
export default defineAppConfig({
  theme: {
    primaryColor: '#3b82f6',
    dark: false
  },
  siteName: 'My App'
})

// 在组件中使用
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor)

第十四章 性能优化策略

14.1 自动代码分割

Nuxt 自动对以下内容进行代码分割:

14.2 预取策略

<NuxtLink> 组件内置了智能预取:

typescript
// NuxtLink 的预取逻辑(简化)
class NuxtLinkPrefetch {
  // 1. 使用 IntersectionObserver 监听链接是否进入视口
  observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        this.prefetchRoute(entry.target.href)
      }
    })
  })

  // 2. 预取目标页面的 JS chunk
  async prefetchRoute(url) {
    // 动态 import() 页面组件
    // 浏览器会在后台下载但不执行
  }

  // 3. 可选:预取页面的数据
  async prefetchData(url) {
    // 提前调用 API 获取数据
  }
}

14.3 图片优化

通过 @nuxt/image 模块:

vue
<NuxtImg
  src="/images/hero.jpg"
  width="800"
  height="400"
  format="webp"
  quality="80"
  loading="lazy"
  placeholder
/>

自动处理:格式转换(WebP/AVIF)、尺寸优化、懒加载、响应式图片(srcset)、低质量占位图。

14.4 字体优化

Nuxt 3 内置了 @nuxt/fonts 模块,自动处理:字体文件自托管、预加载关键字体、字体子集化(只包含页面使用的字符)、font-display: swap 防止 FOIT。

14.5 SEO 优化(useHead)

typescript
useHead({
  title: 'My Page Title',
  meta: [
    { name: 'description', content: 'Page description for SEO' },
    { property: 'og:title', content: 'My Page Title' },
    { property: 'og:image', content: 'https://example.com/image.jpg' }
  ],
  link: [
    { rel: 'canonical', href: 'https://example.com/page' }
  ],
  script: [
    { src: 'https://analytics.example.com/script.js', defer: true }
  ]
})

// 或者使用 useSeoMeta(更便捷的 SEO 元信息)
useSeoMeta({
  title: 'My Page',
  description: 'Page description',
  ogTitle: 'My Page',
  ogDescription: 'Page description',
  ogImage: 'https://example.com/image.jpg',
  twitterCard: 'summary_large_image'
})

useHead 底层使用 @unhead/vue,它会在 SSR 时将所有 head 标签序列化到 HTML 的 <head> 中,在客户端通过 DOM 操作动态更新。

14.6 渲染优化


第十五章 部署架构

15.1 部署模式对比

部署模式运行环境要求适用场景构建命令
Node.js ServerNode.js 进程需要 SSR、API 路由npx nuxi build + node .output/server/index.mjs
Static Hosting任意静态文件服务器纯静态网站、博客npx nuxi generate
ServerlessAWS Lambda 等弹性伸缩、低流量npx nuxi build(preset: aws-lambda)
EdgeCloudflare Workers 等低延迟、全球分发npx nuxi build(preset: cloudflare)
DockerDocker 运行时容器化部署npx nuxi build + Dockerfile

15.2 Node.js 部署

bash
# 构建
npx nuxi build

# 启动(生产)
PORT=3000 HOST=0.0.0.0 node .output/server/index.mjs

# 或使用 PM2
pm2 start .output/server/index.mjs --name my-nuxt-app

15.3 Docker 部署

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

15.4 环境变量

Nuxt 3 有两种环境变量:构建时环境变量(通过 nuxt.config.ts 中的 env.env 文件)和运行时环境变量(通过 runtimeConfig 配置):

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // 仅在服务端可用
    apiSecret: process.env.API_SECRET,
    dbUrl: process.env.DATABASE_URL,

    // 在客户端和服务端都可用
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || 'https://api.example.com',
      appName: 'My Nuxt App'
    }
  }
})

// 在代码中使用
const config = useRuntimeConfig()
// 服务端:可以访问 config.apiSecret 和 config.public.apiBase
// 客户端:只能访问 config.public.apiBase

环境变量覆盖规则:运行时配置可以通过环境变量覆盖,命名规则为 NUXT_ 前缀 + 大写 + 下划线分隔。例如 runtimeConfig.apiSecretNUXT_API_SECRETruntimeConfig.public.apiBaseNUXT_PUBLIC_API_BASE


第十六章 测试策略

16.1 测试工具链

Nuxt 3 官方提供 @nuxt/test-utils,集成了 Vitest:

typescript
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  // 自动设置 Nuxt 测试环境
})

16.2 测试类型

组件单元测试

typescript
// components/__tests__/Button.spec.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import Button from '~/components/ui/Button.vue'

test('Button renders correctly', async () => {
  const component = await mountSuspended(Button, {
    props: { label: 'Click me' }
  })
  expect(component.text()).toContain('Click me')
})

mountSuspended 是增强版 mount,会等待所有异步 setup(useAsyncData 等)完成后再返回。

Composable 测试

typescript
import { mockNuxtImport } from '@nuxt/test-utils/runtime'

// Mock 自动导入的 composable
mockNuxtImport('useFetch', () => {
  return () => ({
    data: ref({ items: [] }),
    pending: ref(false),
    error: ref(null)
  })
})

API 路由测试

typescript
import { $fetch, setup } from '@nuxt/test-utils/e2e'

await setup({ server: true })

test('GET /api/products', async () => {
  const data = await $fetch('/api/products')
  expect(data).toHaveProperty('items')
})

E2E 测试(结合 Playwright)

typescript
import { expect, test } from '@playwright/test'
import { setup, $fetch } from '@nuxt/test-utils/e2e'

await setup({ server: true })

test('home page renders', async ({ page }) => {
  await page.goto('/')
  await expect(page.locator('h1')).toHaveText('Welcome')
})

第十七章 高频面试题与深度解答

Q1: Nuxt 和纯 Vue SPA 有什么本质区别?

:本质区别在于 Nuxt 在 Vue 之上增加了服务端层构建编排层

  1. 渲染能力:Nuxt 支持 SSR、SSG、ISR、混合渲染,而 Vue SPA 只有客户端渲染
  2. 路由系统:Nuxt 基于文件系统自动生成路由
  3. 全栈能力:Nuxt 内置了 Nitro 服务器引擎,可以直接写 API 端点
  4. 构建优化:Nuxt 自动配置代码分割、预取、资源优化
  5. 数据获取协调useAsyncData / useFetch 自动处理 SSR-CSR 数据同步
  6. 模块生态:通过模块系统快速集成 Tailwind、Pinia、i18n 等

Q2: Nuxt 3 为什么选择 Nitro 作为服务器引擎?

:Nuxt 2 的服务器紧耦合于 Node.js + Connect/Express,无法部署到 Edge Runtime。Nitro 基于 h3(使用 Web Standards API),通过 Rollup 构建和 tree-shaking,通过 preset 系统适配不同平台。同一个 Nuxt 应用可以用不同 preset 部署到不同平台,代码不需要任何修改。

Q3: SSR 中数据是如何从服务端传递到客户端的?

:通过 Payload 机制。SSR 渲染时 useAsyncData/useFetch 的数据存入 NuxtSSRContext.payload.datauseState 存入 payload.state。渲染完成后序列化为 JSON 嵌入 HTML 的 <script> 标签中。客户端水合时读取 payload 恢复数据,避免重复请求。

Q4: 什么是水合不匹配?如何避免?

:水合不匹配发生在服务端渲染的 HTML 与客户端虚拟 DOM 不一致时。常见原因:浏览器 API(window/document)、时间/随机数、HTML 非法嵌套。解决方案:使用 <ClientOnly> 包裹、import.meta.client 条件判断、确保 HTML 语义正确。

Q5: useFetch 和 useAsyncData 应该如何选择?

useFetchuseAsyncData + $fetch 的便捷封装。调用本站 API 用 useFetch(最简洁);调用外部 API 或需要自定义逻辑用 useAsyncData + $fetch;非 HTTP 请求用 useAsyncData。两者在 SSR 数据同步方面行为一致。

Q6: Nuxt 的自动导入是怎么实现的?

:自动导入是构建时的代码转换。Nuxt 使用 unimport 扫描所有可导入来源,生成 import map,通过 Vite 插件进行 AST 分析,自动插入缺失的 import 语句。最终产物是普通的 ES module import,零运行时开销

Q7: Nuxt 模块和 Vue 插件有什么区别?

:Nuxt 模块工作在构建时,可修改配置、注册组件、添加服务器处理器等;Vue 插件工作在运行时,通过 app.use() 注册。一个 Nuxt 模块通常会注册 Vue 插件,但职责更广。

Q8: Nuxt 中间件和服务端中间件有什么不同?

:路由中间件(middleware/)在客户端导航时执行,用于页面级权限验证;服务端中间件(server/middleware/)在 Nitro 服务器上执行,对每个 HTTP 请求都执行,用于 API 认证、CORS 等。

Q9: 如何在 Nuxt 中处理认证?

:典型架构:登录 → 获取 token → HttpOnly Cookie 存储(最安全)→ 路由中间件保护客户端路由 → server middleware 验证服务端请求 → SSR 时使用 useRequestFetch() 转发 Cookie。

typescript
// composables/useAuth.ts
export const useAuth = () => {
  const user = useState('user', () => null)
  const fetch = useRequestFetch()  // 自动转发 SSR Cookie

  const { data } = await useAsyncData('user', () =>
    fetch('/api/auth/me').catch(() => null)
  )
  user.value = data.value

  return { user, isLoggedIn: computed(() => !!user.value) }
}

Q10: Nuxt 3 如何处理错误?

:多层错误处理:<NuxtErrorBoundary>(组件级)、app:error 钩子(应用级)、error.vue(全局错误页)、createError(API 错误)、useFetchonResponseError(请求错误)。

typescript
// 抛出错误
throw createError({
  statusCode: 404,
  statusMessage: 'Page Not Found',
  fatal: true
})

// 全局错误处理插件
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:error', (error) => {
    console.error('Global error:', error)
  })
})

Q11-Q16: 更多面试要点速览


第十八章 数据流全景总结

18.1 完整数据流示意图

text
                              ┌──────────────────┐
                              │   浏览器/客户端    │
                              └────────┬─────────┘
                                       │
                          HTTP 请求(首次请求 / API 调用)
                                       │
                              ┌────────▼─────────┐
                              │   Nitro Server    │
                              │   (h3 路由匹配)   │
                              └───┬──────────┬───┘
                                  │          │
                     ┌────────────▼──┐  ┌────▼────────────┐
                     │  SSR 渲染路径  │  │  API 路由路径    │
                     │               │  │                  │
                     │ 创建 Vue App  │  │ 执行 Handler    │
                     │ 执行 setup()  │  │ defineEventHandler│
                     │ useFetch →    │  │                  │
                     │ 直接调用      │  │ 返回 JSON       │
                     │ API Handler   │  │                  │
                     │ (无网络开销)  │  └────────┬────────┘
                     │               │          │
                     │ 渲染 HTML     │          │
                     │ 序列化 payload │          │
                     └───────┬───────┘          │
                             │                  │
                    HTML + Payload           JSON 响应
                             │                  │
                      ┌──────▼──────────────────▼──┐
                      │        浏览器接收            │
                      └──────┬──────────────────┬──┘
                             │                  │
                     渲染 HTML(立即可见)        │
                     下载 JS Bundle              │
                             │                  │
                      ┌──────▼──────┐           │
                      │   水合阶段   │           │
                      │ 读取 payload │           │
                      │ 恢复状态    │           │
                      │ 绑定事件    │           │
                      │ 激活响应式  │           │
                      └──────┬──────┘           │
                             │                  │
                      ┌──────▼──────┐           │
                      │  可交互状态  │           │
                      │ 后续导航:    │           │
                      │ 客户端路由   │           │
                      │ + API 请求  │───────────┘
                      └─────────────┘

18.2 核心数据流向一句话总结

服务端到客户端:数据通过 SSR 渲染时获取 → 存入 payload → 序列化到 HTML → 客户端水合时读取 payload → 恢复为响应式状态。

客户端到服务端:用户操作 → 触发事件 → $fetch/useFetch 发起 HTTP 请求 → Nitro 路由匹配 → 执行 API handler → 返回 JSON → 更新客户端状态。

服务端内部快捷路径:SSR 渲染时 useFetch('/api/xxx') → Nitro 检测到是本站 API → 直接调用 handler 函数(不经过网络层)→ 零延迟获取数据。


第十九章 高级话题

19.1 自定义渲染器

Nuxt 允许自定义 HTML 渲染过程,通过 Nitro 的 render:html 钩子:

typescript
// server/plugins/customRenderer.ts
export default defineStritroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    // html 结构:
    // {
    //   htmlAttrs: string[],      // <html> 的属性
    //   head: string[],           // <head> 内容
    //   bodyAttrs: string[],      // <body> 的属性
    //   bodyPrepend: string[],    // <body> 开头注入
    //   body: string[],           // <body> 主要内容
    //   bodyAppend: string[]      // <body> 结尾注入
    // }

    html.head.push('<meta name="custom" content="value" />')
    html.bodyAppend.push('<script>console.log("injected")</script>')
  })
})

19.2 自定义 Nitro Preset

如果要部署到不被官方支持的平台,可以创建自定义 preset:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'custom-preset',
    rollupConfig: {
      // 自定义 Rollup 配置
    }
  }
})

19.3 多层应用(Multi-App / Layers)

Nuxt 3 支持将多个 Nuxt 应用组合为一个,称为 Layers:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    './base-app',              // 本地路径
    'my-shared-layer',         // npm 包
    'github:org/repo'          // GitHub 仓库
  ]
})

Layer 可以提供:页面、组件、composables、插件、服务器处理器、配置等。所有 layer 的功能会被合并到主应用中。适用于微前端、共享基础应用、团队间代码共享。

19.4 Virtual Files(虚拟文件系统)

Nuxt 的虚拟文件系统(VFS)是构建时的关键概念。.nuxt/ 目录下的文件在开发模式下通过 Vite 的虚拟模块机制在内存中提供:

typescript
// 在代码中可以使用 # 前缀引用虚拟文件
import { useAppConfig } from '#app'
import { useRuntimeConfig } from '#imports'

这些 #app#imports 等别名指向的就是 VFS 中的虚拟模块。

19.5 DevTools 集成

Nuxt DevTools 是官方提供的开发调试工具,可以:查看路由和导航历史、检查 payload 数据、查看模块和插件列表、监控服务器 API 请求、分析组件依赖关系、查看 Vite 构建信息。


附录:关键术语速查表

术语说明
NitroNuxt 3 的服务器引擎,与平台无关
h3Nitro 底层的 HTTP 框架
NuxtApp运行时应用实例(每个请求一个,客户端全局一个)
NuxtSSRContextSSR 时的请求上下文
Payload服务端传递给客户端的序列化数据
Hydration客户端接管服务端渲染的 DOM 的过程
Route Rules路由级别的渲染策略配置
ISRIncremental Static Regeneration,增量静态再生成
SWRStale-While-Revalidate,过期缓存策略
Islands岛屿架构,部分组件客户端水合
useAsyncData核心数据获取 composable
useFetchuseAsyncData + $fetch 的便捷封装
$fetch底层 HTTP 客户端(ofetch)
navigateTo编程式导航函数
definePageMeta页面元信息编译器宏
defineNuxtModule模块定义函数
defineNuxtPlugin插件定义函数
defineEventHandler服务器 API 处理函数定义
createError创建结构化错误
useRuntimeConfig访问运行时配置
useAppConfig访问应用配置
useStateNuxt 内置的跨端状态管理
useHead管理 HTML head 标签
useCookieSSR 安全的 Cookie 读写 composable
useRequestFetch自动转发 SSR 原始请求头的 $fetch 实例
useRequestHeaders获取 SSR 原始请求的请求头
unstorageNitro 内置的统一键值存储抽象
NuxtLink增强的链接组件(预取 + 路由)
NuxtPage路由出口组件
NuxtLayout布局容器组件
presetNitro 的部署平台预设
unimport自动导入的底层库
VFSVirtual File System,虚拟文件系统
jitiTypeScript 配置文件加载器

本文档覆盖了 Nuxt 3 从架构设计到运行时数据流的核心知识。掌握这些内容后,面试中关于 Nuxt 的问题——无论是架构设计理念、SSR 原理、数据获取机制、模块系统、性能优化还是部署策略——都可以从容应对。