Nuxt 迁移指南:从 Nuxt 2 迁移 Plugins 与 Route Middleware
本文是 Nuxt 迁移系列中针对 插件(Plugins) 与 路由中间件(Route Middleware) 的专门指南,讲解从 Nuxt 2 升级到 Nuxt 3 时这两类机制在 API 形态、注册方式与运行模型上的全部变化。读完本文,你将掌握:如何用 defineNuxtPlugin 重构旧插件并用 nuxtApp.provide 替代 inject、如何利用 ~/plugins 目录自动注册与 .client/.server 文件后缀、如何用 defineNuxtRouteMiddleware 重构路由守卫并用 navigateTo/abortNavigation 取代 redirect/next(),以及如何把全局中间件迁移到 ~/middleware/*.global.ts。文中代码均以当前仓库(Nuxt 主仓库,内含 docs 迁移指南与全部运行时源码)为基准。
迁移总览:两处「参数签名 + 注册方式」的范式变化
在 Nuxt 2 中,插件与路由中间件都依赖「上下文对象 + 副作用注册」的旧范式:插件回调接收 (ctx, inject) 两个参数,通过在 ctx.app 上注入 $ 前缀方法或读写 Vuex store 来工作;中间件同样接收包含 store、redirect 的上下文对象。Nuxt 3 之后这两套 API 被彻底重构,共同点非常清晰:
| 关注点 | Nuxt 2 | Nuxt 3+ |
|---|---|---|
| 插件定义 | 默认导出 (ctx, inject) => {} |
默认导出 defineNuxtPlugin((nuxtApp) => {}) |
| 注入方法 | inject('name', fn) |
nuxtApp.provide('name', fn) 或插件 return { provide: {...} } |
| 中间件定义 | 默认导出 ({ store, redirect }) => {} |
默认导出 defineNuxtRouteMiddleware((to, from) => {}) |
| 重定向 | redirect('/login') |
return navigateTo('/login') |
| 页面引用中间件 | 组件选项 middleware: 'auth' |
definePageMeta({ middleware: 'auth' }) |
| 客户端/服务端限定 | nuxt.config 中 mode: 'client' / 'server' |
文件名后缀 .client.ts / .server.ts |
| 全局中间件 | nuxt.config 中声明 |
~/middleware/auth.global.ts 文件后缀 |
下面分两大部分详述迁移步骤与背后的源码依据。
插件(Plugins)迁移
新格式:单一参数 nuxtApp,inject 变为 provide
Nuxt 3 的插件回调只接收一个参数 nuxtApp(当前 Nuxt 应用实例,类型为 NuxtApp)。原文档给出了最小对照示例:
export default (ctx, inject) => {
inject('injected', () => 'my injected function')
}
export default defineNuxtPlugin((nuxtApp) => {
// 现在在 `nuxtApp.$injected` 上可用
nuxtApp.provide('injected', () => 'my injected function')
// 也可以使用这种写法,自带自动类型推导
return {
provide: {
injected: () => 'my injected function',
},
}
})
注意两点关键差异:
- 旧
ctx对象被移除。Nuxt 2 上下文中的app、store、route等在 Nuxt 3 中不再作为参数注入插件。你应改用对应的组合式函数在插件内获取所需能力——例如需要访问 Vue 应用实例时使用nuxtApp.vueApp,需要读取路由时可在此调用组合式函数(因为插件运行在nuxtApp的 effect scope 内,useRouter()、useState()等均可正常使用)。 - 自动的
$前缀。provide的键会自动带上$前缀挂载。在源码 packages/nuxt/src/app/nuxt.ts 中可以看到nuxtApp.provide的实现:它会同时把值定义为nuxtApp自身与vueApp.config.globalProperties上的$前缀 getter——因此组件模板中可直接使用$injected,脚本中也可通过useNuxtApp().$injected访问:
nuxtApp.provide = (name: string, value: any) => {
const $name = '$' + name
defineGetter(nuxtApp, $name, value)
defineGetter(nuxtApp.vueApp.config.globalProperties, $name, value)
}
推荐用 return { provide } 以获得自动类型
defineNuxtPlugin 的类型签名会从 provide 返回对象的键推断注入内容。在 packages/nuxt/src/app/nuxt.ts 中 defineNuxtPlugin 被实现为一个几乎「透明」的包装函数——若传入普通函数则原样返回;若传入对象(含 setup、name、enforce、parallel、dependsOn 等字段)则把对象拍平为带元信息的函数,同时保留 NuxtPluginIndicator 标记,供运行时用 isNuxtPlugin 识别:
export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPlugin<T>): Plugin<T> & ObjectPlugin<T> {
if (typeof plugin === 'function') { return plugin }
// ... 对象语法插件会被归一化为 setup 函数并附带元信息
}
因此官方推荐优先使用「返回 provide 对象」的第二种写法:类型会由返回值自动推导,useNuxtApp() 的返回类型与组件模板中的 $xxx 都能获得完整补全,无需手写 .d.ts 模块声明。仓库 packages/nuxt/src/app/nuxt.ts 中 Nuxt 自身的运行时也遵循这一约定,例如注入 config 时即使用 nuxtApp.provide(key, provide[key]) 遍历合并用户插件返回的 provide。
顺带一提,源码还导出 definePayloadPlugin(同文件 L520),它是 defineNuxtPlugin 的类型别名,专用于需要在两端(服务端渲染结果与客户端水合)同步 payload 数据的场景,迁移时若你的 Nuxt 2 插件本就是在 payload 层面工作的,可留意这一 API。
迁移步骤
原文档给出了插件迁移的三步清单,结合目录结构与源码可以展开如下:
1. 用 defineNuxtPlugin 包裹插件。 将所有 app/plugins/ 下(或 nuxt.config 中注册的)插件导出改为 export default defineNuxtPlugin((nuxtApp) => { ... }),并把 inject('name', value) 改写为 nuxtApp.provide('name', value) 或 return { provide: { name: value } }。defineNuxtPlugin 是自动导入的(见仓库导入预设 packages/nuxt/src/imports/presets.ts),无需手动 import。
2. 删除 nuxt.config plugins 数组中位于 app/plugins/ 目录内的条目。 Nuxt 3 会自动注册该目录下的文件:仅顶层文件以及任何子目录中的 index 文件会被扫描(见目录文档 docs/2.directory-structure/1.app/1.plugins.md)。例如:
-| app/plugins/
---| foo.ts // 自动注册
---| bar/
-----| baz.ts // 不会自动注册
-----| index.ts // 自动注册(取父目录名 bar)
如果需要注册子目录中的非 index 文件,才需在 nuxt.config.ts 的 plugins 数组显式声明,如 plugins: ['~/plugins/bar/baz']。
3. 用文件名后缀替代 mode。 若你的插件原本通过 mode: 'client' / mode: 'server' 限定执行环境,请删除该配置并把模式写进文件名:~/plugins/my-plugin.client.ts 只在客户端加载,~/plugins/my-plugin.server.ts 只在服务端加载。不写后缀则两端都会执行。若插件同时需要「仅客户端指令 + 服务端空实现」,按目录文档建议分别提供 my-directive.client.ts 与 my-directive.server.ts。
进阶:对象语法与加载顺序(额外补充)
如果你的插件原本依赖注册顺序,迁移后要注意 Nuxt 3 的目录文档新增的规则(这些能力同样影响迁移质量):
- 数字前缀控制顺序:
01.myPlugin.ts先于02.myOtherPlugin.ts执行;注意文件按「字符串排序」,两位数请补零(10.x会排在2.x前面)。 - 对象语法支持
enforce: 'pre' | 'post'、parallel: true(不必等待前一个插件完成)、dependsOn: ['plugin-name'](显式声明依赖)以及hooks(直接注册 app 运行时钩子)。这些元信息会被构建期静态分析(见仓库 packages/nuxt/src/core/plugins/plugin-metadata.ts),所以应在编译期写死,不要在运行时用import.meta.server动态生成enforce。 - 一个用于 Vue 生态插件的迁移示例:在
app/plugins/vue-gtag.client.ts中通过nuxtApp.vueApp.use(VueGtag, {...})挂载 Vue 插件,并可用useRouter()追踪路由变化。
路由中间件(Route Middleware)迁移
新格式:(to, from) 守卫,靠返回值而非 next() 控制导航
路由中间件的核心变化是:不再接收包含 store/redirect 的上下文,而是成为标准的 vue-router 导航守卫,接收 to(目标路由)与 from(来源路由),通过返回值声明式地决定导航结果。原文档的对照示例(典型的登录鉴权场景):
export default function ({ store, redirect }) {
// 如果用户未认证
if (!store.state.authenticated) {
return redirect('/login')
}
}
export default defineNuxtRouteMiddleware((to, from) => {
const auth = useState('auth')
if (!auth.value.authenticated) {
return navigateTo('/login')
}
})
上例同时示范了两个迁移要点:
- Vuex
store.state不再可注入。全局状态请改用useState('auth')(在 docs/4.api/2.composables/use-state.md 可查看其用法),或改用 Pinia 并借助useState在其上建立 SSR 安全的全局单例;这一思路在迁移系列的配置篇 docs/7.migration/2.configuration.md 中也有对应说明。 - 重定向由「调用副作用」改为「返回导航结果」。在 packages/nuxt/src/app/composables/router.ts 中,中间件的类型定义如下——返回值兼容 vue-router 的
NavigationGuard,但官方建议只使用 Nuxt 提供的辅助函数:
export interface RouteMiddleware {
(to: RouteLocationNormalized, from: RouteLocationNormalized): ReturnType<NavigationGuard>
}
export function defineNuxtRouteMiddleware (middleware: RouteMiddleware): RouteMiddleware {
return middleware
}
中间件可返回的值及含义(详见中间件目录文档 docs/2.directory-structure/1.app/1.middleware.md):
- 什么都不返回 —— 不阻断导航,继续执行下一个中间件或完成路由跳转;
return navigateTo('/')—— 重定向,服务端发起时为302 Found(navigateTo 文档);return navigateTo('/', { redirectCode: 301 })—— 以301 Moved Permanently重定向;return abortNavigation()—— 中止当前导航;return abortNavigation(error)—— 中止导航并携带错误(abortNavigation 文档)。
navigateTo 只是路由辅助函数之一,同类还有 abortNavigation、addRouteMiddleware 等(在仓库中它们与中间件定义同处于 packages/nuxt/src/app/composables/router.ts)。重定向时请留意防止死循环:例如先判断 to.path !== '/login' 再重定向。
页面如何引用中间件:从组件选项到 definePageMeta
与 Nuxt 2 相同,放在 ~/middleware 目录(即 app/middleware/)中的中间件文件会被自动注册,之后即可在页面里按名字引用。区别在于:Nuxt 2 用组件选项 middleware: 'auth',Nuxt 3 用编译宏 definePageMeta:
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
// 或数组形式,按声明顺序执行:
// middleware: [function (to, from) { /* 内联匿名中间件 */ }, 'auth'],
})
</script>
注意两点由源码与目录文档共同确认的规则:
- 中间件名会被规范为 kebab-case:文件
myMiddleware.ts注册名为my-middleware; - 只注册目录顶层文件与子目录的
index文件,且middleware/auth/index.ts会以父目录名auth注册。若使用了更深层目录的中间件文件,需通过definePageMeta引用对应名称或用addRouteMiddleware手动注册。
中间件(含自动导入的 defineNuxtRouteMiddleware、navigateTo 等)都在 ~/middleware、~/pages、~/plugins 内自动导入,无需显式 import(见 packages/nuxt/src/imports/presets.ts)。
在中间件内访问路由的注意事项
中间件内应始终通过 to/from 参数访问路由,不要在中间件内部调用 useRoute()——中间件执行期间并不存在确定的「当前路由」,因为导航可能被重定向或中止。若你封装的工具函数内部隐式调用了 useRoute(),也会触发这一隐患。因此最佳实践是把路由作为参数传入辅助函数,例如把逻辑抽到 utils/handle-route.ts 时采用 export function doSomethingWithRoute (route = useRoute()) 的形式。
全局中间件:.global 后缀替代 nuxt.config 声明
原文档的第二条迁移说明指出:原先声明在 nuxt.config 中的全局中间件(例如 router: { middleware: ['auth'] } 风格)应迁移到 ~/middleware 目录,并在文件名上追加 .global 后缀:
-| app/middleware/
---| auth.global.ts // 每次路由变化都会执行
---| profile.ts // 具名中间件,仅在页面引用时执行
运行时在 packages/nuxt/src/app/plugins/router.ts(客户端路由插件)中会把构建期产物 #build/middleware 中的全局中间件与运行时通过 addRouteMiddleware 追加的全局中间件合并执行:先全局、后页面声明的具名/内联中间件。源码中的内部数据结构 _middleware: { global: RouteMiddleware[]; named: Record<string, RouteMiddleware> }(packages/nuxt/src/app/nuxt.ts)清晰反映了「全局数组 + 具名字典」的存储模型。
关于全局中间件顺序与执行时机的几个迁移要点:
- 全局中间件默认按文件名字母序执行,可用数字前缀强制排序(如
01.setup.global.ts、02.analytics.global.ts,注意字符串排序需补零); - SSR/静态生成时,首次进入页面的中间件会在服务端渲染和客户端水合后各执行一次。若中间件依赖浏览器环境(如读 localStorage),可在内部用
import.meta.server/import.meta.client或useNuxtApp().isHydrating判断跳过;渲染错误页属于一次全新的页面加载,已注册的全局中间件会再次执行,可用useError()感知错误上下文; - 想在插件中动态注册中间件(替代以往在
nuxt.config或router.extendRoutes中拼装逻辑的做法),可使用addRouteMiddleware(docs/4.api/3.utils/add-route-middleware.md):
export default defineNuxtPlugin(() => {
addRouteMiddleware('global-test', () => {
console.log('每次路由变化都会运行的全局中间件')
}, { global: true })
})
迁移步骤
- 把中间件改写为
defineNuxtRouteMiddleware((to, from) => {}):将上下文对象参数替换为路由守卫参数,redirect()换成return navigateTo(...),store相关状态改用useState等组合式函数,next()语义替换为按需return或return abortNavigation()。 - 把全局中间件从
nuxt.config移到~/middleware目录,并以.global后缀结尾,例如~/middleware/auth.global.ts。 - 把页面中引用中间件的组件选项改为
definePageMeta(该宏在编译期被静态分析,只接受字面量数组/字符串,见 packages/schema/src/config/build.ts 中的异步转换配置);同时留意中间件名统一为 kebab-case,并核对app/middleware/的顶层/index扫描规则。
小结
从 Nuxt 2 升级到 Nuxt 3 的过程中,插件与路由中间件的迁移可以概括为两条主线:插件从「上下文 + inject」走向「nuxtApp 单参数 + provide/返回对象」,并全面依赖 ~/plugins 目录自动注册与 .client/.server 文件后缀来替代手动 mode 配置;路由中间件从「{ store, redirect } 副作用式守卫」走向「(to, from) + 返回值导航」的纯函数式守卫,配合 ~/middleware/*.global.ts 与 definePageMeta 完成注册与引用。迁移完成后,可继续阅读目录结构篇的 plugins 目录文档、middleware 目录文档,或在源码 packages/nuxt/src/app/nuxt.ts、packages/nuxt/src/app/composables/router.ts 与 packages/nuxt/src/app/plugins/router.ts 中追踪这两套机制的完整实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00