Nuxt 插件机制精讲
Nuxt 插件是在 Vue 应用创建后、页面渲染前执行的初始化代码。它既不是 Composable(不依赖组件实例),也不是 Nitro 插件(那是服务引擎层的)。理解 Nuxt 插件的执行时机、SSR/CSR 分离、依赖顺序和provide 注入,是构建可扩展应用架构的关键。
1. 插件的本质与执行时机
1.1 插件在生命周期中的位置
Nuxt 应用的启动流程:
1. Nitro 服务器启动(server/plugins/)
2. 收到 HTTP 请求
3. 创建 Vue 应用实例
4. ➡️ 执行 Nuxt 插件(plugins/) ← 这里
5. 执行路由中间件
6. 渲染页面组件(setup → template)
7. 序列化 Payload,返回 HTML(SSR)
8. 客户端 Hydration
9. ➡️ 再次执行 Nuxt 插件(CSR) ← 这里
核心要点:
- 插件在每个请求(SSR)或应用初始化(CSR)时执行
- 执行时机在 Vue 应用创建后、路由导航前——此时可以访问
nuxtApp,但还没有组件实例 - SSR 阶段执行一次,CSR 阶段再执行一次(除非用
.server.ts/.client.ts限定)
1.2 与其他"插件"的区别
| 类别 | 文件位置 | 执行环境 | 作用 |
|---|---|---|---|
| Nuxt 插件 | plugins/*.ts | SSR + CSR | 初始化全局依赖、注入 provide |
| Nitro 插件 | server/plugins/*.ts | 仅服务端 | 数据库连接、生命周期钩子 |
| Vue 插件 | 在 Nuxt 插件中注册 | SSR + CSR | app.use() 注册 Vue 插件 |
| Nuxt 模块 | modules/*.ts | 构建时 | 修改 Nuxt 配置和构建流程 |
1.3 基本语法
// plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
// nuxtApp.vueApp — Vue 应用实例
// nuxtApp.hook() — 注册生命周期钩子
// nuxtApp.provide() — 注入全局依赖
console.log('插件执行了', import.meta.server ? '服务端' : '客户端')
})2. .client.ts 与 .server.ts 分离
2.1 环境限定
通过文件名后缀控制插件只在特定环境执行:
plugins/
├── analytics.client.ts ← 只在客户端执行(浏览器 API)
├── sentry.client.ts ← 只在客户端执行(错误监控)
├── auth.server.ts ← 只在服务端执行(Token 初始化)
└── api.ts ← 两端都执行
什么时候用 .client.ts?
- 需要访问浏览器 API(
window、document、localStorage) - 初始化第三方 SDK(Google Analytics、Sentry 浏览器端)
- 注册 Service Worker
什么时候用 .server.ts?
- 从请求头/Cookie 初始化认证状态
- 设置服务端特有的全局配置
- 很少用——大多数服务端初始化放在
server/plugins/(Nitro 层)更合适
什么时候两端都执行?
- 注册 Vue 插件(如
app.use(pinia)) provide注入全局工具(两端都需要使用)- 注册全局错误处理
2.2 编译时优化
.client.ts 后缀不只是运行时判断——Nuxt 在构建时会将其完全排除在服务端 bundle 之外。这意味着:
- 客户端专属插件引入的第三方库不会增加服务端 bundle 大小
- 不需要在代码中写
if (import.meta.client)判断 - 相比在通用插件中做运行时判断,编译时分离更高效
3. provide 注入全局依赖
3.1 基本用法
provide 让你在插件中注入全局可用的值,任何组件中通过 useNuxtApp() 访问:
// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig()
const api = $fetch.create({
baseURL: config.public.apiBase,
onRequest({ options }) {
const token = useCookie('auth-token')
if (token.value) {
options.headers.set('Authorization', `Bearer ${token.value}`)
}
},
onResponseError({ response }) {
if (response.status === 401) navigateTo('/login')
},
})
return { provide: { api } }
})// 任何组件中使用
const { $api } = useNuxtApp()
const videos = await $api('/videos') // 自动带 Token 和 baseURL注意前缀:provide 中的 api 在使用时会变成 $api——Nuxt 自动加 $ 前缀,与组件自身的属性区分。
3.2 TypeScript 类型声明
默认情况下 $api 没有类型提示。需要通过模块声明扩展:
// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
const api = $fetch.create({ /* ... */ })
return { provide: { api } }
})
// 在同一文件底部,或单独的 types/ 目录中
declare module '#app' {
interface NuxtApp {
$api: typeof $fetch
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$api: typeof $fetch
}
}这样在组件中使用 $api 就有完整的类型提示和参数补全。
3.3 provide vs Composable
两种方式都能提供全局功能,如何选择?
| 维度 | provide(插件注入) | Composable |
|---|---|---|
| 调用方式 | useNuxtApp().$xxx | useXxx() |
| 是否依赖组件实例 | ❌ | ✅ |
| 可在模板中直接用 | ✅ $xxx | ❌ 需要先在 setup 中调用 |
| 适合场景 | 全局工具(API 客户端、格式化函数) | 业务逻辑(视频列表、认证) |
| 生命周期管理 | 无(全局单例) | 有(跟随组件) |
经验法则:无状态的工具用 provide,有状态的逻辑用 Composable。
4. 插件依赖与执行顺序
4.1 默认顺序
插件按以下规则排序执行:
nuxt.config.ts中plugins数组的顺序plugins/目录下按文件名字母序自动扫描
// nuxt.config.ts
export default defineNuxtConfig({
plugins: [
'~/plugins/01.auth.ts', // 手动指定的优先执行
'~/plugins/02.api.ts',
],
})如果没有在 nuxt.config.ts 中手动指定,plugins/ 目录下的文件会按字母序自动注册。推荐用数字前缀控制顺序。
4.2 依赖声明
如果插件 B 依赖插件 A 的 provide,可以用 dependsOn 显式声明:
// plugins/02.api.ts — 依赖 auth 插件先初始化
export default defineNuxtPlugin({
name: 'api',
dependsOn: ['auth'], // 等 auth 插件执行完再执行
setup(nuxtApp) {
// 此时 $auth 已经可用
const { $auth } = nuxtApp
// ...
},
})// plugins/01.auth.ts
export default defineNuxtPlugin({
name: 'auth', // 被依赖的插件必须声明 name
setup(nuxtApp) {
return { provide: { auth: { /* ... */ } } }
},
})4.3 并行 vs 串行
默认情况下,没有依赖关系的插件会串行执行(按顺序)。Nuxt4 引入了 parallel 选项允许并行执行以加速启动:
export default defineNuxtPlugin({
name: 'analytics',
parallel: true, // 不阻塞其他插件
setup(nuxtApp) {
// 初始化分析 SDK(不需要等待结果)
},
})5. 异步插件与初始化等待
5.1 异步初始化
插件可以是异步的——Nuxt 会等待所有插件执行完毕后才开始渲染:
// plugins/auth.server.ts
export default defineNuxtPlugin(async (nuxtApp) => {
const token = useCookie('auth-token')
if (token.value) {
// 服务端启动时验证 Token,获取用户信息
try {
const user = await $fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token.value}` },
})
useState('user', () => user)
} catch {
token.value = null // Token 过期,清除
}
}
})注意:异步插件会阻塞页面渲染。如果插件初始化很慢(如调用外部 API),会直接影响 TTFB。非关键初始化应该用 parallel: true 或移到 onNuxtReady 中。
5.2 避免阻塞的策略
| 策略 | 实现 | 适用场景 |
|---|---|---|
parallel: true | 不阻塞其他插件 | 不依赖其他插件的初始化 |
.client.ts + onNuxtReady | Hydration 后初始化 | 分析 SDK、聊天组件 |
| 后台初始化 + 状态标记 | 启动异步任务但不 await | SDK 预热 |
// plugins/chat.client.ts — 不阻塞渲染
export default defineNuxtPlugin((nuxtApp) => {
// 不用 async/await,不阻塞
onNuxtReady(async () => {
const { default: ChatWidget } = await import('~/lib/chat-widget')
ChatWidget.init({ appId: 'xxx' })
})
})6. 注册 Vue 插件
Nuxt 插件中可以注册 Vue 插件、全局组件、全局指令:
// plugins/vue-plugins.ts
import VueTippy from 'vue-tippy'
export default defineNuxtPlugin((nuxtApp) => {
// 注册 Vue 插件
nuxtApp.vueApp.use(VueTippy, {
defaultProps: { placement: 'top', arrow: true },
})
// 注册全局组件
nuxtApp.vueApp.component('GlobalLoader', defineAsyncComponent(
() => import('~/components/GlobalLoader.vue')
))
// 注册全局指令
nuxtApp.vueApp.directive('focus', {
mounted(el) { el.focus() },
})
})7. 生命周期钩子
插件中可以注册 Nuxt 的全局生命周期钩子,用于监控、日志、错误处理:
// plugins/lifecycle-logger.ts
export default defineNuxtPlugin((nuxtApp) => {
// 页面导航开始
nuxtApp.hook('page:start', () => {
console.log('页面导航开始')
})
// 页面导航完成
nuxtApp.hook('page:finish', () => {
console.log('页面导航完成')
})
// Vue 错误捕获(全局 errorHandler)
nuxtApp.hook('vue:error', (error, instance, info) => {
console.error('Vue 错误:', error, info)
// 上报到监控平台
})
// 应用挂载完成(仅客户端)
nuxtApp.hook('app:mounted', () => {
console.log('应用挂载完成')
})
})常用钩子:
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
app:created | Vue 实例创建后 | 早期初始化 |
app:mounted | 客户端挂载完成 | DOM 操作 |
page:start | 路由导航开始 | 加载进度条 |
page:finish | 路由导航完成 | 停止进度条、页面埋点 |
vue:error | Vue 渲染/setup 错误 | 错误监控上报 |
app:error | 致命错误 | 全局错误兜底 |
8. 插件实战:全局加载进度条
一个经典的插件案例——NProgress 加载进度条:
// plugins/loading-bar.client.ts
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
export default defineNuxtPlugin((nuxtApp) => {
NProgress.configure({ showSpinner: false, minimum: 0.1 })
nuxtApp.hook('page:start', () => {
NProgress.start()
})
nuxtApp.hook('page:finish', () => {
NProgress.done()
})
// 错误时也要停止进度条
nuxtApp.hook('vue:error', () => {
NProgress.done()
})
})这个插件用 .client.ts 后缀——NProgress 操作 DOM,服务端无需执行。利用 page:start 和 page:finish 钩子自动控制进度条,无需在每个页面中手动调用。
本章小结
- 执行时机:Vue 应用创建后、路由导航前,SSR 和 CSR 各执行一次
- 环境分离:
.client.ts/.server.ts后缀实现编译时分离,不只是运行时判断 - provide 注入:全局工具用
provide($前缀访问),业务逻辑用 Composable - 类型声明:通过
declare module '#app'扩展NuxtApp接口获得类型提示 - 执行顺序:数字前缀控制顺序,
dependsOn声明依赖,parallel允许并行 - 异步插件:会阻塞渲染,非关键初始化用
onNuxtReady或parallel: true - 生命周期钩子:
page:start/finish(导航)、vue:error(错误)、app:mounted(挂载)