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/*.tsSSR + CSR初始化全局依赖、注入 provide
Nitro 插件server/plugins/*.ts仅服务端数据库连接、生命周期钩子
Vue 插件在 Nuxt 插件中注册SSR + CSRapp.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(windowdocumentlocalStorage
  • 初始化第三方 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().$xxxuseXxx()
是否依赖组件实例
可在模板中直接用$xxx❌ 需要先在 setup 中调用
适合场景全局工具(API 客户端、格式化函数)业务逻辑(视频列表、认证)
生命周期管理无(全局单例)有(跟随组件)

经验法则:无状态的工具用 provide,有状态的逻辑用 Composable。

4. 插件依赖与执行顺序

4.1 默认顺序

插件按以下规则排序执行:

  1. nuxt.config.tsplugins 数组的顺序
  2. 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 + onNuxtReadyHydration 后初始化分析 SDK、聊天组件
后台初始化 + 状态标记启动异步任务但不 awaitSDK 预热
// 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:createdVue 实例创建后早期初始化
app:mounted客户端挂载完成DOM 操作
page:start路由导航开始加载进度条
page:finish路由导航完成停止进度条、页面埋点
vue:errorVue 渲染/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:startpage:finish 钩子自动控制进度条,无需在每个页面中手动调用。

本章小结

  • 执行时机:Vue 应用创建后、路由导航前,SSR 和 CSR 各执行一次
  • 环境分离.client.ts / .server.ts 后缀实现编译时分离,不只是运行时判断
  • provide 注入:全局工具用 provide$ 前缀访问),业务逻辑用 Composable
  • 类型声明:通过 declare module '#app' 扩展 NuxtApp 接口获得类型提示
  • 执行顺序:数字前缀控制顺序,dependsOn 声明依赖,parallel 允许并行
  • 异步插件:会阻塞渲染,非关键初始化用 onNuxtReadyparallel: true
  • 生命周期钩子page:start/finish(导航)、vue:error(错误)、app:mounted(挂载)