Skip to content
概述
Nuxt 项目打开后的第一组固定名称目录并不是简单的分类收纳——它们直接被框架识别并赋予行为。文件夹名称决定了路由生成、布局匹配、中间件注册等一系列自动化过程。本文围绕 pages/、layouts/、middleware/ 和 nuxt.config.ts 这几个核心参与者,梳理 Nuxt 路由与渲染体系的工作原理。
注意:Nuxt 4 目录布局
Nuxt 4 计划将pages/等目录迁移至app/内,即app/pages/、app/layouts/等。本文以 Nuxt 3 结构为主,概念和机制在 v4 中基本保留,仅是目录位置调整。
目录蓝图:约定即架构
一个最小化的 Nuxt 3 项目结构大致如下:
my-app/
├── .nuxt/ # 构建产物,自动生成
├── pages/ # 页面组件,映射为 URL 路由
├── layouts/ # 布局组件
├── middleware/ # 路由中间件
├── composables/ # 自动导入的组合式函数
├── public/ # 静态资源,直接映射到根路径
├── assets/ # 需要构建处理的资源(CSS、图片等)
├── nuxt.config.ts # 框架配置
├── package.json
└── tsconfig.json核心在于 pages/、layouts/、middleware/ 这三个目录和配置文件 nuxt.config.ts。其余如 composables/、server/、plugins/ 等也具备自动导入和约定行为,但在路由与渲染体系里,前三者是最直接的参与者。
路由即文件:从 pages/ 到 URL
pages/ 目录是路由系统的唯一数据源。在该目录下创建 .vue 文件,框架就会依据文件名和目录层次自动生成 Vue Router 的路由配置,无需手写 router.js。这种文件系统路由借鉴了 Next.js、Cloudflare Pages Functions 等方案[1],Nuxt 在此基础上增加了动态参数、可选段、嵌套路由等更细粒度的约定。
静态路由与文件路径的映射规则
文件路径到 URL 的映射是确定性的:
| 文件路径 | 对应的 URL |
|---|---|
pages/index.vue | / |
pages/about.vue | /about |
pages/users/index.vue | /users |
pages/users/profile.vue | /users/profile |
index.vue 在任意目录中代表该目录的根路由。规则很简单:去掉 pages/ 前缀和 .vue 后缀,余下的路径段就是 URL 路径。
动态路由、可选参数与通配段
需要参数化的场景(如 /users/123)通过文件名中的方括号定义动态段:
pages/users/[id].vue→ 匹配/users/123、/users/abc等,id作为路由参数可在组件内通过useRoute().params.id获取。- 方括号内可以放多个片段,但一般一个
[param]文件就够用。
可选参数用双层方括号:
pages/users/[[id]].vue→ 匹配/users和/users/123。在 Nuxt 3 中,可选参数会生成两个路径映射。- 静态路由优先级高于动态段,编排文件时需要注意冲突情况。
通配路由用于匹配任意深度的路径:
pages/docs/[...slug].vue→ 匹配/docs/a、/docs/a/b/c等。slug参数是一个数组['a', 'b', 'c']。pages/docs/[[...slug]].vue→ 同时匹配/docs本身,slug 可选,匹配到时为数组,否则为undefined。
通配段常用于文档站点、CMS 驱动页面等弹性匹配场景。
嵌套路由与页面组件的层级关系
当页面之间存在层级 UI 关系(例如侧边栏 + 内容区)时,通过目录嵌套和 <NuxtPage> 组件即可实现父子路由。目录结构:
pages/
├── parent.vue
└── parent/
└── child.vuepages/parent.vue 中必须放置一个 <NuxtPage> 作为子路由的渲染出口。访问 /parent 时,只渲染 parent.vue;访问 /parent/child 时,parent.vue 内部的 <NuxtPage> 位置会显示 child.vue 的内容。目录结构直接表达了组件树,省去了手动配置 <router-view> 的步骤。
嵌套路由的层级可以继续延伸,例如 pages/parent/child/index.vue 和 pages/parent/child/grandchild.vue,只需在 child.vue 中继续放置 <NuxtPage>。
布局系统:layouts/ 的查找与渲染
layouts/ 目录提供了一个可复用的外壳,页面组件在布局的某个插槽中渲染。布局本身是一个 Vue 组件,通过 <slot /> 输出页面内容。
默认布局与命名布局的匹配机制
- 如果存在
layouts/default.vue,所有页面默认使用该布局。 - 如果没有
default.vue,页面直接渲染而不带布局。 - 其他布局文件如
layouts/auth.vue、layouts/blog.vue属于命名布局。
Nuxt 会扫描 layouts/ 目录,将每个 .vue 文件注册为一个布局组件,文件名即布局名称(default 对应默认布局)。布局组件内需要有一个 <slot />,Nuxt 在渲染页面时会将匹配的页面组件插入该 slot。
页面级布局的指定方式
页面通过 definePageMeta 编译器宏声明使用的布局:
vue
<script setup>
definePageMeta({
layout: 'blog'
})
</script>这个宏在编译阶段被处理,并注入到路由元信息中。框架匹配到该页面时会读取 layout 字段,查找对应的布局组件包裹页面。不指定时默认使用 default,如果也没找到则忽略布局。
路由守卫:middleware/ 的执行机制
middleware/ 目录下的文件会被 Nuxt 自动注册为路由中间件,可在导航到达页面前执行权限校验、重定向、设置全局状态等逻辑。
中间件的命名约定与注册方式
中间件文件的默认导出是一个函数,接收 to 和 from 两个参数,与 Vue Router 的导航守卫参数一致。中间件可以返回 navigateTo 调用的结果来执行重定向,或者直接返回 true / 不返回来放行。Nuxt 3 中也允许返回 Promise 或抛出错误中断导航。
命名中间件(如 middleware/auth.ts)有两种使用方式:
- 在
nuxt.config.ts中通过router.middleware定义全局中间件,应用于所有路由。 - 在页面中使用
definePageMeta的middleware字段指定:
vue
<script setup>
definePageMeta({
middleware: 'auth'
})
</script>'auth' 即中间件名称,对应 middleware/auth.ts。多个中间件可以组成数组,按数组顺序执行。
Nuxt 也支持匿名(内联)中间件,直接在 definePageMeta 中定义函数,但通常建议使用命名中间件以便复用。
中间件的执行顺序与导航守卫流程
中间件在路由解析之后、页面组件渲染之前执行:
- 用户点击链接或 URL 变化触发导航。
- Nuxt 解析目标路由,获取匹配的页面组件及其元信息(布局、中间件等)。
- 按顺序执行:全局中间件(
nuxt.config中注册的) → 页面级中间件(definePageMeta.middleware中声明的数组)。 - 任一中间件返回
navigateTo或抛出错误,导航中断并跳转或错误处理。全部通过后,进入页面组件渲染。 - 布局组件包裹页面组件渲染,最终输出 HTML。
在 SSR 下,中间件在服务端执行一次,客户端激活时不会再次执行(除非配置为仅客户端执行)。因此中间件代码需注意服务端环境,避免直接访问 window 等浏览器专属对象,除非包裹在 process.client 判断中。
渲染模式:SSR、SSG 与混合方案
Nuxt 的渲染模式不是简单的“开/关服务端渲染”,它允许细粒度地控制每个路由的渲染策略。核心配置在 nuxt.config.ts 中。
nuxt.config 中的渲染模式配置
两个关键选项:
ssr: true | false—— 全局控制是否启用服务端渲染。默认为true。设为false时,整个应用退化为纯客户端渲染(CSR),服务端只返回一个空壳 HTML 和 JS 包。routeRules—— 用于定义每个路由的渲染策略,实现混合渲染:
ts
export default defineNuxtConfig({
ssr: true,
routeRules: {
'/': { prerender: true },
'/api/**': { cors: true },
'/users/**': { ssr: true },
'/admin/**': { ssr: false },
'/posts/**': { swr: 3600 }
}
})routeRules 中的规则会被 Nitro 引擎处理。prerender: true 在构建时生成静态 HTML;swr 是增量静态再生(ISR)策略,首次请求后缓存指定时间,期间返回缓存页面;ssr: false 强制该路由在客户端渲染;未明确配置的路由继承全局 ssr 设置。
请求处理流程与运行时差异
在启用 SSR 时,一次完整的请求大致经过:
- Nitro 服务器收到请求 URL。
- 匹配
routeRules,决定渲染策略(预渲染、SSR、CSR 等)。 - 如果是 SSR,执行 Vue 应用的服务端入口,初始化数据获取(
useAsyncData、useFetch),执行中间件,渲染页面为 HTML 字符串。 - 将序列化后的状态(由
useState管理的数据)嵌入 HTML 的<script>中。 - 返回完整 HTML 给客户端。
- 客户端接管(hydration),Vue 重新挂载,激活 DOM 并恢复状态。
纯 CSR 模式下,服务器返回的 HTML 只有一个挂载点,JS 下载后完全在客户端渲染。混合方案下,不同路由走不同分支,Nitro 在内部路由层做出判断。
SSG 模式下,nuxi generate 会遍历可预渲染的路由,生成静态 HTML 文件,部署时不需要 Node 服务端,但动态路由如果在构建时未知,需要提供 generate.routes 函数或配合 ISR。
内置组件:NuxtLink 与 NuxtPage 的运作方式
NuxtLink 的预加载与 active 类匹配
<NuxtLink> 是对 Vue Router 的 <RouterLink> 的封装,增加了性能优化和 Nuxt 特有行为。
基本用法:
html
<NuxtLink to="/about">关于我们</NuxtLink>to 属性指定目标,可以是路径字符串或路由对象。其他关键行为:
- 预加载:当链接出现在视口中时(默认行为),NuxtLink 会预加载目标页面的代码拆分 chunk,减少实际点击后的渲染等待。该行为通过
prefetch属性控制,可以关闭或调整触发条件。 - active 类名匹配:与 Vue Router 一样,当目标路由匹配当前路径时,会自动添加
router-link-active和router-link-exact-active类。NuxtLink 支持activeClass属性自定义类名,方便结合 Tailwind CSS 等框架高亮当前导航。匹配规则基于 Vue Router 的isActive逻辑,部分匹配会应用router-link-active。 - 外部链接:当
to是外部 URL 时,NuxtLink 会渲染为普通<a>标签,避免使用路由跳转。
NuxtPage 的页面加载与布局组合
<NuxtPage> 是页面渲染的容器,替代了 Vue Router 的 <RouterView>,并加入了布局解析、中间件触发和页面过渡支持。
在 app.vue(或 layouts/default.vue)中必然出现 <NuxtPage>。它做的事情:
- 根据当前路由匹配对应的页面组件(来自
pages/目录编译的路由配置)。 - 执行该路由声明的中间件(全局和页面级)。
- 读取
definePageMeta中的layout字段,查找对应的布局组件。 - 将页面组件作为子组件传递给布局组件,布局内部通过
<slot />接收页面内容。 - 渲染出完整的组件树。
如果有嵌套路由,父页面组件中的 <NuxtPage> 会渲染子页面,实现层级嵌套。
NuxtPage 默认支持 <Transition> 动画,可通过 pageTransition 和 layoutTransition 在 nuxt.config.ts 或页面元信息中配置过渡效果。
SSR 安全的数据获取与共享
在 SSR 场景下,数据获取需要在服务端运行,并将结果传递到客户端以便 hydration。处理不当会导致水合不匹配(hydration mismatch)或数据重复请求。Nuxt 提供了三个组合式函数来解决。
useState 的跨组件共享机制
useState 创建一个在服务端和客户端之间共享的响应式状态,且对 hydration 安全。服务端渲染时,所有调用 useState(key, init) 的状态会被收集并序列化到 HTML 的 window.__NUXT__ 对象中。客户端激活时,直接读取该全局变量恢复状态,而不是重新执行初始化函数。
ts
const counter = useState('counter', () => 0)key 是全局唯一的标识符,保证组件之间可以访问同一份状态——即使它们不在父子关系中,这对于跨路由缓存数据(如用户信息、购物车数量)非常有用。在纯客户端导航(SPA 模式)下,useState 的行为类似于一个响应式单例,状态保持在内存中。
useFetch 与 useAsyncData 的执行模型
useAsyncData 是数据获取的基础封装。它接收一个唯一键和一个异步函数,返回 data 和 error 等响应式引用。关键行为:
- 服务端执行:首次请求时在服务端执行异步函数获取数据,并将结果序列化注入到
__NUXT__。 - 客户端 hydration:不会再次执行异步函数,直接使用服务端传递的数据。
- 客户端导航:从页面 A 导航到页面 B(纯客户端路由切换)时,
useAsyncData会在客户端重新执行异步函数。可通过watch选项控制重新获取条件。
useFetch 是 useAsyncData 之上的便捷封装,内部直接调用 $fetch(基于 ofetch 的 HTTP 客户端),并自动生成键名:
ts
const { data: posts } = await useFetch('/api/posts')这等价于:
ts
const { data } = await useAsyncData('posts', () => $fetch('/api/posts'))两者核心能力一致:自动处理服务端/客户端的请求去重和状态传递。useFetch 更简洁,直接传入 URL;useAsyncData 更灵活,可以执行任意异步操作。返回对象还包含 refresh、pending、error 等属性,方便实现刷新、加载态和错误处理。通过配置 server 选项可以控制请求是否仅在客户端执行(server: false),避免在服务端执行无意义的客户端专用请求。
注意点与边界
- 目录名准确:
pages写成page或Pages都不会被识别,约定是大小写敏感的。 - 动态路由冲突:如果同时存在
pages/users/new.vue和pages/users/[id].vue,静态路由new会优先于动态段匹配,Nuxt 在编译阶段能处理这种优先级。 - 可选通配段的局限:
[[...slug]].vue在 Nuxt 3 中,当路径精确匹配该目录(不带额外段)时,slug参数为undefined,组件内需要做判断。 - 中间件执行环境:SSR 阶段中间件在 Node 环境下运行,无法访问浏览器 API。如果需要在中间件中使用
localStorage等,应通过process.client包裹或推迟到客户端组件中执行。 - 状态序列化限制:
useState在服务端和客户端之间的数据传输基于序列化和反序列化,状态不能包含不可序列化的对象(函数、类实例、Symbol 等)。复杂对象需要拆分或使用其他方案。 - useFetch 自动去重:同一页面内多个组件使用相同 URL 的
useFetch,实际只发起一次网络请求,结果共享。如果希望强制多次请求,需显式指定不同的 key。 - 混合渲染的路由匹配:
routeRules中的路由模式匹配使用的是 Nitro 的路由匹配规则,与文件系统路由生成的路由名称未必完全一致。书写时需按照构建后的路由路径(通常是 kebab-case)来配置。 - Nuxt 4 迁移:本文所述机制在 Nuxt 4 中并未废弃,但目录嵌套层级变了。迁移时需将
pages/移到app/pages/,其他目录同理。
