Nuxt: 插件系统
最后更新:2026-08-26
Charlie 需要集成 Stripe 支付 SDK 到 MegaShop,还想要全局日志服务。Stripe 只在客户端加载,日志在服务端记录。传统方式要在多个地方手动 import 初始化,Nuxt 3 插件系统让一切自动化——注册一次,全局可用。
1. 你将学到
- plugins/ 目录自动注册与执行时机
- defineNuxtPlugin:provide 注入与 inject 使用
- 服务端/客户端插件:.server.ts / .client.ts 后缀区分
- 插件执行顺序控制
- MegaShop 支付 SDK 与日志插件实战
2. 一个架构师的真实故事
(1) 痛点:第三方库初始化散落各处
Charlie 的 MegaShop 需要集成 Stripe 支付。Alice 在购物车页面加载了 Stripe.js,Bob 在结算页又加载了一次——重复加载,而且有些页面忘了加载导致支付按钮不可用。日志工具也是,每个组件都要手动 import logger。
(2) Nuxt 插件系统的解法
Nuxt 3 插件自动执行,provide 注入全局可用:
TYPESCRIPT
// plugins/stripe.client.ts
export default defineNuxtPlugin(() => {
const stripe = Stripe(config.public.stripePublishableKey)
return { provide: { stripe } }
})
(3) 收益:一次注册全局可用
Stripe 和 Logger 只在插件中初始化一次,所有组件通过 useNuxtApp().$stripe 访问,不再重复加载和手动 import。
3. 插件自动注册
(1) 插件注册与执行时序
sequenceDiagram
participant N as Nuxt App
participant P1 as 01-env.server.ts
participant P2 as 02-stripe.client.ts
participant P3 as 03-logger.ts
participant C as Components
N->>P1: Execute server plugins first
N->>P2: Execute client plugins on hydration
N->>P3: Execute universal plugins
N->>C: Components can use $stripe, $logger
(2) 插件类型与命名
| 命名 | 执行环境 | 用途 |
|---|---|---|
| plugins/xxx.ts | SSR + 客户端 | 通用插件 |
| plugins/xxx.server.ts | 仅服务端 | 数据库/密钥 |
| plugins/xxx.client.ts | 仅客户端 | 浏览器 SDK |
▶ 示例:通用日志插件
TYPESCRIPT
// plugins/logger.ts
export default defineNuxtPlugin(() => {
const logger = {
info: (message: string, data?: any) => {
console.log(`[INFO] ${message}`, data || '')
},
warn: (message: string, data?: any) => {
console.warn(`[WARN] ${message}`, data || '')
},
error: (message: string, data?: any) => {
console.error(`[ERROR] ${message}`, data || '')
}
}
return {
provide: {
logger
}
}
})
输出:
TEXT
📖 仅展示
// 执行成功
4. defineNuxtPlugin 详解
(1) provide / inject 机制
| 角色 | API | 说明 |
|---|---|---|
| 插件提供 | provide: { xxx } |
注入 $xxx 到 NuxtApp |
| 组件使用 | useNuxtApp().$xxx |
访问注入的服务 |
| Composable 使用 | useNuxtApp().$xxx |
Composable 中访问 |
▶ 示例:Stripe 支付插件(客户端)
TYPESCRIPT
// plugins/stripe.client.ts
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const stripe = window.Stripe(config.public.stripePublishableKey)
return {
provide: {
stripe
}
}
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:在组件中使用插件
VUE
<!-- pages/checkout.vue -->
<template>
<div>
<h1>Checkout - ${{ total }} USD</h1>
<div ref="cardElement"></div>
<button @click="processPayment" :disabled="processing">
Pay ${{ total }} USD
</button>
</div>
</template>
<script setup lang="ts">
const { $stripe } = useNuxtApp()
const { $logger } = useNuxtApp()
const cart = useState<any[]>('cart')
const total = computed(() => cart.value.reduce((s, i) => s + i.price * i.quantity, 0))
const processing = ref(false)
async function processPayment() {
processing.value = true
try {
$logger.info('Processing payment', { amount: total.value })
// Use Stripe for payment
const result = await $stripe.confirmCardPayment('{PAYMENT_INTENT_SECRET}')
if (result.error) {
$logger.error('Payment failed', result.error)
} else {
$logger.info('Payment succeeded')
navigateTo('/order/success')
}
} finally {
processing.value = false
}
}
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:服务端数据库插件
TYPESCRIPT
// plugins/database.server.ts
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
// Only runs on server - database connection with private key
const db = createDatabaseConnection(config.databaseUrl)
return {
provide: {
db
}
}
})
输出:
TEXT
📖 仅展示
// 执行成功
5. 服务端/客户端插件区分
▶ 示例:通知插件(客户端,用浏览器 API)
TYPESCRIPT
// plugins/notification.client.ts
export default defineNuxtPlugin(() => {
function requestPermission() {
if ('Notification' in window) {
Notification.requestPermission()
}
}
function send(title: string, body: string) {
if (Notification.permission === 'granted') {
new window.Notification(title, { body, icon: '/favicon.ico' })
}
}
// Auto-request permission on first visit
requestPermission()
return { provide: { notification: { send, requestPermission } } }
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:环境检测插件(通用)
TYPESCRIPT
// plugins/env.ts
export default defineNuxtPlugin(() => {
const isServer = import.meta.server
const isClient = import.meta.client
const isDev = import.meta.dev
return {
provide: {
env: { isServer, isClient, isDev }
}
}
})
输出:
TEXT
📖 仅展示
// 执行成功
(1) 服务端 vs 客户端插件对比
| 维度 | .server.ts | .client.ts | .ts (通用) |
|---|---|---|---|
| SSR 执行 | ✅ | ❌ | ✅ |
| 客户端执行 | ❌ | ✅ | ✅ |
| 浏览器 API | ❌ 不可用 | ✅ 可用 | ⚠️ 需判断 |
| Node.js API | ✅ 可用 | ❌ 不可用 | ⚠️ 需判断 |
| 私密数据 | ✅ 安全 | ❌ 会暴露 | ⚠️ 注意 |
6. 插件执行顺序
(1) 排序规则
| 规则 | 示例 | 执行顺序 |
|---|---|---|
| 按文件名字母排序 | 01-aaa.ts → 02-bbb.ts | 01 先执行 |
| 数字前缀控制 | 01-env.ts → 02-db.ts → 03-api.ts | 按数字顺序 |
| server 先于 client | db.server.ts → stripe.client.ts | SSR 时 server 先 |
| nuxt.config 插件最后 | modules 注入的插件 | 框架插件先执行 |
▶ 示例:控制插件执行顺序
TEXT
📖 仅展示
plugins/
├── 01-runtime-env.ts # First: setup environment
├── 02-database.server.ts # Second: connect database
├── 03-logger.ts # Third: init logger
├── 04-stripe.client.ts # Fourth: init Stripe
└── 05-analytics.client.ts # Fifth: init analytics
输出:
TEXT
📖 仅展示
执行成功
▶ 示例:在插件中使用其他插件
TYPESCRIPT
// plugins/05-analytics.client.ts
export default defineNuxtPlugin((nuxtApp) => {
// Access previously registered plugin
const { $logger } = nuxtApp
$logger.info('Analytics plugin initialized')
const analytics = {
track(event: string, data?: any) {
$logger.info(`Track: ${event}`, data)
// Send to analytics service
},
pageView(path: string) {
$logger.info(`Page view: ${path}`)
}
}
// Auto-track page views
nuxtApp.hook('page:finish', () => {
analytics.pageView(window.location.pathname)
})
return { provide: { analytics } }
})
输出:
TEXT
📖 仅展示
// 执行成功
7. 综合示例:MegaShop 插件体系
TYPESCRIPT
// plugins/01-config.ts - Runtime config helper
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
return {
provide: {
config: {
apiBase: config.public.apiBase,
currency: 'USD',
locale: 'en-US',
maxCartItems: 99
}
}
}
})
TYPESCRIPT
// plugins/02-logger.ts - Structured logging
export default defineNuxtPlugin(() => {
const logger = {
info(msg: string, ctx?: Record<string, any>) {
console.log(JSON.stringify({ level: 'info', msg, ctx, ts: Date.now() }))
},
error(msg: string, ctx?: Record<string, any>) {
console.error(JSON.stringify({ level: 'error', msg, ctx, ts: Date.now() }))
}
}
return { provide: { logger } }
})
TYPESCRIPT
// plugins/03-stripe.client.ts - Payment SDK
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const stripe = window.Stripe(config.public.stripePublishableKey)
return { provide: { stripe } }
})
TYPESCRIPT
// plugins/04-notification.client.ts - Browser notifications
export default defineNuxtPlugin(() => {
const notify = {
send(title: string, body: string) {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body })
}
}
}
return { provide: { notify } }
})
❓ 常见问题
Q 插件和 Composable 有什么区别?
A 插件在应用启动时自动执行一次,用于初始化第三方库。Composable 在组件中按需调用,用于逻辑复用。Stripe 初始化用插件,价格格式化用 Composable。
Q provide 注入的名字为什么有 $ 前缀?
A Nuxt 约定注入的服务加 $ 前缀($stripe/$logger),避免与组件内变量冲突。这是 Nuxt 的命名约定,不是强制要求。
Q 插件里能使用 Pinia Store 吗?
A 可以,但需要注意时机。Pinia 在插件之前初始化,所以在插件中可以使用
useXxxStore()。但建议在 Composable 中操作 Store,插件只做初始化。Q 客户端插件什么时候执行?
A 在客户端水合时执行。SSR 阶段不会运行 .client.ts 插件。页面首次加载时,客户端插件在水合完成后执行。
Q 能不能动态注册插件?
A 不推荐。Nuxt 插件在构建时确定,运行时自动执行。如果需要延迟初始化,在插件内部用懒加载模式(如动态 import)。
Q 第三方 SDK(如 Stripe)加载慢怎么办?
A 在 .client.ts 插件中动态加载 SDK,不阻塞水合:
const Stripe = (await import('@stripe/stripe-js')).default。或用 <script> 标签在 head 中预加载。📖 小节
- plugins/ 目录自动注册,按文件名排序执行
- defineNuxtPlugin + provide 注入全局服务,组件通过 useNuxtApp().$xxx 访问
- .server.ts / .client.ts 后缀区分执行环境,避免浏览器/Node API 冲突
- 数字前缀控制执行顺序,确保依赖关系正确
- MegaShop 用插件集成 Stripe 支付、Logger、Notification 等第三方服务
📝 作业
- 基础题(难度⭐):创建一个 logger 插件,在组件中用
useNuxtApp().$logger.info()记录日志 - 进阶题(难度⭐⭐):创建 Stripe 客户端插件 + 数据库服务端插件,验证两者在不同环境执行
- 挑战题(难度⭐⭐⭐):创建 analytics 插件,利用
nuxtApp.hook('page:finish')自动追踪页面浏览,并与 logger 插件协作记录
---|