扫描王OCR
44M · 2026-04-08
Nuxt 4 适合的,不只是“想写 Vue 项目”的场景,而是“希望在 Vue 之上直接获得一整套成熟应用能力”的场景。它解决的核心问题不是单纯把页面跑起来,而是把路由、数据获取、服务端渲染、服务端接口、部署形态和工程组织一起收进同一个框架里。
以下场景通常很适合选择 Nuxt 4:
以下场景则建议先评估:
一句话概括:Nuxt 4 不是“Vue 的脚手架”,而是 Vue 生态里的全栈应用框架。
Nuxt 4 是构建在 Vue 3 之上的全栈框架。它把现代 Web 应用里常见但又重复的能力预先组织好了,例如:
Nuxt 4 的核心价值,在于它把这些能力整合成一个统一的开发体验。你写页面、写组件、写接口、写配置、写部署策略,都不再是彼此割裂的几套工具链,而是在同一个框架中完成。
从官方文档当前 4.x 版本可以明确确认几件很重要的事:
app/ 目录下。mindmap
root((Nuxt 4))
Vue 应用层
页面路由
布局系统
组件自动导入
中间件
全栈能力
SSR
Server API
数据获取
useState
服务端引擎
Nitro
routeRules
prerender
cache
工程体验
零碎配置更少
目录约定清晰
模块生态
多种部署形态
Nuxt 经常被拿来和 Vue + Vite 一起讨论,但它们不是同一层次的工具。
| 方案 | 定位 | 优势 | 适合场景 |
|---|---|---|---|
| Vue + Vite | 前端应用基础组合 | 轻量、自由、上手快 | 纯前端 SPA、小型项目、已有成熟工程体系 |
| Nuxt 4 | Vue 全栈应用框架 | 路由、SSR、服务端、数据获取、部署策略一体化 | 官网、内容站、SaaS、需要 SEO 或 SSR 的 Vue 应用 |
如果你已经确定技术栈是 Vue,那么思考:
当项目出现下面这些需求时,Nuxt 的优势会非常明显:
这一部分先解决“怎么把项目跑起来”,同时把几个最常见的理解误区顺手讲清楚。
根据 Nuxt 4 官方文档,建议准备:
# 查看 Node.js 版本
node -v
# 查看 pnpm 版本
pnpm -v
如果你在 Windows 环境下感觉本地开发响应偏慢,官方文档也特别提醒了两点:
127.0.0.1:3000 往往会比 localhost:3000 更快。# 创建 Nuxt 4 项目
pnpm create nuxt@latest my-nuxt-app
创建完成后进入目录:
cd my-nuxt-app
Nuxt 会根据模板生成基础项目结构。和很多旧教程不同,Nuxt 4 默认不是把页面代码全放在根目录,而是默认使用 app/ 目录作为应用源码目录。
pnpm dev -o
默认开发地址通常是:
启动后你会立刻感受到 Nuxt 的几个默认体验:
# 生产构建
pnpm build
# 本地预览构建结果
pnpm preview
和纯前端项目相比,Nuxt 的“构建结果”不只是静态资源这么简单。根据渲染模式不同,它可能包含:
因此,pnpm preview 比“只是看看页面能不能打开”更重要,它能帮助你提前发现渲染模式、资源路径和运行时配置相关的问题。
Nuxt 4 最大的学习成本,不在 API 本身,而在于你要先接受它的目录约定。目录一旦理解顺了,后面的很多能力都会自然变得清晰。
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 服务端逻辑。如果把这三个目录的职责混在一起,后面几乎所有问题都会开始变得难排查。
Nuxt 4 默认的 srcDir 是 app/。这意味着页面、组件、布局、组合式函数等前台应用代码,默认都应该往这里放。
可以简单理解为:
app/pages/ 决定页面路由。app/layouts/ 决定页面外壳。app/components/ 决定可复用视图单元。app/composables/ 决定通用逻辑复用。app/plugins/ 决定应用级注入与初始化。Nuxt 的服务端能力不是“顺手加了个 API 目录”,而是由 Nitro 提供的正式能力。
例如:
server/api/hello.ts 会生成 /api/helloserver/routes/health.ts 会生成 /healthserver/middleware/log.ts 会在请求进入时执行这意味着 Nuxt 项目天然就可以既写页面,又写服务端接口,不需要额外再搭一个独立的 Node 服务才能开始工作。
这两个目录在所有前端框架里都容易让人混淆,在 Nuxt 中也一样:
public/ 中的资源不会经过构建转换,适合 favicon、robots.txt、静态下载文件这类稳定资源。app/assets/ 中的资源会进入构建流程,更适合业务图片、样式资源、字体等。如果一个资源你希望它保持稳定 URL,优先考虑 public/;如果你希望它参与构建优化、哈希命名、依赖分析,优先考虑 app/assets/。
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[应用根结构]
.env 文件本质上就是一个“给项目提供变量值”的配置文件。
mindmap
root((环境变量))
最常见的例子
接口基础地址
第三方服务密钥
站点标题
功能开关
最适合放
不能写死在代码里的值
随环境变化的值
多环境可切换的值
先看一个最简单的例子:
NUXT_API_SECRET=super-secret
NUXT_PUBLIC_API_BASE=
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_ 的,通常更适合服务端私有使用这是 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 服务端引擎配置
它的作用很直接:给 Nuxt 安装并启用框架级能力扩展。
可以把它理解成“Nuxt 官方推荐的扩展入口”
@pinia/nuxt 自动在 Nuxt 中注册 Pinia,无需手动写 createPinia() + app.use();直接在项目任意组件 / 页面中使用 useStore,无需重复引入。@nuxtjs/tailwindcss 自动配置 Tailwind 依赖、PostCSS、样式注入;自动识别项目中的 Tailwind 类名,无需手动创建 tailwind.config.js 基础配置;这里注册的是整个应用都会生效的全局样式文件。
如果只是某个组件自己的样式,依然优先放回组件内部;css 更适合“全项目共享”的样式入口。
它管的是整个网站通用、所有页面都生效的设置,不是某一个页面,是全站统一的规则。
这里只写了head,对应网页源码里的 <head> 标签,全站统一配置网页头部,不用每个页面单独写。
这项配置是 Nuxt 里“怎么安全、统一地读取环境配置”的标准入口;public 里的给前后端共用,外面的只留给服务端。专门存放不能写死在代码里、会随环境变化、敏感保密的配置(比如接口密钥、接口地址),运行时自动加载,不用改代码。
很多新手困惑:不是已经有 .env 了吗,为什么还要多这一层?
这里最容易踩的坑是:把敏感信息放进 public,或者误以为 .env 本身就是配置系统。
最简单的理解是:.env:负责“提供原始变量值”,runtimeConfig:负责“把变量按 Nuxt 的规则组织起来,供项目读取”;
也就是说,你可以把 .env 里的值先交给 runtimeConfig,然后在项目里统一通过 useRuntimeConfig() 去读取,而不是到处直接写 process.env.xxx。
先看一个最常见、也最实用的例子:
NUXT_API_SECRET=super-secret
NUXT_PUBLIC_API_BASE=
// 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 / 数据库配置"]
这项配置让你可以按路由粒度决定页面或接口的行为,给网站里不同的网址路径,单独设置「怎么渲染、要不要缓存、能不能访问」的规则,不用全站统一设置,精准优化每个页面。
上面这个例子表达的意思分别是:
如果你在学 Nuxt 时只记住一个“和部署形态高度相关”的配置,那通常就是它。
Nuxt 底层使用 Vite 作为开发和构建能力的一部分,因此当你确实需要改 Vite 行为时,可以从这里传配置。
常见场景包括:
更稳妥的原则是:
vite。nitro 是面向服务端引擎这一层的配置入口,常见用途包括:
它和 vite 的区别可以直接这样记:
vite 偏前端构建与开发链路nitro 偏服务端运行与输出链路对于刚接触 Nuxt 的读者来说,不需要一开始就深入 nitro 的所有细节,但至少要知道:Nuxt 不是只有 Vue 应用层配置,它还有服务端这一层。
如果想把这一节收束成最实用的判断原则,可以记住:
nuxt.config.ts。app.config.ts 或 runtimeConfig。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 最有代表性的能力。
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 结构天然对齐,项目越大越能体会到这种可读性。
布局放在 app/layouts/ 中,适合承载:
然后在 app.vue 中配合 <NuxtLayout> 使用。
如果某个页面需要特殊布局,也可以在页面中通过 definePageMeta 指定。这样做比“在每个页面里重复写头尾结构”更清晰,也更符合 Nuxt 的组织方式。
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 别名。
自动导入的好处很明显,但也要保持清醒:
所以团队协作里,通常建议对公共逻辑命名保持克制,不要让自动导入把语义搞得太散。
这一节是 Nuxt 最容易“看起来会用、实际上没用明白”的部分。因为在普通 Vue 项目里,大家很容易形成一种习惯:哪里要数据,就直接 fetch 一下。
但在 Nuxt 里,这样想往往不够,因为页面首屏数据获取要同时考虑:
$fetch 是基础能力,本质上是 Nuxt 中一个很强的同构请求工具,底层来自 ofetch。它能在服务端和客户端两边工作。你可以把它理解成:
适合在服务端路由、插件、事件处理函数里直接使用
useAsyncData 是核心的 SSR 友好数据获取,是 Nuxt 数据获取体系里更底层、也更核心的组合式函数。它做的事情不是“帮你请求”,而是:
包裹一个异步函数,在服务端执行它
把结果放进 Nuxt 的数据传递链路
在客户端 hydration 时复用这份结果,防止“二次获取”
它有一个非常重要的点:需要一个唯一的key 来去重。
useFetch 是最常用的便利层,可以理解成:useAsyncData + $fetch 的常用封装。当在页面或组件里“从某个 API 地址拿数据”时,它通常就是最自然的首选。
它之所以常用,是因为:
写法短
自带 pending、error、refresh
会根据 URL 和选项自动生成 key
如果你只是想在页面里拿 /api/products 这种接口数据,先用 useFetch,通常就是最对路的选择。
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 链路。
如果你想快速做判断,可以直接按这个规则记:
useFetchuseAsyncData$fetch$fetchuseAsyncData 的 key 一定要唯一<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:主动刷新<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 非常重要。
useState 本质上也是响应式状态,但它比普通 ref 多了一层 Nuxt 的 SSR 友好能力。可以把它理解成:
ref,会在使用相同 key 的地方共享状态所以它解决的根本问题,不只是“共享”,而是:让客户端以和服务端渲染时完全一致的初始状态启动。
如果没有这层机制,就很容易出现“服务端渲染出的是 A,客户端接管时算出来的是 B”,最终引发 hydration 不匹配。
而是把它包进一个组合式函数里。这样做的好处是: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。
useState 非常适合处理简单到中等复杂度的状态。对于复杂的全局状态管理,特别是当需要 actions、getters 以及 Vue DevTools 的时间旅行调试等高级功能时,那么官方推荐的方向就是 Pinia。Nuxt 也提供了专门的 Pinia 模块来帮你处理 SSR 集成。
你可以直接把两者理解成:
useState:轻量、直接、SSR 友好,适合大多数简单共享状态Pinia:更完整的状态管理方案,适合复杂全局状态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 全栈方案的核心竞争力之一。
如果你需要在所有请求进入前做日志、鉴权上下文注入等处理,还可以使用 server/middleware/。
// server/middleware/logger.ts
export default defineEventHandler((event) => {
console.log(`[${event.method}] 新请求: ${getRequestURL(event).pathname}`);
});
代码注意事项:服务器中间件不应返回值或结束响应,其职责是修改 event 上下文或执行副作用操作。若返回值会导致请求短路,使其无法到达目标处理器。
对新手而言,服务器中间件(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>
在 Nuxt 里,页面 <head> 相关内容不是后期随便拼接的,而是框架级能力。
从使用角度看,可以先分成两层:
nuxt.config.ts 里的 app.head,适合放全站通用的标题模板、基础 meta、favicon。useHead / useSeoMeta,适合根据当前页面数据动态设置标题和描述。可以简单理解成:
如果只写全局 app.head,当然能让站点具备基础头部信息;但如果你要真正做好 SEO,尤其是商品页、文章页、详情页这种“每页内容都不同”的页面,就必须进入页面级动态管理。
Nuxt 的 SSR 基础天然对 SEO 友好,因为搜索引擎拿到的不是一个空壳 HTML,而是已经包含页面内容的首屏结构。
但 SSR 只是基础,真正把 SEO 做细,还需要把标题、描述、Open Graph、T@witter Card 这些元数据管起来。
这里最值得先掌握的三个工具是:
useHeaduseSeoMetauseHeadSafeuseHead 是最基础、也最通用的组合式函数。只要是合法的 head 标签内容,它基本都能管理,例如:title、meta、link、script、htmlAttrs、bodyAttrs
比如:
<script setup lang="ts">
useHead({
title: '商品详情页',
meta: [
{ name: 'description', content: '这是商品详情页' }
]
})
</script>
你可以把它理解成:
如果你的目标主要是 SEO,而不是任意 head 标签管理,那么更推荐优先使用 useSeoMeta。
它的特点是:
例如你不用自己纠结:
nameproperty而是直接写更语义化的字段:title、description、ogTitle、ogDescription、ogImage、twitterCard
所以从教程角度,可以直接给一个很实用的结论:
useHeaduseSeoMetaNuxt 真正体现优势的地方,不是“能写一个静态 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 获取这正是 Nuxt 对 SEO 友好的关键原因之一。
如果你处理的是用户生成内容,或者来源不完全可控的数据,例如:
那么在把这些内容放进 head 时,要特别注意安全问题。
这时更适合使用 useHeadSafe,因为它会对输入内容做更安全的处理,避免把危险属性或值直接渲染进页面头部,从而降低 XSS 风险。
可以简单理解成:
useHead / useSeoMetauseHeadSafe如果你希望这部分先能落地,而不是一下子记一堆 API,可以先记住下面几条:
app.headuseSeoMetauseHeaduseHeadSafeNuxt 真正拉开和普通前端项目差距的地方,就在这里。你不只是“构建一个站点”,而是在决定每条路由应该怎样被渲染。
Nuxt 的默认优势之一,就是它天然适合 SSR。你不用先把 Vue 项目搭起来,再额外拼接 SSR 方案。
但 Nuxt 也不只支持 SSR,它可以在同一个项目里组合多种策略:
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”灵活得多。
根据官方文档,Nuxt 在预渲染时还会生成 _payload.json,其中包含 useAsyncData 和 useFetch 产生的序列化数据。客户端导航时可以直接读取这些 payload,而不是重复请求。
这也是为什么前面一直强调:
这一章不再按“功能清单”来讲,而是按 Nuxt 真正的工作方式来拆。重点是把 Nuxt 的生成层、运行层、服务端层、客户端接管层,以及这些层之间怎么衔接讲清楚,尤其补清楚两个经常被讲虚的点:Nuxt 到底扫描了什么,以及应用真正的入口链路是什么。
一个比较完整、能拿分的回答应该是:
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"]
很多教程会说“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
“Nuxt 的入口是什么”这个问题特别容易回答得似是而非。
从应用视图树的角度看,app/app.vue 是 Nuxt 应用的根组件入口。官方文档也明确把 app.vue 定义为 Nuxt application 的 main component。
但这里一定要分清两个层次:
app.vueapp.vue 决定的是:
最常见的写法是:
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
这里三者职责分别是:
app.vue:应用根组件<NuxtLayout>:当前页面外面套哪层布局<NuxtPage>:当前路由对应的页面组件渲染到哪里这也解释了一个很容易忽略的点:
app/pages/ 只是被扫描并生成路由记录app.vue 里的 <NuxtPage />.nuxt 里的生成入口Nuxt 框架当然不是“从你手写的 app.vue 文件直接启动”的。更准确的说法是:
app.vue 接成根组件所以要把这两层区分开:
app.vue 是应用视图树入口.nuxt 里的生成入口是框架真正的运行入口这题不要回答成“就启动开发服务器”。更完整的回答是:
nuxt.config.tsapp/ 和 server/.nuxt 与类型更直观的流程可以看这张图:
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按官方 Lifecycle 文档,可以整理成更适合理解框架执行链路的顺序:
server/plugins/这里容易被问到一个细节:
server/plugins/ 更接近服务端启动初始化server/middleware/app/plugins/validateapp/middleware/useFetch / useAsyncData这套顺序非常值得记,因为它能帮你回答很多追问:
server/middleware/ 和 app/middleware/ 有什么区别useFetch 能参与 SSR官方文档明确说明,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:开发期 / 生成期的中间结果.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。
Windows + RTX 5090 + ComfyUI 桌面版 安装 SageAttention 完全手册
08-Java工程师的Python第八课-框架入门
2026-04-08
2026-04-08