Nuxt: 模块开发
最后更新:2026-08-26
Charlie 发现 MegaShop 的埋点逻辑、错误上报、性能监控散落在各处。Alice 和 Bob 的其他项目也需要同样的能力。把这些通用功能封装成 Nuxt 模块,一次开发处处复用,还能发布到 npm 给社区用。
1. 你将学到
- 模块架构:defineNuxtModule() + installModule() + 生命周期钩子
- 模块能力:注入组件/Composable/插件/中间件/server 路由/配置
- 模块发布:npm 包打包 + TypeScript 类型
- 模块测试:@nuxt/test-utils + fixtures
- MegaShop @megashop/analytics 模块实战
2. 一个架构师的真实故事
(1) 痛点:通用功能重复开发
Charlie 的 MegaShop 需要埋点——每次页面浏览、每次加购、每次点击都要记录。他在 5 个组件里手动调 API。Alice 的另一个项目也需要埋点,Bob 重新写了一遍。代码重复且不一致。
(2) Nuxt 模块的解法
封装为模块后,安装即用——自动注入 Composable 和 server API:
TYPESCRIPT
// nuxt.config.ts
modules: ['@megashop/analytics']
(3) 收益:一次开发多处复用
所有项目安装模块即可获得埋点能力,Alice 在任何项目用 useAnalytics() 就能埋点,零配置。
3. 模块架构
(1) Nuxt 模块生命周期
graph TB
A[defineNuxtModule] --> B[Setup Function]
B --> C[installModule - Dependencies]
B --> D[addPlugin - Register Plugins]
B --> E[addComposable - Inject Composables]
B --> F[addServerHandler - API Routes]
B --> G[addLayout - Custom Layouts]
B --> H[addComponent - Auto Components]
B --> I[extendConfig - Modify Config]
J[Nuxt Hooks] --> K[modules:before]
J --> L[modules:done]
J --> M[build:before]
J --> N[build:done]
▶ 示例:最小模块骨架
TYPESCRIPT
// src/module.ts
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
meta: {
name: '@megashop/analytics',
configKey: 'analytics',
compatibility: {
nuxt: '^3.0.0'
}
},
defaults: {
enabled: true,
endpoint: '/api/analytics',
debug: false
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// Register plugin
addPlugin(resolve('./runtime/plugin'))
// Expose options to runtime
nuxt.options.runtimeConfig.public.analytics = {
enabled: options.enabled,
endpoint: options.endpoint,
debug: options.debug
}
}
})
输出:
TEXT
📖 仅展示
// 执行成功
(2) 模块能力速查
| 能力 | API | 说明 |
|---|---|---|
| 注册插件 | addPlugin() | 自动执行初始化逻辑 |
| 注入组件 | addComponent() | 自动导入 Vue 组件 |
| 注入 Composable | addImports() | 自动导入函数 |
| 添加 API 路由 | addServerHandler() | 自动注册 server 路由 |
| 添加布局 | addLayout() | 注册自定义布局 |
| 添加中间件 | addRouteMiddleware() | 注册路由中间件 |
| 修改配置 | extendConfig() | 修改 Nuxt 配置 |
| 安装依赖模块 | installModule() | 安装其他模块 |
4. 模块实战:@megashop/analytics
▶ 示例:模块入口
TYPESCRIPT
// src/module.ts
import { defineNuxtModule, addPlugin, addImports, addServerHandler, createResolver } from '@nuxt/kit'
export interface ModuleOptions {
enabled: boolean
endpoint: string
debug: boolean
trackPageViews: boolean
trackClicks: boolean
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@megashop/analytics',
configKey: 'analytics',
compatibility: { nuxt: '^3.0.0' }
},
defaults: {
enabled: true,
endpoint: '/api/analytics',
debug: false,
trackPageViews: true,
trackClicks: true
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// 1. Register client plugin for auto-tracking
if (options.trackPageViews || options.trackClicks) {
addPlugin(resolve('./runtime/plugin.client'))
}
// 2. Auto-import useAnalytics composable
addImports({
name: 'useAnalytics',
from: resolve('./runtime/composables/useAnalytics')
})
// 3. Add server API for receiving events
addServerHandler({
method: 'post',
route: options.endpoint,
handler: resolve('./runtime/server/api/analytics')
})
// 4. Expose config to runtime
nuxt.options.runtimeConfig.public.analytics = {
enabled: options.enabled,
endpoint: options.endpoint,
debug: options.debug
}
}
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Runtime Plugin
TYPESCRIPT
// src/runtime/plugin.client.ts
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig().public.analytics
if (!config.enabled) return
// Auto-track page views
if (config.trackPageViews) {
nuxtApp.hook('page:finish', () => {
useAnalytics().trackPageView(window.location.pathname)
})
}
// Auto-track clicks on data-track elements
if (config.trackClicks) {
document.addEventListener('click', (e) => {
const target = (e.target as HTMLElement).closest('[data-track]')
if (target) {
const event = target.getAttribute('data-track') || 'click'
useAnalytics().track(event, { element: target.tagName })
}
})
}
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Runtime Composable
TYPESCRIPT
// src/runtime/composables/useAnalytics.ts
export function useAnalytics() {
const config = useRuntimeConfig().public.analytics
async function track(event: string, data?: Record<string, any>) {
if (!config.enabled) return
if (config.debug) console.log('[Analytics]', event, data)
await $fetch(config.endpoint, {
method: 'POST',
body: { event, data, timestamp: Date.now(), url: import.meta.client ? window.location.href : '' }
})
}
function trackPageView(path: string) {
track('page_view', { path })
}
function trackAddToCart(productId: number, productName: string, price: number) {
track('add_to_cart', { productId, productName, price, currency: 'USD' })
}
function trackPurchase(orderId: string, total: number) {
track('purchase', { orderId, total, currency: 'USD' })
}
return { track, trackPageView, trackAddToCart, trackPurchase }
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Runtime Server API
TYPESCRIPT
// src/runtime/server/api/analytics.ts
import { defineEventHandler, readBody, setHeader } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate required fields
if (!body.event) {
throw createError({ statusCode: 400, message: 'Event name required' })
}
// Store event (in production: send to analytics service)
const storage = useStorage('analytics')
const key = `event:${Date.now()}:${Math.random().toString(36).slice(2)}`
await storage.setItem(key, {
event: body.event,
data: body.data || {},
timestamp: body.timestamp || Date.now(),
url: body.url,
userAgent: getHeader(event, 'user-agent')
})
setHeader(event, 'cache-control', 'no-store')
return { success: true }
})
输出:
TEXT
📖 仅展示
// 执行成功
5. 模块发布
▶ 示例:package.json 配置
JSON
{
"name": "@megashop/analytics",
"version": "1.0.0",
"type": "module",
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs",
"types": "./dist/types.d.ts"
},
"./runtime/*": "./dist/runtime/*"
},
"files": ["dist"],
"scripts": {
"build": "nuxt-module-build",
"dev": "nuxt-module-build --stub",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"nuxt": "^3.0.0"
},
"devDependencies": {
"@nuxt/module-builder": "^0.6.0",
"@nuxt/test-utils": "^3.0.0",
"nuxt": "^3.12.0"
}
}
输出:
JSON
{
"name": "@megashop/analytics",
"version": "1.0.0",
"type": "module",
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs",
"types": "./dist/types.d.ts"
},
"./runtime/*": "./dist/runtime/*"
},
"files": [
"dist"
],
"scripts": {
"build": "nuxt-module-build",
"dev": "nuxt-module-build --stub",
"test": "vitest run",
"prepublishOnly": "npm run
▶ 示例:在项目中使用
TYPESCRIPT
// nuxt.config.ts of MegaShop
export default defineNuxtConfig({
modules: [
// Local module during development
'~/modules/analytics',
// Published module in production
// '@megashop/analytics'
],
analytics: {
enabled: true,
endpoint: '/api/analytics',
debug: process.env.NODE_ENV === 'development',
trackPageViews: true,
trackClicks: true
}
})
输出:
TEXT
📖 仅展示
// 执行成功
6. 模块测试
▶ 示例:模块测试 fixture
TYPESCRIPT
// test/module.test.ts
import { setupTest } from '@nuxt/test-utils'
describe('@megashop/analytics module', () => {
setupTest({
fixture: './test/fixtures/basic',
build: true
})
test('registers analytics plugin', () => {
// Plugin auto-registers, check Nuxt plugins
const nuxt = useNuxt()
const hasPlugin = nuxt.options.plugins.some(p => p.src?.includes('analytics'))
expect(hasPlugin).toBe(true)
})
test('exposes useAnalytics composable', async () => {
// Auto-import available
const { data } = await useFetch('/api/analytics', {
method: 'POST',
body: { event: 'test', data: {} }
})
expect(data.value).toBeDefined()
})
test('analytics endpoint accepts events', async () => {
const response = await $fetch('/api/analytics', {
method: 'POST',
body: { event: 'page_view', data: { path: '/' } }
})
expect(response.success).toBe(true)
})
})
输出:
TEXT
📖 仅展示
// 执行成功
7. 综合示例:@megashop/analytics 使用
VUE
<!-- pages/products/[id].vue - Using analytics module -->
<template>
<div v-if="product">
<h1>{{ product.name }}</h1>
<button
@click="handleAddToCart"
data-track="add_to_cart"
>
Add to Cart
</button>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
// useAnalytics auto-imported by module
const { trackAddToCart, trackPageView } = useAnalytics()
// Manual tracking
onMounted(() => {
trackPageView(`/products/${route.params.id}`)
})
async function handleAddToCart() {
if (!product.value) return
trackAddToCart(product.value.id, product.value.name, product.value.price)
// ... add to cart logic
}
</script>
❓ 常见问题
Q 模块和插件有什么区别?
A 模块在构建时执行(配置阶段),可以注入插件/组件/Composable/路由。插件在运行时执行(应用启动),做初始化逻辑。模块可以包含插件。
Q 模块里的 runtime 目录是什么?
A runtime 是运行时代码——只在应用运行时执行,不参与构建配置。插件、Composable、server handler 都放在 runtime/ 下,不会被 Nuxt Kit 处理。
Q 本地模块怎么开发调试?
A 在项目 modules/ 目录下创建模块,nuxt.config.ts 引用
~/modules/xxx。开发时用 nuxt-module-build --stub 生成软链接,改代码实时生效。Q 模块能依赖其他模块吗?
A 可以。在 setup 中用
installModule('@pinia/nuxt') 安装依赖模块。Nuxt 会自动去重。Q 发布到 npm 需要什么?
A package.json 配置 exports + types,用 @nuxt/module-builder 构建,npm publish 发布。推荐用 GitHub Actions 自动发布。
Q 模块的 TypeScript 类型怎么导出?
A @nuxt/module-builder 自动生成类型声明。在 package.json 的 exports.types 指向 dist/types.d.ts,用户安装后自动获得类型提示。
📖 小节
- defineNuxtModule 定义模块:meta + defaults + setup 函数
- 模块能力:addPlugin/addImports/addServerHandler/addComponent 等注入一切
- Runtime 代码放在 runtime/ 下,不参与构建配置阶段
- @megashop/analytics 模块:自动埋点 + useAnalytics Composable + server API
- 模块发布用 @nuxt/module-builder 构建,npm publish 发布
📝 作业
- 基础题(难度⭐):创建最小模块骨架,注册一个 plugin 输出 "Module loaded"
- 进阶题(难度⭐⭐):开发 @megashop/analytics 模块,实现 useAnalytics Composable + server API
- 挑战题(难度⭐⭐⭐):添加自动页面浏览追踪 + data-track 点击追踪,编写模块测试验证功能
---|