详解 Nuxt 4 ,快速上手使用!
一、Nuxt 4 适用前提
Nuxt 4 适合的,不只是“想写 Vue 项目”的场景,而是“希望在 Vue 之上直接获得一整套成熟应用能力”的场景。它解决的核心问题不是单纯把页面跑起来,而是把路由、数据获取、服务端渲染、服务端接口、部署形态和工程组织一起收进同一个框架里。
以下场景通常很适合选择 Nuxt 4:
- 你要做的是内容站、官网、博客、文档站、电商前台、中后台、SaaS 前端这类真正的网站或 Web 应用,而不是只做几个纯前端页面。
- 你希望默认就具备 SSR、SEO、文件路由、布局系统、数据获取、服务端接口等能力,而不是自己从 Vue + Vite 一项项拼出来。
- 你希望前后端边界更顺滑,例如前端页面和服务端 API 放在同一个仓库中协作。
- 你需要根据不同页面选择 SSR、预渲染、缓存、重定向等渲染策略。
- 你更看重“约定优于配置”的开发效率,希望团队新成员进入项目后能更快读懂结构。
以下场景则建议先评估:
- 项目只是一个非常轻量的纯前端单页应用,没有 SEO、SSR、服务端逻辑需求,用 Vue + Vite 往往更直接。
- 团队已经有成熟的纯前端架构和配套基础设施,Nuxt 带来的约定反而可能束缚既有体系。
- 项目需要极度特殊的路由、渲染或服务端组织方式,而你不想遵循 Nuxt 的目录约定。
一句话概括:Nuxt 4 不是“Vue 的脚手架”,而是 Vue 生态里的全栈应用框架。
二、Nuxt 4 简介
Nuxt 4 是构建在 Vue 3 之上的全栈框架。它把现代 Web 应用里常见但又重复的能力预先组织好了,例如:
- 文件系统路由
- 布局系统
- 自动导入
- SSR 与 CSR
- 服务端 API
- 预渲染与混合渲染
- 模块化扩展
Nuxt 4 的核心价值,在于它把这些能力整合成一个统一的开发体验。你写页面、写组件、写接口、写配置、写部署策略,都不再是彼此割裂的几套工具链,而是在同一个框架中完成。
从官方文档当前 4.x 版本可以明确确认几件很重要的事:
- 新项目要求 Node.js 20.x 或更高版本。
- Nuxt 4 默认将应用源码放在
app/目录下。 - 服务端能力由 Nitro 提供。
- 数据获取、状态共享、自动导入、路由生成等能力是框架级设计,不是后期再补的插件习惯。
Nuxt 4 的核心能力
mindmap
root((Nuxt 4))
Vue 应用层
页面路由
布局系统
组件自动导入
中间件
全栈能力
SSR
Server API
数据获取
useState
服务端引擎
Nitro
routeRules
prerender
cache
工程体验
零碎配置更少
目录约定清晰
模块生态
多种部署形态
三、Nuxt 4 与 Vue 的关系
Nuxt 经常被拿来和 Vue + Vite 一起讨论,但它们不是同一层次的工具。
| 方案 | 定位 | 优势 | 适合场景 |
|---|---|---|---|
| Vue + Vite | 前端应用基础组合 | 轻量、自由、上手快 | 纯前端 SPA、小型项目、已有成熟工程体系 |
| Nuxt 4 | Vue 全栈应用框架 | 路由、SSR、服务端、数据获取、部署策略一体化 | 官网、内容站、SaaS、需要 SEO 或 SSR 的 Vue 应用 |
如果你已经确定技术栈是 Vue,那么思考:
- 你是否只需要 Vue + Vite 这样的基础前端组合。
- 还是你已经进入“需要框架级约定和全栈能力”的阶段。
当项目出现下面这些需求时,Nuxt 的优势会非常明显:
- 需要 SEO。
- 需要 SSR 或预渲染。
- 需要文件路由。
- 需要服务端 API。
- 需要更细粒度的页面级渲染控制。
四、Nuxt 4 快速上手
这一部分先解决“怎么把项目跑起来”,同时把几个最常见的理解误区顺手讲清楚。
1. 前置准备
根据 Nuxt 4 官方文档,建议准备:
- Node.js 20.x 或更高版本,优先使用当前 LTS。
- 一个具备 Vue 语言服务支持的编辑器。
- 包管理器保持团队一致,本文以 pnpm 为例。
# 查看 Node.js 版本
node -v
# 查看 pnpm 版本
pnpm -v
如果你在 Windows 环境下感觉本地开发响应偏慢,官方文档也特别提醒了两点:
- 可考虑使用 WSL 改善 HMR 和文件监听体验。
- 浏览器访问本地开发服务时,使用
127.0.0.1:3000往往会比localhost:3000更快。
2. 创建项目
# 创建 Nuxt 4 项目
pnpm create nuxt@latest my-nuxt-app
创建完成后进入目录:
cd my-nuxt-app
Nuxt 会根据模板生成基础项目结构。和很多旧教程不同,Nuxt 4 默认不是把页面代码全放在根目录,而是默认使用 app/ 目录作为应用源码目录。
3. 启动开发服务
pnpm dev -o
默认开发地址通常是:
http://localhost:3000
启动后你会立刻感受到 Nuxt 的几个默认体验:
- 页面路由会根据目录自动生成。
- 组件、组合式函数、工具函数有不少可以自动导入。
- 页面切换和数据获取已经考虑了 SSR/CSR 之间的衔接。
4. 构建与预览
# 生产构建
pnpm build
# 本地预览构建结果
pnpm preview
和纯前端项目相比,Nuxt 的“构建结果”不只是静态资源这么简单。根据渲染模式不同,它可能包含:
- 服务器运行所需产物
- 预渲染页面
- 客户端资源
- payload 数据
因此,pnpm preview 比“只是看看页面能不能打开”更重要,它能帮助你提前发现渲染模式、资源路径和运行时配置相关的问题。
五、Nuxt 4 项目结构与目录认知
Nuxt 4 最大的学习成本,不在 API 本身,而在于你要先接受它的目录约定。目录一旦理解顺了,后面的很多能力都会自然变得清晰。
1. 一个典型的 Nuxt 4 目录结构
my-nuxt-app/
├── app/
│ ├── assets/ # 会进入构建流程的资源
│ ├── components/ # 组件
│ ├── composables/ # 组合式函数
│ ├── layouts/ # 布局
│ ├── middleware/ # 路由中间件
│ ├── pages/ # 页面路由
│ ├── plugins/ # Nuxt 插件
│ ├── utils/ # 工具函数
│ ├── app.config.ts # 应用级公开配置
│ └── app.vue # 应用根组件
├── public/ # 原样公开的静态资源
├── server/
│ ├── api/ # /api/* 接口
│ ├── middleware/ # 服务端中间件
│ ├── plugins/ # Nitro 插件
│ └── routes/ # 非 /api 前缀服务端路由
├── nuxt.config.ts # Nuxt 核心配置
├── .env # Nuxt 读取的环境变量
├── package.json
└── tsconfig.json
这里最容易理解错的,是 app/、public/、server/ 三者的边界:
-
app/放的是 Vue 应用层代码。 -
public/放的是原样对外提供的静态资源。 -
server/放的是 Nitro 服务端逻辑。
如果把这三个目录的职责混在一起,后面几乎所有问题都会开始变得难排查。
2. app/ 是 Nuxt 4 的前台应用层
Nuxt 4 默认的 srcDir 是 app/。这意味着页面、组件、布局、组合式函数等前台应用代码,默认都应该往这里放。
可以简单理解为:
-
app/pages/决定页面路由。 -
app/layouts/决定页面外壳。 -
app/components/决定可复用视图单元。 -
app/composables/决定通用逻辑复用。 -
app/plugins/决定应用级注入与初始化。
3. server/ 是 Nuxt 的服务端能力入口
Nuxt 的服务端能力不是“顺手加了个 API 目录”,而是由 Nitro 提供的正式能力。
例如:
-
server/api/hello.ts会生成/api/hello -
server/routes/health.ts会生成/health -
server/middleware/log.ts会在请求进入时执行
这意味着 Nuxt 项目天然就可以既写页面,又写服务端接口,不需要额外再搭一个独立的 Node 服务才能开始工作。
4. public/ 与 app/assets/ 的区别
这两个目录在所有前端框架里都容易让人混淆,在 Nuxt 中也一样:
-
public/中的资源不会经过构建转换,适合 favicon、robots.txt、静态下载文件这类稳定资源。 -
app/assets/中的资源会进入构建流程,更适合业务图片、样式资源、字体等。
如果一个资源你希望它保持稳定 URL,优先考虑 public/;如果你希望它参与构建优化、哈希命名、依赖分析,优先考虑 app/assets/。
六、Nuxt 4 核心配置文件介绍
Nuxt 4 的配置理解难点,不在于“配置项很多”,而在于它有几层配置分别面向不同用途。
graph LR
A[nuxt.config.ts] --> B[框架级配置]
C[app/app.config.ts] --> D[应用公开配置]
E[.env] --> F[runtimeConfig 环境变量注入]
G[tsconfig.json] --> H[类型系统与编辑器体验]
I[app/app.vue] --> J[应用根结构]
1. 文件 .env:环境变量
.env 文件本质上就是一个“给项目提供变量值”的配置文件。
mindmap
root((环境变量))
最常见的例子
接口基础地址
第三方服务密钥
站点标题
功能开关
最适合放
不能写死在代码里的值
随环境变化的值
多环境可切换的值
先看一个最简单的例子:
NUXT_API_SECRET=super-secret
NUXT_PUBLIC_API_BASE=https://api.example.com
NUXT_PUBLIC_SITE_NAME=我的 Nuxt 网站
你可以先把 .env 理解成“变量值仓库”。它只负责提供值,本身不负责告诉 Nuxt“这些值该怎么安全地在项目里使用”。这也是为什么后面还需要 runtimeConfig。
值会怎么进入项目?
最常见的链路是这样的:
flowchart LR
A[".env 文件"] -->|Nuxt 启动时把变量放到| B["process.env"]
B -->|变量配置分类| D["nuxt.config.ts<br><b>runtimeConfig</b>"]
D -->|业务代码读取| E["useRuntimeConfig"]
下面这句代码意思就是:
process.env.NUXT_API_SECRET
- 去当前运行环境里读取名为
NUXT_API_SECRET的环境变量 - 这个值通常来自
.env,也可能来自系统环境变量或部署平台配置
为什么变量名前面经常有 NUXT_PUBLIC_?
这是 Nuxt 用来区分“这个值能不能给前端看到”的重要约定。
你现在先记住最实用的一层就够了:
-
NUXT_PUBLIC_开头的,通常是准备给前端也能访问的值 - 不带
NUXT_PUBLIC_的,通常更适合服务端私有使用
2. 文件 nuxt.config.ts:Nuxt 的总开关
这是 Nuxt 项目的核心配置入口。绝大部分全局能力,都应该优先从这里理解。
先看一个比前文更完整、也更接近真实项目的示例:
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['@pinia/nuxt', '@nuxtjs/tailwindcss'],
css: ['~/assets/styles/main.scss'],
app: {
head: {
title: 'Nuxt 4 Demo',
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: 'Nuxt 4 快速上手示例项目' }
]
}
},
runtimeConfig: {
apiSecret: process.env.NUXT_API_SECRET,
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
},
routeRules: {
'/': { prerender: true },
'/blog/**': { prerender: true },
'/admin/**': { ssr: false },
'/api/**': { cache: { maxAge: 60 * 5 } }
},
vite: {
server: {
port: 3000
}
},
nitro: {
compressPublicAssets: true
}
})
mindmap
root((Config))
modules 接入生态能力
css 全局样式引入
runtimeConfig 运行时参数管理
routeRules 路由渲染与缓存
vite 前端构建配置
nitro 服务端引擎配置
(1)modules:接入 Nuxt 模块生态
它的作用很直接:给 Nuxt 安装并启用框架级能力扩展。
可以把它理解成“Nuxt 官方推荐的扩展入口”
- 作用:一键集成第三方扩展包 / 插件,自动为项目注入功能、简化配置、拓展 Nuxt 核心能力,不用你手动写繁琐的初始化、注册、兼容代码。
- 举例说明:
-
@pinia/nuxt自动在 Nuxt 中注册 Pinia,无需手动写createPinia()+app.use();直接在项目任意组件 / 页面中使用useStore,无需重复引入。 -
@nuxtjs/tailwindcss自动配置 Tailwind 依赖、PostCSS、样式注入;自动识别项目中的 Tailwind 类名,无需手动创建tailwind.config.js基础配置;
-
(2)css:注册全局样式
这里注册的是整个应用都会生效的全局样式文件。
如果只是某个组件自己的样式,依然优先放回组件内部;css 更适合“全项目共享”的样式入口。
(4)app:全局基础配置
它管的是整个网站通用、所有页面都生效的设置,不是某一个页面,是全站统一的规则。
这里只写了head,对应网页源码里的 <head> 标签,全站统一配置网页头部,不用每个页面单独写。
(3)runtimeConfig:管理运行时配置与环境变量
这项配置是 Nuxt 里“怎么安全、统一地读取环境配置”的标准入口;public 里的给前后端共用,外面的只留给服务端。专门存放不能写死在代码里、会随环境变化、敏感保密的配置(比如接口密钥、接口地址),运行时自动加载,不用改代码。
很多新手困惑:不是已经有 .env 了吗,为什么还要多这一层?
这里最容易踩的坑是:把敏感信息放进 public,或者误以为 .env 本身就是配置系统。
最简单的理解是:.env:负责“提供原始变量值”,runtimeConfig:负责“把变量按 Nuxt 的规则组织起来,供项目读取”;
也就是说,你可以把 .env 里的值先交给 runtimeConfig,然后在项目里统一通过 useRuntimeConfig() 去读取,而不是到处直接写 process.env.xxx。
先看一个最常见、也最实用的例子:
NUXT_API_SECRET=super-secret
NUXT_PUBLIC_API_BASE=https://api.example.com
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: process.env.NUXT_API_SECRET,
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
}
})
这段配置的意思其实很简单:
-
apiSecret是服务端私有配置,例如第三方服务密钥。 -
public.apiBase是公开配置,例如前端请求接口时要用的基础地址。
在代码里这样读取:
const config = useRuntimeConfig()
console.log(config.public.apiBase)
如果是在服务端代码里,还可以读取私有配置:
export default defineEventHandler(() => {
const config = useRuntimeConfig()
return {
apiBase: config.public.apiBase,
hasSecret: Boolean(config.apiSecret)
}
})
graph LR
A[".env"] --> C["nuxt.config.ts<br><b>runtimeConfig</b>"]
C --> D["public"]
C --> E["private"]
D --> F["前端可读"]
D --> G["服务端也可读"]
D --> H["例子:api基础网址 / 网站标题"]
E --> I["仅服务端可读"]
E --> J["浏览器不可见"]
E --> K["例子:api密码 / token / 数据库配置"]
(4)routeRules:控制页面级渲染与缓存策略
这项配置让你可以按路由粒度决定页面或接口的行为,给网站里不同的网址路径,单独设置「怎么渲染、要不要缓存、能不能访问」的规则,不用全站统一设置,精准优化每个页面。
上面这个例子表达的意思分别是:
- 首页构建时预渲染
- 博客文章页也走预渲染
- 后台页面关闭 SSR,走客户端渲染
- 接口增加短时缓存
如果你在学 Nuxt 时只记住一个“和部署形态高度相关”的配置,那通常就是它。
(5)vite:向底层 Vite 传递配置
Nuxt 底层使用 Vite 作为开发和构建能力的一部分,因此当你确实需要改 Vite 行为时,可以从这里传配置。
常见场景包括:
- 调整开发端口
- 配置样式预处理器
- 传递一部分 Vite 构建或开发参数
更稳妥的原则是:
- 先找 Nuxt 自己有没有对应配置。
- 只有 Nuxt 层没有、而底层 Vite 层确实需要改时,再用
vite。
(6)nitro:向服务端引擎传递配置
nitro 是面向服务端引擎这一层的配置入口,常见用途包括:
- 调整服务端输出行为
- 配置压缩、缓存、部署相关细节
- 对 Nitro 运行时做更底层的控制
它和 vite 的区别可以直接这样记:
-
vite偏前端构建与开发链路 -
nitro偏服务端运行与输出链路
对于刚接触 Nuxt 的读者来说,不需要一开始就深入 nitro 的所有细节,但至少要知道:Nuxt 不是只有 Vue 应用层配置,它还有服务端这一层。
如果想把这一节收束成最实用的判断原则,可以记住:
- 和 Nuxt 整体行为相关的,先看
nuxt.config.ts。 - 和应用运行中要读到的公开配置相关的,再看
app.config.ts或runtimeConfig。
3. app/app.config.ts:公开且偏静态的应用配置
Nuxt 官方文档明确区分了 runtimeConfig 和 app.config。如果配置是:
- 公开的,不敏感的
- 更偏应用展示层
- 更适合在构建期确定
那么它通常更适合放在 app/app.config.ts 里。
示例:
export default defineAppConfig({
siteName: 'Nuxt 4 Demo',
theme: {
primaryColor: '#0ea5e9'
}
})
在代码中读取:
const appConfig = useAppConfig()
如果你拿不准该放哪,可以这样判断:
- 和密钥、环境变量、部署环境相关的,先想
runtimeConfig。 - 和站点标题、主题、公开开关相关的,先想
app.config。
七、Nuxt 4 核心能力实操
这一部分不追求列全,而是优先讲 Nuxt 最有代表性的能力。
1. 文件系统路由
Nuxt 的页面路由来自 app/pages/ 目录。例如:
app/pages/
├── index.vue
├── about.vue
└── posts/
└── [id].vue
它大致会生成这样的路由:
graph TD
A["app/pages/index.vue"] --> B["/"]
C["app/pages/about.vue"] --> D["/about"]
E["app/pages/posts/[id].vue"] --> F["/posts/:id"]
这套规则的价值不只是“少写路由表”,而是让页面结构和 URL 结构天然对齐,项目越大越能体会到这种可读性。
2. 布局系统
布局放在 app/layouts/ 中,适合承载:
- 顶部导航
- 侧边栏
- 页脚
- 页面公共壳层
然后在 app.vue 中配合 <NuxtLayout> 使用。
如果某个页面需要特殊布局,也可以在页面中通过 definePageMeta 指定。这样做比“在每个页面里重复写头尾结构”更清晰,也更符合 Nuxt 的组织方式。
3. 自动导入
Nuxt 的自动导入是它最能提升开发手感的能力之一。根据官方文档,以下目录默认就有明显的自动导入能力:
app/components/app/composables/app/utils/
这意味着很多时候你不需要手动 import:
<script setup lang="ts">
const count = useState('count', () => 0)
const doubled = computed(() => count.value * 2)
</script>
如果你更希望显式导入,Nuxt 也提供了 #imports 别名。
自动导入的好处很明显,但也要保持清醒:
- 它提升了开发效率。
- 但也会让“这个函数到底来自哪里”变得没那么直观。
所以团队协作里,通常建议对公共逻辑命名保持克制,不要让自动导入把语义搞得太散。
4. 数据获取
这一节是 Nuxt 最容易“看起来会用、实际上没用明白”的部分。因为在普通 Vue 项目里,大家很容易形成一种习惯:哪里要数据,就直接 fetch 一下。
但在 Nuxt 里,这样想往往不够,因为页面首屏数据获取要同时考虑:
- 服务端渲染
- payload 传递
- hydration 复用
- 避免客户端再次请求同一份数据
-
$fetch是基础能力,本质上是 Nuxt 中一个很强的同构请求工具,底层来自ofetch。它能在服务端和客户端两边工作。你可以把它理解成:-
适合在服务端路由、插件、事件处理函数里直接使用
- 但它本身不负责把 SSR 数据自动安全地传给客户端复用
-
-
useAsyncData是核心的 SSR 友好数据获取,是 Nuxt 数据获取体系里更底层、也更核心的组合式函数。它做的事情不是“帮你请求”,而是:-
包裹一个异步函数,在服务端执行它
-
把结果放进 Nuxt 的数据传递链路
-
在客户端 hydration 时复用这份结果,防止“二次获取”
它有一个非常重要的点:需要一个唯一的
key来去重。 -
-
useFetch是最常用的便利层,可以理解成:useAsyncData+$fetch的常用封装。当在页面或组件里“从某个 API 地址拿数据”时,它通常就是最自然的首选。它之所以常用,是因为:
-
写法短
-
自带
pending、error、refresh -
会根据 URL 和选项自动生成 key
如果你只是想在页面里拿
/api/products这种接口数据,先用useFetch,通常就是最对路的选择。 -
SSR 友好的数据获取:Nuxt 怎么防止二次获取
Nuxt 通过“水合” (hydration) 过程来解决二次获取问题:数据在服务器上获取,页面被渲染成 HTML,获取到的数据被序列化并嵌入到 HTML 载荷(payload)中。在客户端,Nuxt 读取这个载荷并“水合”应用状态,从而避免了重新请求相同的数据。
flowchart LR
A["服务端执行<br>useFetch/useAsyncData<br>获取数据"] --> C["渲染 HTML"]
A --> D["写入 payload"]
C --> E["返回浏览器"]
D --> E
E --> F["浏览器 hydration"]
F --> G["复用 payload"]
这就是为什么 Nuxt 官方文档一直强调:页面初始化数据,不要在 <script setup> 里直接裸用 $fetch,否则很可能服务端请求一次,客户端 hydration 时又请求一次。
所以这一句一定要记住:$fetch 能请求数据,但不自动帮你解决 SSR 首屏复用;useFetch 和 useAsyncData 才会把数据接入 Nuxt 的 SSR 链路。
如果你想快速做判断,可以直接按这个规则记:
- 页面首屏数据、希望 SSR 参与并防止二次获取:优先
useFetch - 页面首屏需要执行更灵活的异步逻辑:优先
useAsyncData - 点击按钮、提交表单、手动触发请求:直接用
$fetch - 页面初始化阶段,不要直接裸用
$fetch -
useAsyncData的key一定要唯一
代码片段:最常见的 useFetch
<script setup lang="ts">
const { data: products, pending, error, refresh } = await useFetch('/api/products')
</script>
<template>
<div v-if="pending">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<ul v-else>
<li v-for="product in products" :key="product.id">{{ product.name }}</li>
</ul>
<button @click="refresh">刷新数据</button>
</template>
这个例子很典型,因为它基本覆盖了页面首屏取数最常见的需求:
-
data:取到的数据 -
pending:加载状态 -
error:错误状态 -
refresh:主动刷新
代码片段:useAsyncData 与唯一 key
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(
`post-${route.params.slug}`,
() => $fetch(`/api/posts/${route.params.slug}`)
)
</script>
这段代码里最重要的不是 $fetch,而是前面的这个 key:
`post-${route.params.slug}`
虽然 useFetch 往往会自动生成 key,但对 useAsyncData,或者对 useFetch 那些 URL 本身不够独特的场景,手动提供清晰且唯一的 key 非常重要。
5. 状态共享
useState 本质上也是响应式状态,但它比普通 ref 多了一层 Nuxt 的 SSR 友好能力。可以把它理解成:
- 它像
ref,会在使用相同 key 的地方共享状态 - 但它会参与 Nuxt 的服务端到客户端状态传递
- 它能在 hydration 时复用服务端已经生成好的状态,解决“水合不匹配”
所以它解决的根本问题,不只是“共享”,而是:让客户端以和服务端渲染时完全一致的初始状态启动。
如果没有这层机制,就很容易出现“服务端渲染出的是 A,客户端接管时算出来的是 B”,最终引发 hydration 不匹配。
最佳实践:把 useState 包装成组合式函数
而是把它包进一个组合式函数里。这样做的好处是:key 集中不易乱、类型更稳定、更容易复用
// app/composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0)
// 创建一个名为 `counter` 的共享状态,用 `0` 作为初始值
// 之后其他地方只要使用同样的 key,就会拿到同一份状态
然后在组件里这样使用:
<!-- app/components/TheCounter.vue -->
<script setup lang="ts">
const counter = useCounter()
</script>
<template>
<div>
<p>计数器: {{ counter }}</p>
<button @click="counter++">+</button>
</div>
</template>
代码注意事项
-
不要在文件顶层直接用 ref 做全局状态
这一点非常重要,尤其是对刚从普通 Vue 项目切过来的同学。下面这种写法在 Nuxt 的通用渲染应用里是有风险的:
const counter = ref(0)如果它出现在文件顶层作用域,就可能变成服务端进程里的单例状态。这样一来,不同用户请求之间就有机会共享同一份状态,严重时甚至可能导致数据泄露。
-
useState 里的值要可序列化
因为
useState的值需要从服务端传到客户端,所以它本质上也会进入序列化流程。这意味着你放进去的数据最好是可序列化的,例如:字符串、数字、布尔值、数组、普通对象
不推荐直接放进去的有:函数、类实例、带复杂原型链的对象
可以简单理解成:能安全“打包后再还原”的数据,更适合放进
useState。
扩展:何时以及为何集成 Pinia
useState 非常适合处理简单到中等复杂度的状态。对于复杂的全局状态管理,特别是当需要 actions、getters 以及 Vue DevTools 的时间旅行调试等高级功能时,那么官方推荐的方向就是 Pinia。Nuxt 也提供了专门的 Pinia 模块来帮你处理 SSR 集成。
你可以直接把两者理解成:
-
useState:轻量、直接、SSR 友好,适合大多数简单共享状态 -
Pinia:更完整的状态管理方案,适合复杂全局状态
6. 服务端 API
Nuxt 项目可以直接在 server/ 目录中写接口,通过文件命名约定来处理不同的 HTTP 方法。
-
基本的 GET 端点:
// server/api/hello.get.ts export default defineEventHandler(() => { return {message: 'hello from server'} })页面中直接调用:
<script setup lang="ts"> const { data } = await useFetch('/api/hello') </script> -
基本的 POST 端点:
// server/api/users.post.ts export default defineEventHandler(async (event) => { const body = await readBody(event); // 创建新用户的逻辑... console.log('新用户:', body); setResponseStatus(event, 201); // 设置 HTTP 状态码 return { success: true, user: body }; });
这套体验的价值在于:
- 前端页面和接口距离更近。
- 本地联调成本更低。
- 对中小型项目尤其友好。
很多人会说 “Nitro 就是 Nuxt 的服务端引擎”,这句话没错,但还不够完整。更准确地说,Nitro 是 Nuxt 的服务端运行时,负责接收请求、执行服务端逻辑、处理接口、参与 SSR 渲染、应用 routeRules,并生成可部署的服务端运行结果。
它主要承担这些核心能力:server/api/ 与 server/routes/ 路由处理、server/middleware/ 中间件、server/plugins/ 插件、SSR 服务端渲染执行、缓存、预渲染、重定向等路由规则,以及最终 .output/server 运行时产物的构建。
可以简单理解为:Nuxt 负责应用框架层面,Nitro 负责服务端执行层面,Nuxt 将 Vue 应用层与 Nitro 服务端层整合在了同一套工作流中。
Nitro 其一大核心优势便是同构 fetch 优化。在 SSR 渲染阶段调用内部 API 路由(如 useFetch('/api/hello'))时,Nuxt 并不会发起真实的 HTTP 网络请求,而是直接在当前进程内调用对应的事件处理函数。
这种机制彻底消除了网络开销与延迟,带来了显著的性能提升,这是传统前后端分离部署难以实现的。因此,在 Nuxt 中将 API 与前端代码同构存放,不只是开发上的便捷性,更是为了在服务端渲染阶段获得实打实的性能增益,也是 Nuxt 全栈方案的核心竞争力之一。
7. 服务端中间件
如果你需要在所有请求进入前做日志、鉴权上下文注入等处理,还可以使用 server/middleware/。
// server/middleware/logger.ts
export default defineEventHandler((event) => {
console.log(`[${event.method}] 新请求: ${getRequestURL(event).pathname}`);
});
代码注意事项:服务器中间件不应返回值或结束响应,其职责是修改 event 上下文或执行副作用操作。若返回值会导致请求短路,使其无法到达目标处理器。
8. 客户端路由中间件
对新手而言,服务器中间件(server/middleware/)和路由中间件(app/middleware/)的区别很容易混淆。
服务器中间件运行在 Nitro 服务端层面,处理原始 HTTP 请求,对所有请求生效,包括 API 与静态资源请求。
路由中间件则基于 Vue 和 vue-router 运行,在客户端或服务端页面导航时执行,不会作用于直接的 API 调用。
例如,路由中间件一个简单的登录保护:
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware(() => {
const user = useState('user')
if (!user.value) {
return navigateTo('/login')
}
})
页面中使用:
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
</script>
9. Head 管理基础
在 Nuxt 里,页面 <head> 相关内容不是后期随便拼接的,而是框架级能力。
从使用角度看,可以先分成两层:
-
全局层:例如
nuxt.config.ts里的app.head,适合放全站通用的标题模板、基础 meta、favicon。 -
页面层:例如在页面或组件里使用
useHead/useSeoMeta,适合根据当前页面数据动态设置标题和描述。
可以简单理解成:
- 全局层解决“整个站点默认长什么样”
- 页面层解决“当前这个页面要展示什么头部信息”
如果只写全局 app.head,当然能让站点具备基础头部信息;但如果你要真正做好 SEO,尤其是商品页、文章页、详情页这种“每页内容都不同”的页面,就必须进入页面级动态管理。
10. SEO 与元数据管理
Nuxt 的 SSR 基础天然对 SEO 友好,因为搜索引擎拿到的不是一个空壳 HTML,而是已经包含页面内容的首屏结构。
但 SSR 只是基础,真正把 SEO 做细,还需要把标题、描述、Open Graph、Twitter Card 这些元数据管起来。
这里最值得先掌握的三个工具是:
useHeaduseSeoMetauseHeadSafe
useHead:通用的 Head 管理工具
useHead 是最基础、也最通用的组合式函数。只要是合法的 head 标签内容,它基本都能管理,例如:title、meta、link、script、htmlAttrs、bodyAttrs
比如:
<script setup lang="ts">
useHead({
title: '商品详情页',
meta: [
{ name: 'description', content: '这是商品详情页' }
]
})
</script>
你可以把它理解成:
- 能力最全
- 适合需要自己精细控制 head 结构时使用
- 但写法也相对更底层、更自由
useSeoMeta:更推荐的 SEO 写法
如果你的目标主要是 SEO,而不是任意 head 标签管理,那么更推荐优先使用 useSeoMeta。
它的特点是:
- 更贴近 SEO 场景
- 类型更清晰
- 能减少常见拼写错误
例如你不用自己纠结:
- 这里应该写
name - 还是应该写
property
而是直接写更语义化的字段:title、description、ogTitle、ogDescription、ogImage、twitterCard
所以从教程角度,可以直接给一个很实用的结论:
- 想通用控制 head:用
useHead - 想专门做 SEO 元数据:优先
useSeoMeta
动态 SEO:产品页是最典型的场景
Nuxt 真正体现优势的地方,不是“能写一个静态 title”,而是可以结合页面数据,动态生成每个页面自己的 SEO 信息。
例如商品详情页:
<script setup lang="ts">
const { data: product } = await useFetch('/api/products/some-product')
useSeoMeta({
title: () => `${product.value?.name} - 我的商店`,
description: () => product.value?.description,
ogTitle: () => `${product.value?.name} - 我的商店`,
ogDescription: () => product.value?.description,
ogImage: () => product.value?.imageUrl,
twitterCard: 'summary_large_image'
})
</script>
这段代码最重要的不是 API 写法,而是它体现出的思路:
- 页面数据先通过
useFetch获取 - SEO 标签再根据这份数据动态生成
- 因为 Nuxt 支持 SSR,所以搜索引擎拿到的首屏里就已经包含这些元数据
这正是 Nuxt 对 SEO 友好的关键原因之一。
什么时候要用 useHeadSafe
如果你处理的是用户生成内容,或者来源不完全可控的数据,例如:
- 用户输入的标题
- CMS 后台可编辑的描述
- 外部系统返回的富文本摘要
那么在把这些内容放进 head 时,要特别注意安全问题。
这时更适合使用 useHeadSafe,因为它会对输入内容做更安全的处理,避免把危险属性或值直接渲染进页面头部,从而降低 XSS 风险。
可以简单理解成:
- 普通可控内容:
useHead/useSeoMeta - 用户生成或不完全可信内容:优先考虑
useHeadSafe
实战里的最实用建议
如果你希望这部分先能落地,而不是一下子记一堆 API,可以先记住下面几条:
- 全站默认头部放
app.head - 页面级动态 SEO 优先用
useSeoMeta - 需要更细粒度 head 控制时再用
useHead - 页面 SEO 最好绑定真实页面数据,而不是写死模板文本
- 用户生成内容进入 head 时,优先考虑
useHeadSafe
八、Nuxt 4 渲染模式与部署思路
Nuxt 真正拉开和普通前端项目差距的地方,就在这里。你不只是“构建一个站点”,而是在决定每条路由应该怎样被渲染。
1. 默认并不只是 SPA
Nuxt 的默认优势之一,就是它天然适合 SSR。你不用先把 Vue 项目搭起来,再额外拼接 SSR 方案。
但 Nuxt 也不只支持 SSR,它可以在同一个项目里组合多种策略:
- SSR(Server-Side Rendering,服务端渲染)
- CSR(Client-Side Rendering,客户端渲染)
- 预渲染
- 混合渲染
- Edge 部署
2. routeRules 是理解 Nuxt 渲染能力的关键
Nuxt 官方文档明确指出,Nitro 的 routeRules 可以对不同路径设置不同规则。
示例:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/blog/**': { prerender: true },
'/api/**': { cache: { maxAge: 60 * 60 } },
'/old-page': {
redirect: {
to: '/new-page',
statusCode: 302
}
}
}
})
这意味着同一个 Nuxt 项目里,你完全可以让:
- 首页在构建时预渲染
- 博客页按规则预渲染
- 某些接口带缓存
- 某些旧地址自动重定向
这就是 Nuxt 的“混合渲染”思路,它比“整个站点只有 SSR 或只有 SPA”灵活得多。
3. 预渲染不只是“导出静态 HTML”
根据官方文档,Nuxt 在预渲染时还会生成 _payload.json,其中包含 useAsyncData 和 useFetch 产生的序列化数据。客户端导航时可以直接读取这些 payload,而不是重复请求。
这也是为什么前面一直强调:
- 数据获取方式和渲染模式是连在一起的。
- 不能把 Nuxt 中的数据获取简单当作普通前端里的异步请求。
九、Nuxt 4 核心架构与工作流程
这一章不再按“功能清单”来讲,而是按 Nuxt 真正的工作方式来拆。重点是把 Nuxt 的生成层、运行层、服务端层、客户端接管层,以及这些层之间怎么衔接讲清楚,尤其补清楚两个经常被讲虚的点:Nuxt 到底扫描了什么,以及应用真正的入口链路是什么。
1. Nuxt 到底是什么架构,不只是“Vue + SSR”
一个比较完整、能拿分的回答应该是:
Nuxt 不是简单把 Vue 套上 SSR,而是把 Vue 应用层、服务端引擎、构建生成层和部署产物层组织成统一工作流的全栈框架。
它至少可以拆成 4 层:
-
app/:Vue 应用层,放页面、布局、组件、composables、插件 -
server/:Nitro 服务端层,放 API、server middleware、server plugins、routes -
.nuxt/:生成层,把约定式源码整理成可运行的应用骨架 -
.output/:部署层,生产环境真正运行的产物
graph TD
A["源码"] --> B["app/"]
A --> C["server/"]
A --> D["nuxt.config.ts"]
B --> E["Vue 应用层"]
C --> F["Nitro 服务端层"]
D --> G["全局配置层"]
E --> H["Nuxt 生成层 .nuxt"]
F --> H
G --> H
H --> I["开发运行"]
H --> J["生产构建"]
J --> K["部署产物 .output"]
2. Nuxt 到底扫描了什么:扫描对象、规则和结果
很多教程会说“Nuxt 会扫描目录”,但如果不继续说清楚“扫描什么、按什么规则扫描、扫描后拿这些结果做什么”,这一句其实帮助不大。
Nuxt 扫描的不是整个项目里所有文件,而是被框架约定为有特殊语义的目录和文件。最重要的几类包括:
-
app/pages/:扫描后生成页面路由 -
app/layouts/:扫描后生成布局映射 -
app/middleware/:扫描后生成客户端路由中间件映射 -
app/plugins/:扫描并自动注册 Nuxt 插件 -
app/components/:扫描后生成组件自动导入能力 -
app/composables/、app/utils/:扫描后生成自动导入声明 -
server/api/:扫描后生成/api/*服务端路由 -
server/routes/:扫描后生成普通服务端路由 -
server/middleware/:扫描后挂载到 Nitro 请求链路 -
server/plugins/:扫描后在 Nitro 启动时执行
这里最关键的细节是:不同目录的扫描规则并不完全一样。
例如 app/plugins/ 并不是“递归扫描一切文件并全部注册”。官方文档明确说明:
- 默认自动注册的是顶层插件文件
- 子目录下的
index文件目前也会被扫描,但这种形式已经不推荐长期依赖 - 也就是说,自动注册是有边界和规则的,不是无差别递归
所以更准确地说,Nuxt 的扫描是:
- 以目录约定为边界
- 以文件位置和类型为规则
- 以生成
.nuxt中间结果为目的
flowchart LR
A["app/ + server/ + nuxt.config.ts"] --> B["Nuxt 扫描器"]
B --> C["路由记录"]
B --> D["布局与中间件映射"]
B --> E["插件注册表"]
B --> F["自动导入声明"]
B --> G["Nitro 路由与处理器"]
C --> H["写入 .nuxt"]
D --> H
E --> H
F --> H
G --> H
3. 应用真正的入口是什么
“Nuxt 的入口是什么”这个问题特别容易回答得似是而非。
从应用视图树的角度看,app/app.vue 是 Nuxt 应用的根组件入口。官方文档也明确把 app.vue 定义为 Nuxt application 的 main component。
但这里一定要分清两个层次:
视图根入口:app.vue
app.vue 决定的是:
- 整个应用最外层长什么样
- 布局体系如何包裹页面
- 当前页面最终渲染到哪里
最常见的写法是:
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
这里三者职责分别是:
-
app.vue:应用根组件 -
<NuxtLayout>:当前页面外面套哪层布局 -
<NuxtPage>:当前路由对应的页面组件渲染到哪里
这也解释了一个很容易忽略的点:
-
app/pages/只是被扫描并生成路由记录 - 真正把当前页面显示出来的,是
app.vue里的<NuxtPage />
框架运行入口:.nuxt 里的生成入口
Nuxt 框架当然不是“从你手写的 app.vue 文件直接启动”的。更准确的说法是:
- Nuxt 先生成自己的运行入口
- 这个入口会把
app.vue接成根组件 - 然后再由 Nuxt / Vue 创建应用实例
所以要把这两层区分开:
-
app.vue是应用视图树入口 -
.nuxt里的生成入口是框架真正的运行入口
4. 执行 pnpm dev 后,Nuxt 内部到底发生了什么
这题不要回答成“就启动开发服务器”。更完整的回答是:
- 读取
nuxt.config.ts - 扫描
app/和server/ - 生成
.nuxt与类型 - 启动开发服务器
- 等待请求进入
- 请求进入后,由 Nitro 和 Nuxt 一起处理 SSR/CSR 链路
更直观的流程可以看这张图:
flowchart TD
subgraph 启动阶段
direction LR
A["执行 pnpm dev"] --> B["读取<br>nuxt.config.ts"]
B --> C["扫描 app/ 和 server/"]
C --> D["生成 .nuxt 与类型"]
D --> E["启动 Nuxt Dev Server"]
end
subgraph 请求阶段
direction LR
F["浏览器请求页面"] --> G["Nitro<br>接收请求"]
G --> H["创建<br>Nuxt实例<br>Vue 实例"]
H --> I["执行<br>app<br>plugins<br>服务端相关逻辑"]
I --> J["执行<br>页面校验<br>app middleware"]
J --> K["渲染页面组件"]
K --> L["执行<br>useFetch<br>useAsyncData"]
L --> M["生成<br>HTML<br>payload"]
end
启动阶段 --> 请求阶段
请求阶段 --> N["返回内容到浏览器"]
N--> O["浏览器 hydration"]
这里最值得抓住的两个点通常是:
- Nuxt 开发时不是直接跑源码,而是先生成
.nuxt - 请求进来不是直接进 Vue 组件,而是先过 Nitro,再进入 Nuxt 应用渲染链路
5. 一次页面请求进来以后,Nuxt 内部的执行顺序
按官方 Lifecycle 文档,可以整理成更适合理解框架执行链路的顺序:
服务启动时执行的部分
- Nitro 启动
- 执行
server/plugins/ - 注册服务端钩子和运行时扩展
这里容易被问到一个细节:
-
server/plugins/更接近服务端启动初始化 - 不是每个页面请求都重新执行一次的页面逻辑
每个请求都会走的部分
- 请求进入 Nitro
- 执行
server/middleware/ - 创建 Nuxt 与 Vue 应用实例
- 执行
app/plugins/ - 执行页面
validate - 执行
app/middleware/ - 匹配布局、页面与组件树
- 执行
useFetch/useAsyncData - 生成 HTML
- 把 HTML、payload、资源信息返回浏览器
这套顺序非常值得记,因为它能帮你回答很多追问:
-
server/middleware/和app/middleware/有什么区别 - 为什么插件比页面先执行
- 为什么
useFetch能参与 SSR
6. .nuxt 是什么:先生成再运行
官方文档明确说明,Nuxt 会生成 .nuxt/ 目录,而 nuxt prepare 也会专门创建 .nuxt 并生成类型。
这说明 .nuxt 不是无意义缓存,而是 Nuxt 的中间生成层。
你可以这样答:
.nuxt 是 Nuxt 根据 app/、server/、nuxt.config.ts 等约定式源码,自动生成出来的可运行应用骨架。
它通常承载这些东西:
- 路由生成结果
- 自动导入声明
- 类型文件
- 插件注册结果
- 应用运行入口
flowchart LR
A["app/<br>server/<br>nuxt.config.ts"] --> B["Nuxt 扫描"]
B --> C["生成 .nuxt"]
C --> D["路由定义"]
C --> E["自动导入声明"]
C --> F["类型文件"]
C --> G["插件注册和运行入口"]
换成更底层的理解方式,可以直接这样记:
- 因为 Nuxt 先扫描目录约定
- 再把结果生成进
.nuxt - 最后运行的其实是生成后的应用结构
7. .nuxt 和 .output 有什么区别
这题很经典,因为它能测出你有没有真正理解生成层和部署层。
标准区分方式是:
-
.nuxt:开发期 / 生成期的中间结果 -
.output:生产构建后真正部署和运行的最终结果
flowchart LR
A["源码与配置"] --> B["nuxt build"]
B --> C["生成客户端资源"]
B --> D["生成 Nitro 服务端产物"]
B --> E["应用 routeRules / prerender"]
C --> F[".output"]
D --> F
E --> F
所以:
-
.nuxt更像“运行前整理好的应用骨架” -
.output更像“真正拿去上线部署的产物”
官方部署文档也直接给出运行方式,例如:
node .output/server/index.mjs
这说明生产环境真正跑的不是源码目录,而是 .output。