Nuxt 插件系统详解:app/plugins 目录的自动扫描、排序与并行加载机制
Nuxt 通过 app/plugins/ 目录为 Vue 应用创建阶段提供了一套插件系统,它是向 NuxtApp 实例注入全局能力、注册 Vue 插件/指令、挂载运行时 hook 的标准入口。本文以官方文档中 app/plugins 目录规范为主体,完整梳理插件的扫描规则、函数/对象两种语法、注册顺序控制与并行加载策略,并结合 Nuxt 仓库源码(packages/nuxt/src/app/nuxt.ts、packages/nuxt/src/core/plugins/plugin-metadata.ts、packages/kit/src/plugin.ts)还原其背后的静态分析与运行时调度实现,读完后你能独立编写、排序、并行化 Nuxt 插件,并理解其构建优化原理。
app/plugins/ 目录的自动扫描规则
Nuxt 会自动读取 app/plugins/ 目录中的文件,并在创建 Vue 应用时加载它们。有两点核心约定:
- 自动注册:目录内所有插件都会自动注册,无需在
nuxt.config.ts中再次声明; - 端侧限定:文件名中可使用
.server或.client后缀,让插件仅在服务端或客户端加载,例如vue-gtag.client.ts只会在浏览器端执行。
这个后缀约定在源码中由 normalizePlugin 实现:当插件未显式指定 mode 时,Nuxt 会用正则从文件名中提取 client/server 后缀作为加载模式,缺省值为 all(两端都加载);旧版的 ssr 选项也会被归一化为 mode: 'server'。
哪些文件会被注册
只有目录顶层的文件(或任意子目录中的 index 文件)会被自动注册为插件:
# 目录结构
- plugins/
- foo.ts // 会被扫描
- bar/
- baz.ts // 不会被扫描
- foz.vue // 不会被扫描
- index.ts // 当前会被扫描(已弃用,不推荐)
在上述结构中,只有 foo.ts 和 bar/index.ts 会被注册。如果确实需要注册子目录中的插件,应通过 nuxt.config 的 plugins 选项 显式声明:
// nuxt.config.ts
export default defineNuxtConfig({
plugins: [
'~/plugins/bar/baz',
'~/plugins/bar/foz',
],
})
补充一个源码细节:模块和内置功能通过 addPlugin 注册插件时,默认会插入到插件数组头部(unshift),以保证模块插件先于用户插件执行;只有传入 { append: true } 才会追加到末尾。这也解释了为什么模块注入的插件通常优先于你的业务插件运行。仓库中的 test/fixtures/basic/app/plugins/ 目录是一个很好的真实样例集,其中包含 server-only.server.ts(端侧后缀)、10.layer-ordering.ts(数字前缀排序)、dependsOnPlugin.ts(依赖声明)等覆盖本文各主题的插件示例。
创建插件:函数式语法与对象语法
传给插件的唯一参数是 nuxtApp 实例,最简形式如下:
// app/plugins/hello.ts
export default defineNuxtPlugin((nuxtApp) => {
// 使用 nuxtApp 做一些事情
})
defineNuxtPlugin 的实现在 packages/nuxt/src/app/nuxt.ts:对函数式插件它直接原样返回,仅在函数上打一个 __nuxt_plugin 标记(用于 isNuxtPlugin 判断);对对象式插件,它把 setup 函数与元数据对象合并(Object.assign),并保留 _name 供依赖调度使用。
对象语法(Object Syntax)
对于更高级的场景,可以使用对象语法定义插件:
// app/plugins/hello.ts
export default defineNuxtPlugin({
name: 'my-plugin',
enforce: 'pre', // 或 'post'
async setup (nuxtApp) {
// 等价于普通函数式插件
},
hooks: {
// 可以直接注册 Nuxt app 运行时 hook
'app:created' () {
const nuxtApp = useNuxtApp()
// 在 hook 中做事
},
},
env: {
// 若不希望插件在渲染 server-only 或 island 组件时执行,设为 false
islands: true,
},
})
对象式插件各字段的完整定义见 defineNuxtPlugin 文档:name、enforce、dependsOn、order、parallel、setup、hooks、env 均为可选,其中 order 提供比 enforce 更精细的排序控制,会覆盖 enforce 的取值。
对象属性的静态分析约束。文档特别强调:若使用对象语法,属性会被静态分析以产出更优化的构建产物,因此不应在运行时动态定义属性——例如 enforce: import.meta.server ? 'pre' : 'post' 会破坏 Nuxt 对插件做的任何优化。这一点从源码得到印证:
- extractMetadata 在构建期解析插件源码的 AST,定位
defineNuxtPlugin/definePayloadPlugin调用,仅提取字面量形式的name、order、enforce、dependsOn、parallel,并对hooks/env仅记录“是否存在”(hasHooks/hasEnv标志)。若插件参数是无法静态读取的表达式(导入的标识符、工厂调用等),会被标记为_metaUnknown,回退到完整运行时解析; - internalOrderMap 揭示了排序的数值布局:内置插件占据
nuxt-pre-all(-50)、nuxt-revivers(-30)、nuxt-default(-10)、nuxt-post(10)、nuxt-post-all(30)等区间,用户插件的pre/default/post分别映射到-20/0/+20,即“用户 pre 先于内置 default,用户 post 晚于内置 post”的交错顺序; - RemovePluginMetadataPlugin 是构建期的转换插件,它会把已被静态提取的
name/order/enforce以及按当前构建端(client/server)过滤后的dependsOn从最终产物中移除,从而减小包体并允许两端各自只保留相关依赖声明。
hooks 的执行时机也值得注意:在 applyPlugins 中,所有插件的 hook 会在任何 setup 执行之前被统一批量注册(registerPluginHooks),因此使用对象语法时定义 hook 不必担心插件注册顺序问题。
注册顺序:文件名数字前缀
通过给文件名加上“字母序”数字前缀,可以控制插件的注册顺序:
plugins/
| - 01.myPlugin.ts
| - 02.myOtherPlugin.ts
此时 02.myOtherPlugin.ts 就能访问 01.myPlugin.ts 注入的任何内容,这在插件之间存在依赖(例如一个插件向 nuxtApp 提供状态,另一个插件消费它)时非常有用。
::: 注意:文件名是按字符串排序而非数值排序,10.myPlugin.ts 会排在 2.myOtherPlugin.ts 之前。这就是示例中要给个位数编号补零(01、02)的原因。
加载策略:parallel 与 dependsOn
并行插件
默认情况下 Nuxt 顺序加载插件。将插件标记为 parallel 后,Nuxt 不会等待该插件执行完毕才加载下一个插件:
// app/plugins/my-plugin.ts
export default defineNuxtPlugin({
name: 'my-plugin',
parallel: true,
async setup (nuxtApp) {
// 下一个插件会立即开始执行
},
})
声明插件依赖
如果某个插件需要等待另一个插件完成后再运行,把被依赖插件的 name 加入 dependsOn 数组即可:
// app/plugins/depending-on-my-plugin.ts
export default defineNuxtPlugin({
name: 'depends-on-my-plugin',
dependsOn: ['my-plugin'],
async setup (nuxtApp) {
// 本插件会等待 `my-plugin` 执行完毕后才运行
},
})
从运行时实现看,applyPlugins 首先检查是否存在依赖或并行插件:若都没有,走一条轻量顺序循环(逐个 await applyPlugin,出错时若正在渲染 error.vue 则先短路记录、最后统一抛出);一旦存在,则切换到 applyPluginsWithDependencies 的调度器:
- 每次执行插件前,先按
dependsOn过滤出“尚未 resolve 的依赖名”;有未满足依赖的插件被压入等待队列,依赖 resolve 后按剩余依赖集合自动触发执行; parallel: true的插件以 Promise 形式并发执行、不阻塞主循环,最后由Promise.all(parallels)统一收口;非并行插件则await其完成;- 错误处理策略:非并行插件或正在渲染错误页时,错误立即短路抛出;
parallel插件的错误会被记录并延迟到所有并行插件结束后统一抛出。
另外,当服务端渲染 island 组件时,env.islands === false 的插件会被整体跳过(见 applyPlugins 中的 checkIslandEnv 逻辑),这正是对象语法 env 字段的运行时落点。
在插件中使用 composables
Nuxt 插件内可以使用 composables 和 utils:
// app/plugins/hello.ts
export default defineNuxtPlugin((nuxtApp) => {
const foo = useFoo()
})
但文档明确给出了两条限制,需要牢记:
- 若 composable 依赖一个更晚注册的插件,可能失效。插件按顺序调用且先于其他一切执行,你在其中调用的 composable 所依赖的插件可能尚未被调用;
- 依赖 Vue 组件生命周期的 composable 不可用。Vue composable 通常绑定当前组件实例(
getCurrentInstance),而插件只绑定到nuxtApp实例,运行在组件实例之外。
提供全局助手(provide)
若想在 NuxtApp 实例上提供助手函数,从插件返回值中以 provide 键返回即可:
// 函数式写法(app/plugins/hello.ts)
export default defineNuxtPlugin(() => {
return {
provide: {
hello: (msg: string) => `Hello ${msg}!`,
},
}
})
// 对象语法写法(app/plugins/hello-object-syntax.ts)
export default defineNuxtPlugin({
name: 'hello',
setup () {
return {
provide: {
hello: (msg: string) => `Hello ${msg}!`,
},
}
},
})
之后即可在组件中使用:
<!-- app/components/Hello.vue -->
<script setup lang="ts">
// 也可以在这里直接使用
const { $hello } = useNuxtApp()
</script>
<template>
<div>
{{ $hello('world') }}
</div>
</template>
其底层机制是 createNuxtApp 中的 nuxtApp.provide:以 $ 前缀为 nuxtApp 和 vueApp.config.globalProperties 同时定义 getter,因此 $hello 既可以从 useNuxtApp() 解构,也能在模板全局属性中访问。
两条重要提醒(原文档原样保留):
- 官方强烈建议优先使用 composables 而非 provide 助手,以避免污染全局命名空间、保持主 bundle 入口小巧;
- 若插件 provide 的是
ref或computed,它在组件<template>中不会被自动解包——这是 Vue 对非模板顶层 ref 的固有行为,需在<script>中显式.value访问或手动解包。
插件的类型
如果从插件返回了助手,它们会被自动类型化:useNuxtApp() 的返回值和模板中都会带类型。若需要在另一个插件内部使用已提供的助手,可调用 useNuxtApp() 获取类型化版本,但除非你确定插件执行顺序,否则应避免这种做法。
对高级场景,也可以手动声明注入属性的类型:
// index.d.ts
declare module '#app' {
interface NuxtApp {
$hello (msg: string): string
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$hello (msg: string): string
}
}
export {}
集成 Vue 插件与自定义指令
包装 Vue 生态插件
要使用 vue-gtag-next 这类 Vue 插件(例如为站点接入 Google Analytics),可以在 Nuxt 插件中调用 nuxtApp.vueApp.use()。先安装依赖:
npm install --save-dev vue-gtag-next
# 或 pnpm add -D vue-gtag-next / yarn add --dev vue-gtag-next / bun add -D vue-gtag-next
然后创建插件文件(注意 .client.ts 后缀,统计代码只需在浏览器运行):
// app/plugins/vue-gtag.client.ts
import VueGtag, { trackRouter } from 'vue-gtag-next'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueGtag, {
property: {
id: 'GA_MEASUREMENT_ID',
},
})
trackRouter(useRouter())
})
注册自定义指令
同理,可以在插件中注册自定义 Vue 指令:
// app/plugins/my-directive.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive('focus', {
mounted (el) {
el.focus()
},
getSSRProps (binding, vnode) {
// 在这里提供 SSR 专属属性
return {}
},
})
})
注意:注册 Vue 指令时,除非该指令只在一端渲染,否则必须同时在客户端和服务端注册。如果某指令只在客户端有意义,可将其放在 ~/plugins/my-directive.client.ts,并为服务端在 ~/plugins/my-directive.server.ts 中提供一个“stub”空指令,保证两端行为一致。
小结
app/plugins/ 体系的关键点可以归纳为:顶层文件自动注册、.client/.server 后缀限定执行端、数字前缀控制顺序、对象语法提供 enforce/parallel/dependsOn/hooks/env 等声明式能力,且这些元数据会参与构建期的静态分析(plugin-metadata.ts)以驱动排序与产物精简。仓库内 packages/nuxt/src/app/plugins/ 目录(如 payload.client.ts、preload.server.ts、router.ts)展示了 Nuxt 自身插件的官方写法,可作为生产级插件的参考实现;docs/3.guide/2.best-practices/plugins.md 则进一步给出了插件编写的最佳实践。
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 StartedRust0623
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