模块开发
本文聚焦于开发可发布到 npm 的 Nuxt 模块
如果你只需要项目内使用,请参阅《本地模块》。两者核心 API 相同,但发布模块需要额外的构建配置、测试和发布流程。
什么时候需要开发 npm 模块?
- 你的功能需要跨项目复用
- 你想分享给社区使用
- 你需要版本化管理和独立迭代
如果只是单个项目内复用,本地模块就够了,不需要走完整的发布流程。
模块结构
text
my-module/
├── src/
│ ├── module.ts # 模块定义(核心)
│ └── runtime/
│ ├── plugin.ts # 运行时插件
│ ├── composables/ # 运行时组合式函数
│ └── components/ # 运行时组件
├── playground/ # 测试项目
│ ├── app/
│ │ └── pages/
│ └── nuxt.config.ts
├── package.json
├── tsconfig.json
└── build.config.ts # 构建配置为什么有 src/ 和 runtime/ 的区分?
src/module.ts 是构建时代码,在 Nuxt 构建过程中执行;runtime/ 是运行时代码,在浏览器/服务器中执行。构建产物只会包含 module.ts 的编译结果和对 runtime/ 文件的引用。
defineNuxtModule
ts
// src/module.ts
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
// 定义模块选项的类型
export interface ModuleOptions {
enabled: boolean
prefix: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'my-module',
configKey: 'myModule',
// 声明兼容的 Nuxt 版本,版本不匹配时给出警告
compatibility: {
nuxt: '^3.0.0 || ^4.0.0',
},
},
// 默认配置——会被用户配置覆盖
defaults: {
enabled: true,
prefix: 'My',
},
// 模块设置(构建时执行)
setup(options, nuxt) {
// options:合并后的最终配置(defaults + 用户配置)
// nuxt:Nuxt 实例,可以修改配置、注册钩子
const { resolve } = createResolver(import.meta.url)
// 1. 添加插件
addPlugin(resolve('./runtime/plugin'))
// 2. 添加自动导入目录
nuxt.hook('imports:dirs', (dirs) => {
dirs.push(resolve('./runtime/composables'))
})
// 3. 添加组件(带前缀)
nuxt.hook('components:dirs', (dirs) => {
dirs.push({
path: resolve('./runtime/components'),
prefix: options.prefix, // 组件名前缀,如 MyButton
})
})
// 4. 添加服务端工具
nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.autoImports?.push({
from: resolve('./runtime/server/utils'),
imports: ['myServerUtil'],
})
})
// 5. 添加类型声明
nuxt.hook('prepare:types', ({ references }) => {
references.push({
path: resolve('./runtime/types/index.d.ts'),
})
})
},
})options 合并机制
ts
// 模块定义的 defaults
defaults: { enabled: true, prefix: 'My' }
// 用户在 nuxt.config.ts 中的配置
myModule: { prefix: 'Custom' }
// 最终 options
{ enabled: true, prefix: 'Custom' } // defaults 被用户配置覆盖常用 Kit 工具
| 函数 | 说明 | 典型用法 |
|---|---|---|
defineNuxtModule | 定义模块 | 每个模块必须使用 |
addPlugin | 添加插件 | addPlugin(resolve('./runtime/plugin')) |
addLayout | 添加布局 | addLayout({ src: resolve('./layout.vue'), filename: 'custom.vue' }) |
addComponent | 添加单个组件 | addComponent({ name: 'MyButton', filePath: resolve('./Button.vue') }) |
addComposable | 添加组合式函数 | addComposable(resolve('./runtime/useFeature')) |
addServerPlugin | 添加服务端插件 | addServerPlugin(resolve('./runtime/server/plugin')) |
addServerHandler | 添加服务端路由 | addServerHandler({ route: '/api/health', handler: resolve('./runtime/server/health') }) |
createResolver | 创建路径解析器 | const { resolve } = createResolver(import.meta.url) |
addTemplate | 添加模板文件 | 生成虚拟模块 |
addTypeTemplate | 添加类型模板 | 生成 .d.ts 类型文件 |
TIP
addPlugin/addComponent 是 nuxt.hook 的快捷方式 功能等价但更简洁。例如 addPlugin(resolve('./runtime/plugin')) 等同于 nuxt.hook('plugins:dirs', dirs => dirs.push(...))
构建配置
为什么需要构建?
模块源码是 TypeScript,npm 包需要编译为 JavaScript。@nuxt/module-builder 封装了 unbuild,自动处理 Nuxt 模块的构建需求。
bash
npm install -D @nuxt/module-builderts
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/module',
],
externals: ['vue', '@nuxt/kit', 'nuxt'],
})json
// package.json scripts
{
"scripts": {
"dev": "nuxi dev playground",
"build": "nuxt-module-build",
"prepack": "npm run build",
"release": "npm run build && npm publish"
}
}package.json
json
{
"name": "@myorg/nuxt-module",
"version": "1.0.0",
"type": "module",
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/module.mjs",
"types": "./dist/types.d.ts"
}
},
"files": ["dist"],
"nuxt": {
"configKey": "myModule"
},
"devDependencies": {
"@nuxt/kit": "^3.0.0",
"@nuxt/module-builder": "^0.8.0",
"nuxt": "^3.0.0"
},
"peerDependencies": {
"nuxt": "^3.0.0 || ^4.0.0"
}
}关键字段说明:
| 字段 | 说明 |
|---|---|
main / exports | 指向构建产物,Nuxt 通过此入口加载模块 |
types | TypeScript 类型文件,IDE 自动补全依赖它 |
files | 只发布 dist/ 目录,避免发布源码和测试文件 |
nuxt.configKey | 告诉 Nuxt 用户的配置键名,用于自动合并 |
peerDependencies | 声明兼容的 Nuxt 版本,避免安装不兼容的版本 |
命名规范
npm 模块推荐用 @org/nuxt-xxx 或 nuxt-xxx 格式,方便在 Nuxt 模块生态中被发现。
测试模块
Playground 开发测试
在 playground/ 目录中创建测试项目:
ts
// playground/nuxt.config.ts
export default defineNuxtConfig({
modules: ['../../src/module'],
myModule: {
enabled: true,
},
})bash
# 在模块根目录运行,自动使用 playground 项目
npm run dev单元测试
bash
npm install -D @nuxt/test-utils vitestts
// tests/module.spec.ts
import { describe, it, expect } from 'vitest'
import { setupTest } from '@nuxt/test-utils'
describe('my-module', () => {
const ctx = setupTest({
rootDir: './playground',
})
it('registers plugin', async () => {
const html = await ctx.$fetch('/')
expect(html).toContain('my-module-plugin')
})
})发布流程
bash
# 1. 确保构建成功
npm run build
# 2. 本地测试(在 playground 中验证)
npm run dev
# 3. 版本号更新(遵循 semver)
npm version patch # 1.0.0 → 1.0.1(修复 Bug)
npm version minor # 1.0.0 → 1.1.0(新增功能,向后兼容)
npm version major # 1.0.0 → 2.0.0(破坏性变更)
# 4. 发布
npm publish
# 首次发布 scoped 包需要加 --access public
npm publish --access publicINFO
️ 发布前检查清单:
compatibility中声明了 Nuxt 版本范围package.json的files只包含dist- TypeScript 类型完整导出
- README 中有安装和使用说明
- 在不同 Nuxt 版本中测试过兼容性
最佳实践
- 始终声明
compatibility——避免用户安装不兼容的版本 - 为配置提供完整 TypeScript 类型——
ModuleOptions接口 + 泛型 - 使用
playground/开发时测试——比发布后调试效率高得多 - 运行时代码放在
runtime/中——构建时代码不应出现在客户端包中 - 模块应该是声明式的——
setup中避免耗时操作和副作用 - 遵循命名约定——
@org/nuxt-xxx或nuxt-xxx - 提供
configKey——让用户可以在nuxt.config.ts中直接配置
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 发布后安装报错找不到模块 | exports 配置不正确 | 确保 main 和 exports 指向构建产物 |
| TypeScript 类型不生效 | 未发布类型文件 | 检查 types 字段和 files 是否包含类型文件 |
构建后 createResolver 路径错误 | 构建后 import.meta.url 指向 dist/ | 确保构建配置正确处理路径映射 |
| playground 无法加载模块 | modules 路径不正确 | 使用相对路径 '../../src/module' |
| CI 环境报错但本地正常 | 路径大小写敏感问题(Linux 区分大小写) | 确保所有 import 路径大小写一致 |