插件
约 2068 字大约 7 分钟
2026-01-29
重要
Nuxt 插件用于在应用启动阶段扩展 Nuxt 运行时(NuxtApp)和 Vue 应用实例(vueApp) 它最常见的用途是:
- 注入全局能力(如
$api、$tracker) - 注册 Vue 全局指令/插件
- 监听 Nuxt 生命周期钩子,做统一逻辑处理
- 注册第三方 SDK(如埋点、监控、国际化)
最佳实践
- 单一职责:一个插件只做一类事情
- 显式依赖:有先后关系时用
name+dependsOn - 区分端环境:依赖
window/document的逻辑放.client.ts;服务端的逻辑放.server.ts - 保持轻量:插件初始化不要做重计算或阻塞 IO,避免拖慢首屏
- 统一类型:所有
provide注入能力都补充types/nuxt.d.ts,并优先使用精确类型 - 选项式字段保持静态:
enforce、dependsOn、env不要写运行时分支 - 优先 composable:局部复用逻辑优先 composable,避免插件泛滥
核心概念
defineNuxtPlugin
插件本质是一个由 defineNuxtPlugin 定义的函数(或对象),在应用初始化时执行
export default defineNuxtPlugin((nuxtApp) => {
// 1) 在这里完成初始化逻辑
// 2) 通过 provide 注入全局能力
const hello = (name: string) => `Hello, ${name}!`
return {
provide: {
// 注意:这里写 hello,最终访问名是 $hello
hello,
},
}
})NuxtApp
nuxtApp 是 defineNuxtPlugin 回调函数的唯一参数(对象语法中 setup 的参数),用于获取当前 Nuxt 应用的运行时上下文
常用于
nuxtApp.provide(name, value):手动注入全局属性nuxtApp.hook(name, fn):注册运行时钩子nuxtApp.vueApp:访问 Vue 应用实例,可注册全局插件/指令/组件nuxtApp.ssrContext:仅服务端可用的 SSR 上下文
定义插件
定义插件有两种写法
export default defineNuxtPlugin((nuxtApp) => {
// 使用 hook 监听生命周期
nuxtApp.hook('app:mounted', () => {
console.log('Nuxt app mounted')
})
// 注入全局方法
return {
provide: {
hello: (msg: string) => `Hello ${msg}!`,
},
}
})export default defineNuxtPlugin({
name: 'hello-plugin', // 插件唯一名(便于 dependsOn)
enforce: 'default',
hooks: {
// 声明式注册 hooks
'app:mounted': () => {
console.log('Nuxt app mounted')
},
},
setup() {
return {
provide: {
hello: (msg: string) => `Hello ${msg}!`,
},
}
},
})注册与加载机制
常见的 plugin 文件结构
app
plugins
01.core.ts 自动注册
analytics.client.ts 仅客户端执行
auth.server.ts 仅服务端执行
nested
foo.ts 默认不自动注册
index.ts 当前仍会被扫描,但已不推荐
重要
选项式插件(对象写法)的 name / enforce / dependsOn / hooks / env 等字段会被静态分析,因此这些字段应保持静态常量,不建议写成运行时表达式
自动注册规则
位于 app/plugins 目录下的插件文件会被自动注册,如果插件文件在嵌套目录中,需要在 nuxt.config.ts 中显示声明
export default defineNuxtConfig({
plugins: [
'~/app/plugins/nested/foo',
],
})执行顺序控制
- pre: 在其他插件之前运行
- default(默认值): 按默认顺序运行
- post: 在其他插件之后运行
控制插件的注册顺序常用四种方式:使用文件名前缀、enforce、dependsOn、order
export default defineNuxtPlugin({
name: 'core',
enforce: 'pre', // 先执行
setup() {
return {
provide: {
coreReady: true,
},
}
},
})export default defineNuxtPlugin({
name: 'auth',
dependsOn: ['core'], // 确保在 core 后执行
setup(nuxtApp) {
console.log('core ready:', nuxtApp.$coreReady)
},
})export default defineNuxtPlugin({
name: 'logger',
order: 10, // 在默认组内较早执行
setup() {
console.log('Logger 初始化')
}
})串行与并行
异步插件默认串行执行;当插件互不依赖时可开启 parallel。
export default defineNuxtPlugin({
name: 'analytics',
parallel: true, // 仅在和其它插件无依赖时使用
async setup() {
await initAnalytics()
},
})插件类型声明
定义插件类型通过文件导出类型,再在 d.ts 里复用,这样做的优点是 "类型单一来源" 即便是插件 API 改动后,声明文件不用重复改签名
export interface ApiClient {
get<TData = unknown>(url: string): Promise<TData>
post<TData = unknown, TBody = unknown>(url: string, body?: TBody): Promise<TData>
}import type { ApiClient } from '~/app/plugins/01.api'
declare module '#app' {
interface NuxtApp {
$api: ApiClient
}
}
// 让模板或者 this.$api 这种方式也有类型提示
declare module 'vue' {
interface ComponentCustomProperties {
$api: ApiClient
}
}
export {}信息
#app 是 Nuxt 提供的虚拟模块(virtual module),主要用于暴露 Nuxt 运行时相关的类型和接口(比如 NuxtApp)
示例:封装 API + 页面埋点
准备目录结构
nuxt.config.ts
app
plugins
01.api.ts
10.analytics.client.ts
pages
profile.vue
types
nuxt.d.ts
配置
runtimeConfig在
nuxt.config.ts中定义 API 地址、超时配置(客户端可读)和服务端私有配置。nuxt.config.tsexport default defineNuxtConfig({ runtimeConfig: { // 服务端私有配置(客户端不可读) apiSecret: '', public: { // 给客户端使用的 API 地址 apiBase: 'https://api.example.com', // 统一请求超时时间(毫秒) apiTimeout: 10_000, }, }, })使用回调函数写法创建通用 API 插件并注入
$api这里实现一个可复用的完整封装:统一请求方法、自动鉴权、业务错误处理、HTTP 错误处理、文件上传能力。
app/plugins/01.api.tsimport type { FetchOptions } from 'ofetch' type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' export interface ApiResult<T = unknown> { code: number message: string data: T } export interface ApiRequestOptions<TBody = unknown> extends Omit<FetchOptions<'json'>, 'method' | 'body'> { method?: HttpMethod body?: TBody auth?: boolean } export interface ApiClient { request<TData = unknown, TBody = unknown>( url: string, options?: ApiRequestOptions<TBody>, ): Promise<TData> get<TData = unknown>( url: string, options?: Omit<ApiRequestOptions, 'method' | 'body'>, ): Promise<TData> post<TData = unknown, TBody = unknown>( url: string, body?: TBody, options?: Omit<ApiRequestOptions<TBody>, 'method' | 'body'>, ): Promise<TData> put<TData = unknown, TBody = unknown>( url: string, body?: TBody, options?: Omit<ApiRequestOptions<TBody>, 'method' | 'body'>, ): Promise<TData> patch<TData = unknown, TBody = unknown>( url: string, body?: TBody, options?: Omit<ApiRequestOptions<TBody>, 'method' | 'body'>, ): Promise<TData> delete<TData = unknown>( url: string, options?: Omit<ApiRequestOptions, 'method' | 'body'>, ): Promise<TData> upload<TData = unknown>( url: string, file: File, fieldName?: string, options?: Omit<ApiRequestOptions<FormData>, 'method' | 'body'>, ): Promise<TData> } export default defineNuxtPlugin(() => { const config = useRuntimeConfig() const token = useCookie<string | null>('token') const timeout = Number(config.public.apiTimeout || 10_000) // 原始请求实例:只负责传输层能力 const http = $fetch.create({ baseURL: config.public.apiBase, timeout, retry: 0, }) // 统一入口:处理鉴权、业务错误、HTTP 错误 const request = async <TData = unknown, TBody = unknown>( url: string, options: ApiRequestOptions<TBody> = {}, ): Promise<TData> => { const { auth = true, headers, ...rest } = options const requestHeaders = new Headers(headers) if (auth && token.value) { requestHeaders.set('Authorization', `Bearer ${token.value}`) } try { const res = await http<ApiResult<TData> | TData>(url, { ...rest, headers: requestHeaders, method: rest.method || 'GET', body: rest.body as BodyInit | Record<string, unknown> | undefined, }) // 兼容两种后端返回: // 1) 包装结构:{ code, message, data } // 2) 直接返回 data if (res && typeof res === 'object' && 'code' in res) { const result = res as ApiResult<TData> if (result.code !== 0) { throw createError({ statusCode: 400, statusMessage: result.message || '业务请求失败', data: result, }) } return result.data } return res as TData } catch (error) { const e = error as { statusCode?: number statusMessage?: string data?: { message?: string } } if (e.statusCode === 401 && import.meta.client) { await navigateTo('/login') } throw createError({ statusCode: e.statusCode || 500, statusMessage: e.data?.message || e.statusMessage || '网络请求失败', data: e.data, }) } } const api: ApiClient = { request, get: (url, options) => request(url, { ...options, method: 'GET' }), post: (url, body, options) => request(url, { ...options, method: 'POST', body }), put: (url, body, options) => request(url, { ...options, method: 'PUT', body }), patch: (url, body, options) => request(url, { ...options, method: 'PATCH', body }), delete: (url, options) => request(url, { ...options, method: 'DELETE' }), upload: (url, file, fieldName = 'file', options = {}) => { const formData = new FormData() formData.append(fieldName, file) return request(url, { ...options, method: 'POST', body: formData }) }, } return { provide: { api, }, } })创建页面埋点插件
该插件只在客户端执行,并通过文件名前缀(
01<10)保证在 API 插件之后执行。app/plugins/10.analytics.client.tsexport default defineNuxtPlugin({ name: 'analytics', setup(nuxtApp) { // 仅客户端插件:监听页面切换,发送埋点 nuxtApp.hook('page:finish', () => { const route = useRoute() console.log('[analytics] page view:', route.fullPath) // 可以在这里调用真实的埋点 SDK }) }, })补充注入类型声明
让
useNuxtApp().$api在 TS 下具备完整类型提示;这里直接复用插件里导出的ApiClient类型。types/nuxt.d.tsimport type { ApiClient } from '~/app/plugins/01.api' declare module '#app' { interface NuxtApp { $api: ApiClient } } declare module 'vue' { interface ComponentCustomProperties { $api: ApiClient } } export {}在页面中使用
$api拉取数据页面通过
useAsyncData拉取数据,天然兼容 Nuxt 的 SSR/CSR 数据流。app/pages/profile.vue<script setup lang="ts"> interface UserProfile { id: number nickname: string email: string } const { $api } = useNuxtApp() // 服务端首屏 + 客户端切换时都会按 Nuxt 规则请求 const { data, pending, error, refresh } = await useAsyncData('profile', () => $api.get<UserProfile>('/user/profile'), ) </script> <template> <section> <h1>个人信息</h1> <p v-if="pending">加载中...</p> <p v-else-if="error">加载失败,请重试</p> <pre v-else>{{ data }}</pre> <button @click="refresh">刷新</button> </section> </template>
