自定义 Nuxt Module 开发
当官方模块和社区模块无法满足需求时,你需要自己写一个。Nuxt Module 开发门槛并不高——核心就是一个
setup()函数加上@nuxt/kit提供的工具函数。本章从模块的运行机制讲起,深入 Kit API 的设计哲学,最终完成一个可复用模块的封装与发布。
1. 模块的运行机制
1.1 模块的本质
一个最小的 Nuxt Module 就是一个函数:
// modules/my-module.ts
export default defineNuxtModule({
meta: {
name: 'my-module',
configKey: 'myModule',
},
defaults: {
enabled: true,
},
setup(options, nuxt) {
// 这里是模块的全部逻辑
console.log('Module setup!', options)
},
})setup() 函数在 Nuxt 初始化阶段执行(nuxi dev 或 nuxi build 时),接收两个参数:
options:模块配置(用户在nuxt.config.ts中通过configKey传入,与defaults合并)nuxt:Nuxt 实例,可以读取/修改 Nuxt 配置和 Hook 进生命周期
1.2 模块生命周期
nuxi dev / nuxi build
↓
解析 nuxt.config.ts
↓
按顺序加载 modules 数组中的模块
↓ 每个模块依次执行
├── 合并 defaults + 用户配置 → options
├── 执行 setup(options, nuxt)
│ ├── 注册组件、composables、plugins
│ ├── 添加 server routes
│ ├── 注入 Vite 插件
│ └── Hook 进 Nuxt 生命周期
└── 下一个模块
↓
所有模块加载完毕
↓
Nuxt 构建/启动开发服务器
关键时序:模块的 setup() 在 Vite 启动之前执行,所以你可以在 setup() 中修改 Vite 配置;但 setup() 执行时页面组件还没有被解析,你不能在 setup() 中直接操作组件树。
1.3 本地模块 vs npm 模块
| 方式 | 注册方式 | 适用场景 |
|---|---|---|
| 本地模块 | modules: ['~/modules/my-module'] | 项目特定逻辑 |
| npm 模块 | modules: ['my-nuxt-module'] | 跨项目复用 |
建议从本地模块开始开发和测试,功能稳定后再提取为 npm 包。
2. @nuxt/kit:模块开发工具箱
2.1 Kit 的设计哲学
@nuxt/kit 提供了一组工具函数,让模块开发者不需要直接操作 Nuxt 内部数据结构。每个 Kit 函数都做了两件事:
- 正确地修改 Nuxt 配置(处理路径解析、去重、合并等细节)
- 保持与 Nuxt 版本的兼容(内部实现可能随版本变化,但 Kit API 保持稳定)
2.2 核心 Kit 函数
注入组件:
import { addComponent } from '@nuxt/kit'
// 注册一个全局可用的组件
addComponent({
name: 'VideoPlayer',
filePath: resolver.resolve('./runtime/components/VideoPlayer.vue'),
})注册后,<VideoPlayer> 在任何页面和组件中都可以直接使用,不需要 import。
注入 Composables:
import { addImports, addImportsDir } from '@nuxt/kit'
// 注册单个 composable
addImports({
name: 'useVideoPlayer',
from: resolver.resolve('./runtime/composables/useVideoPlayer'),
})
// 批量注册整个目录
addImportsDir(resolver.resolve('./runtime/composables'))注入 Server Routes:
import { addServerHandler } from '@nuxt/kit'
addServerHandler({
route: '/api/player/analytics',
handler: resolver.resolve('./runtime/server/api/analytics.post'),
})注入 Plugin:
import { addPlugin } from '@nuxt/kit'
addPlugin({
src: resolver.resolve('./runtime/plugins/player.client'),
mode: 'client', // 仅客户端
})修改 Nuxt 配置:
// 添加 CSS
nuxt.options.css.push(resolver.resolve('./runtime/styles/player.css'))
// 添加 Vite 插件
nuxt.options.vite.plugins = nuxt.options.vite.plugins || []
nuxt.options.vite.plugins.push(myVitePlugin())
// 修改运行时配置
nuxt.options.runtimeConfig.public.playerVersion = '1.0.0'生成运行时代码:
import { addTemplate } from '@nuxt/kit'
addTemplate({
filename: 'player-config.mjs',
getContents: () => `export const config = ${JSON.stringify(options)}`,
})addTemplate 生成的文件在 .nuxt/ 目录中,可以被运行时代码 import。这是将构建时配置传递到运行时的桥梁。
2.3 createResolver
模块中引用文件时,路径必须是绝对路径。createResolver 简化了这个过程:
import { createResolver } from '@nuxt/kit'
const { resolve } = createResolver(import.meta.url)
// resolve('./runtime/components/X.vue')
// → 模块目录的绝对路径 + /runtime/components/X.vue3. Nuxt Hooks:介入框架生命周期
3.1 常用 Hooks
setup(options, nuxt) {
// 所有模块加载完毕后
nuxt.hook('modules:done', () => {
console.log('All modules loaded')
})
// Nitro 配置阶段(修改 server 配置)
nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.routeRules = nitroConfig.routeRules || {}
nitroConfig.routeRules['/api/player/**'] = { cors: true }
})
// 构建完成后
nuxt.hook('build:done', () => {
console.log('Build complete!')
})
// 开发模式下页面变化
nuxt.hook('pages:extend', (pages) => {
// 动态添加或修改页面路由
})
// Vite 配置阶段
nuxt.hook('vite:extendConfig', (viteConfig) => {
// 修改 Vite 配置
})
}3.2 Hook 的执行时机
| Hook | 时机 | 典型用途 |
|---|---|---|
modules:done | 所有模块 setup 完毕 | 读取其他模块注册的配置 |
pages:extend | 页面路由解析后 | 动态添加/修改路由 |
components:extend | 组件扫描后 | 修改组件注册信息 |
nitro:config | Nitro 配置阶段 | 修改 server 配置 |
vite:extendConfig | Vite 配置阶段 | 注入 Vite 插件 |
build:before | 构建开始前 | 预处理 |
build:done | 构建完成后 | 后处理、统计 |
close | Nuxt 实例关闭 | 清理资源 |
4. 实战:视频播放器模块
4.1 需求分析
将 AI 视频平台的播放器功能封装为可复用 Nuxt Module:
- 提供
<VideoPlayer>组件(支持 HLS/DASH 自适应流) - 提供
useVideoPlayer()composable(播放控制 API) - 注入播放器分析 API(
/api/player/analytics) - 支持配置(CDN 前缀、默认画质、自动播放策略)
4.2 目录结构
modules/video-player/
├── runtime/
│ ├── components/
│ │ └── VideoPlayer.vue
│ ├── composables/
│ │ └── useVideoPlayer.ts
│ ├── server/
│ │ └── api/
│ │ └── analytics.post.ts
│ ├── plugins/
│ │ └── hls-loader.client.ts
│ └── types.ts
├── module.ts ← 模块入口
└── package.json ← npm 包配置(如果要发布)
runtime 目录约定:runtime/ 下的代码在浏览器/服务端运行(运行时),module.ts 在 Node.js 构建阶段运行(构建时)。这是 Nuxt Module 的核心分层——不要在 module.ts 中 import runtime/ 的代码(除了作为路径字符串传给 Kit 函数)。
4.3 模块入口
// modules/video-player/module.ts
import { defineNuxtModule, addComponent, addImports, addServerHandler, addPlugin, createResolver } from '@nuxt/kit'
export interface ModuleOptions {
cdnPrefix?: string
defaultQuality?: 'auto' | '1080p' | '720p' | '480p'
autoplay?: boolean
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'nuxt-video-player',
configKey: 'videoPlayer',
compatibility: { nuxt: '>=3.0.0' },
},
defaults: {
cdnPrefix: '',
defaultQuality: 'auto',
autoplay: false,
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// 将配置传递到运行时
nuxt.options.runtimeConfig.public.videoPlayer = options
// 注册组件
addComponent({
name: 'VideoPlayer',
filePath: resolve('./runtime/components/VideoPlayer.vue'),
})
// 注册 composable
addImports({
name: 'useVideoPlayer',
from: resolve('./runtime/composables/useVideoPlayer'),
})
// 注册 server API
addServerHandler({
route: '/api/player/analytics',
handler: resolve('./runtime/server/api/analytics.post'),
})
// 客户端 HLS.js 加载插件
addPlugin({
src: resolve('./runtime/plugins/hls-loader.client'),
mode: 'client',
})
},
})4.4 运行时配置传递
setup() 中设置 runtimeConfig.public.videoPlayer = options,运行时代码通过 useRuntimeConfig() 读取:
// runtime/composables/useVideoPlayer.ts
export function useVideoPlayer() {
const config = useRuntimeConfig().public.videoPlayer
// config.cdnPrefix, config.defaultQuality, config.autoplay
}这是构建时配置 → 运行时代码的标准传递方式。
5. 模块测试
5.1 单元测试
import { describe, it, expect } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils/e2e'
describe('video-player module', async () => {
await setup({
rootDir: './test/fixtures/basic',
server: true,
})
it('registers VideoPlayer component', async () => {
const html = await $fetch('/')
expect(html).toContain('video-player')
})
it('registers analytics API', async () => {
const res = await $fetch('/api/player/analytics', {
method: 'POST',
body: { videoId: '1', event: 'play' },
})
expect(res).toBeDefined()
})
})test/fixtures/basic/ 是一个最小的 Nuxt 项目,只包含 nuxt.config.ts(引用你的模块)和一个使用 <VideoPlayer> 的页面。
5.2 Playground
开发阶段用 playground/ 目录作为实时测试环境:
modules/video-player/
├── playground/
│ ├── app.vue ← 使用 <VideoPlayer> 的页面
│ └── nuxt.config.ts ← extends: '../'
├── runtime/
├── module.ts
└── nuxt.config.ts ← 声明模块入口
cd playground && nuxi dev 即可实时看到模块效果。
6. 模块发布
6.1 package.json 配置
{
"name": "nuxt-video-player",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
}
},
"main": "./dist/module.cjs",
"module": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"files": ["dist"],
"scripts": {
"prepack": "nuxt-module-build build"
}
}6.2 nuxt-module-build
@nuxt/module-builder 是官方的模块构建工具,处理 TypeScript 编译、类型生成、runtime 文件复制:
npx nuxt-module-build build构建后 dist/ 目录包含编译好的模块代码和类型定义,可以发布到 npm。
6.3 发布到 npm
npm login
npm publish发布后,其他项目通过 npm install nuxt-video-player 安装,在 nuxt.config.ts 中添加 modules: ['nuxt-video-player'] 即可使用。
7. 模块开发最佳实践
7.1 设计原则
- 零配置可用:
defaults提供合理默认值,用户不配置也能正常工作 - 渐进式配置:简单场景用默认值,复杂场景通过
configKey精细调整 - 最小侵入:不要修改用户没有明确授权的配置。比如不要擅自修改
vite.css配置 - TypeScript 优先:导出完整类型定义,让用户在
nuxt.config.ts中有自动补全
7.2 常见陷阱
- 构建时 import 运行时代码:
module.ts中import了runtime/的代码会导致构建错误。只传路径字符串给 Kit 函数。 - 忘记 createResolver:硬编码相对路径在 npm 发布后会失效,必须用
createResolver解析绝对路径。 - 配置合并冲突:修改
nuxt.options时用 push/merge 而非直接赋值,避免覆盖其他模块的配置。 - 模块间依赖:如果模块 A 依赖模块 B 的注入,用
installModule('module-b')确保 B 先加载,或在modules:doneHook 中读取 B 的配置。
本章小结
- 模块是构建阶段的 setup 函数,通过
@nuxt/kit工具函数注入组件、composables、plugins、server routes - runtime vs module:
runtime/是运行时代码(浏览器/服务端),module.ts是构建时代码(Node.js),不能交叉 import - Kit 核心 API:
addComponent、addImports、addServerHandler、addPlugin、addTemplate、createResolver - Hooks:
nuxt.hook()介入 Nuxt 生命周期,在关键阶段修改配置或执行逻辑 - runtimeConfig:构建时配置传递到运行时的标准桥梁
- 发布流程:
nuxt-module-build构建 → npm publish,零配置可用 + 渐进式配置