模块作者指南
Nuxt 的配置和钩子系统使得定制 Nuxt 的每个方面并添加任何集成都成为可能(Vue 插件、CMS、服务器路由、组件、日志记录等)。
Nuxt 模块 是在使用 nuxt dev
启动 Nuxt 开发模式或使用 nuxt build
构建生产项目时,按顺序执行的函数。利用模块,你可以封装、适当测试并作为 npm 包共享自定义解决方案,而无需给项目添加多余的样板代码,也不必修改 Nuxt 本身。
快速开始
我们推荐你使用我们的启动模板来开始创建 Nuxt 模块:
npm create nuxt -- -t module my-module
yarn create nuxt -t module my-module
pnpm create nuxt -t module my-module
bun create nuxt -t module my-module
这将创建一个名为 my-module
的项目,并包含开发和发布模块所需的所有样板代码。
后续步骤:
- 在你选择的 IDE 中打开
my-module
- 使用你喜欢的包管理器安装依赖
- 使用
npm run dev:prepare
准备本地开发文件 - 按照本文档学习更多关于 Nuxt 模块的信息
使用启动模板
了解如何使用模块启动模板执行基本任务。
开发指南
虽然你的模块源代码位于 src
目录中,大多数情况下,开发模块时你需要一个 Nuxt 应用。这正是 playground
目录存在的意义。它是一个 Nuxt 应用,你可以在其中自由试验,并已配置为与你的模块一起运行。
你可以像使用任何 Nuxt 应用一样操作 playground。
- 使用
npm run dev
启动它的开发服务器,修改src
目录中的模块代码时,服务器会自动重载 - 使用
npm run dev:build
构建应用
nuxt
命令都可以针对 playground
目录使用(例如 nuxt <COMMAND> playground
)。你也可以在 package.json
中自定义额外的 dev:*
脚本以方便调用。测试指南
模块启动模板包含一个基础测试套件:
构建指南
Nuxt 模块自带由 @nuxt/module-builder
提供的构建工具。该构建器无需任何配置,支持 TypeScript,并确保你的资源正确打包,以便分发到其他 Nuxt 应用中。
你可以通过运行 npm run prepack
构建模块。
发布指南
npm login
进行了身份验证。你可以简单地通过修改模块版本并使用 npm publish
命令发布模块,但模块启动模板提供了一个发布脚本,帮助你确保发布的是可用的模块版本,并更便捷地完成发布流程。
使用发布脚本前,请提交所有改动(我们建议遵循Conventional Commits,以便自动版本提升和更新变更日志),然后执行 npm run release
。
运行发布脚本时会发生以下操作:
- 首先运行测试套件:
- 执行代码检查 (
npm run lint
) - 执行单元测试、集成测试和端到端测试 (
npm run test
) - 构建模块 (
npm run prepack
)
- 执行代码检查 (
- 如果测试通过,则进行发布:
- 根据 Conventional Commits 自动提升版本号并生成变更日志
- 发布模块到 npm(为此模块将被再次构建,确保更新的版本号在发布产出物中生效)
- 推送代表新版本的 git 标签到远程仓库
package.json
中根据需要自定义默认的 release
脚本。开发模块
Nuxt 模块拥有丰富且强大的 API 和模式,允许它们以几乎任何方式修改 Nuxt 应用。本节将教你如何利用这些能力。
模块结构
我们可以将 Nuxt 模块分为两类:
- 发布的模块:分发于 npm 上 — 你可以在 Nuxt 网站 查看一些社区模块列表。
- "本地" 模块:存在于 Nuxt 项目内部,要么内联于 Nuxt 配置中(内联模块),要么作为
modules
目录 的一部分。
无论哪种形式,其结构类似。
模块定义
src/module.ts
。模块定义是模块的入口点。Nuxt 在配置中引用你的模块时将加载它。
底层上,Nuxt 模块定义是一个简单且可能是异步的函数,它接受内联用户选项和用于与 Nuxt 交互的 nuxt
对象。
export default function (inlineOptions, nuxt) {
// 你可以在这里任意操作..
console.log(inlineOptions.token) // `123`
console.log(nuxt.options.dev) // `true` 或 `false`
nuxt.hook('ready', async nuxt => {
console.log('Nuxt 已准备就绪')
})
}
你可以使用 Nuxt Kit 提供的高级辅助 defineNuxtModule
获得类型提示支持。
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule((options, nuxt) => {
nuxt.hook('pages:extend', pages => {
console.log(`发现了 ${pages.length} 个页面`)
})
})
然而,我们不建议直接使用此低级函数定义。定义模块时,我们推荐使用带有 meta
属性的对象语法来标识模块,特别是当发布到 npm 时。
此辅助函数通过实现模块所需的许多通用模式,使编写 Nuxt 模块更简单,保证未来兼容性,并改善模块作者和用户的体验。
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
meta: {
// 通常是模块的 npm 包名
name: '@nuxtjs/example',
// 在 `nuxt.config` 中保存模块选项的键名
configKey: 'sample',
// 兼容性约束
compatibility: {
// 支持的 Nuxt 语义化版本范围
nuxt: '>=3.0.0'
}
},
// 模块的默认配置选项,也可以是返回这些选项的函数
defaults: {},
// 注册 Nuxt 钩子的简写语法
hooks: {},
// 包含模块逻辑的函数,可以是异步的
setup(moduleOptions, nuxt) {
// ...
}
})
最终,defineNuxtModule
返回一个封装函数,该函数实现低级 (inlineOptions, nuxt)
模块函数签名。该封装函数在调用你的 setup
函数前,会执行默认值等必要步骤:
- 支持
defaults
和meta.configKey
,自动合并模块选项 - 类型提示和自动类型推断
- 添加基本的 Nuxt 2 兼容性补丁
- 确保模块只安装一次,使用由
meta.name
或meta.configKey
计算的唯一键 - 自动注册 Nuxt 钩子
- 根据模块元信息自动检测兼容性问题
- 暴露
getOptions
和getMeta
给 Nuxt 内部使用 - 只要模块使用来自最新版本
@nuxt/kit
的defineNuxtModule
,就保证前后向兼容 - 集成模块构建工具链
运行时目录
src/runtime
。模块本身以及 Nuxt 配置中的其它内容不包含在应用的运行时中。但你可能希望模块为被安装的应用提供或注入运行时代码。运行时目录就是为了此目的。
运行时目录下,可以提供任何与 Nuxt 应用相关的资源,包括:
- Vue 组件
- 组合函数(composables)
- Nuxt 插件
对于 服务器引擎 Nitro:
- API 路由
- 中间件
- Nitro 插件
或任何你想注入到用户 Nuxt 应用中的其他资源:
- 样式表
- 3D 模型
- 图片
- 等等
你可以在模块定义中注入这些资源到应用。
#imports
等路径导入。
原因是出于性能考虑,自动导入不会针对
node_modules
中的文件(发布模块的最终所在位置)启用。
如果你使用模块启动模板,playground 中也不会启用自动导入。
工具
模块开发配备了一套官方工具,帮助你管理开发流程。
@nuxt/module-builder
Nuxt 模块构建工具是一款零配置的构建工具,负责处理构建和发布模块的大部分复杂工作,确保模组产物与 Nuxt 应用的兼容性。
@nuxt/kit
Nuxt Kit 提供了组合式工具,辅助模块与 Nuxt 应用交互。建议你在可能的情况下使用 Nuxt Kit 工具,确保模块兼容性和代码可读性。
@nuxt/test-utils
Nuxt 测试工具是一个集合,帮助在模块测试中搭建和运行 Nuxt 应用。
使用范例
这里列出常用的模块创作模式。
修改 Nuxt 配置
模块可以读取并修改 Nuxt 配置。例如启用实验性功能的模块示例:
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
// 若还不存在 experimental 对象,则创建
nuxt.options.experimental ||= {}
nuxt.options.experimental.componentIslands = true
}
})
复杂配置修改时建议使用 defu。
向运行时暴露选项
模块不在应用运行时中,因此其选项也不会被包含。但通常你可能需要在运行时代码中访问模块选项。建议通过 Nuxt 的 runtimeConfig
暴露必要的配置。
import { defineNuxtModule } from '@nuxt/kit'
import { defu } from 'defu'
export default defineNuxtModule({
setup (options, nuxt) {
nuxt.options.runtimeConfig.public.myModule = defu(nuxt.options.runtimeConfig.public.myModule, {
foo: options.foo
})
}
})
注意,我们使用 defu
以扩展用户提供的公共运行时配置,而非覆盖。
你可以像访问普通运行时配置那样,在插件、组件或应用中访问模块选项:
const options = useRuntimeConfig().public.myModule
使用 addPlugin
注入插件
插件是模块添加运行时代码的常用方式。你可以使用 addPlugin
工具从模块中注册插件。
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
// 创建路径解析器
const resolver = createResolver(import.meta.url)
addPlugin(resolver.resolve('./runtime/plugin'))
}
})
使用 addComponent
注入 Vue 组件
如果模块提供 Vue 组件,可以使用 addComponent
工具将它们添加为 Nuxt 的自动导入组件。
import { defineNuxtModule, addComponent, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
// 从 runtime 目录注入组件
addComponent({
name: 'MySuperComponent', // Vue 模板中使用的组件名
export: 'MySuperComponent', // (可选)若组件为具名导出而非默认导出
filePath: resolver.resolve('runtime/components/MySuperComponent.vue')
})
// 来自一个库
addComponent({
name: 'MyAwesomeComponent',
export: 'MyAwesomeComponent',
filePath: '@vue/awesome-components'
})
}
})
或者使用 addComponentsDir
一次添加整个目录:
import { defineNuxtModule, addComponentsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
addComponentsDir({
path: resolver.resolve('runtime/components')
})
}
})
使用 addImports
和 addImportsDir
注入组合函数 (composables)
如果模块提供组合函数,可以使用 addImports
工具添加自动导入支持:
import { defineNuxtModule, addImports, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
addImports({
name: 'useComposable', // 组合函数名
as: 'useComposable',
from: resolver.resolve('runtime/composables/useComposable') // 组合函数路径
})
}
})
或者使用 addImportsDir
一次添加整个目录:
import { defineNuxtModule, addImportsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
addImportsDir(resolver.resolve('runtime/composables'))
}
})
使用 addServerHandler
注入服务器路由
import { defineNuxtModule, addServerHandler, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
addServerHandler({
route: '/api/hello',
handler: resolver.resolve('./runtime/server/api/hello/index.get')
})
}
})
你也可以添加动态路径的服务器路由:
import { defineNuxtModule, addServerHandler, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
addServerHandler({
route: '/api/hello/:name',
handler: resolver.resolve('./runtime/server/api/hello/[name].get')
})
}
})
注入其他资源
如果模块提供其他类型资源,也可以注入。例如向 Nuxt 的 css
数组推入样式表:
import { defineNuxtModule, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.options.css.push(resolver.resolve('./runtime/style.css'))
}
})
更高级地,可通过 Nitro 的 publicAssets
选项暴露一整文件夹的资源:
import { defineNuxtModule, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.hook('nitro:config', async (nitroConfig) => {
nitroConfig.publicAssets ||= []
nitroConfig.publicAssets.push({
dir: resolver.resolve('./runtime/public'),
maxAge: 60 * 60 * 24 * 365 // 1 年
})
})
}
})
在模块中使用其他模块
如果你的模块依赖其他模块,可以使用 Nuxt Kit 的 installModule
工具添加它们。例如,若你的模块需要使用 Nuxt Tailwind:
import { defineNuxtModule, createResolver, installModule } from '@nuxt/kit'
export default defineNuxtModule({
async setup (options, nuxt) {
const resolver = createResolver(import.meta.url)
// 注入包含 Tailwind 指令的 CSS
nuxt.options.css.push(resolver.resolve('./runtime/assets/styles.css'))
await installModule('@nuxtjs/tailwindcss', {
// 模块配置
exposeConfig: true,
config: {
darkMode: 'class',
content: {
files: [
resolver.resolve('./runtime/components/**/*.{vue,mjs,ts}'),
resolver.resolve('./runtime/*.{mjs,js,ts}')
]
}
}
})
}
})
使用钩子
生命周期钩子允许你扩展 Nuxt 的各个方面。模块可通过 hooks
对象声明或编程式调用 nuxt.hook
注册钩子。
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
// 通过 `hooks` 对象监听 `app:error` 钩子
hooks: {
'app:error': (err) => {
console.info(`发生错误:${err}`);
}
},
setup (options, nuxt) {
// 编程式监听 `pages:extend` 钩子
nuxt.hook('pages:extend', (pages) => {
console.info(`发现了 ${pages.length} 个页面`);
})
}
})
如果你的模块开启了监听器或其他资源,应在 Nuxt 生命周期结束时关闭它们。可以使用
close
钩子实现。import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
nuxt.hook('close', async nuxt => {
// 你的清理代码
})
}
})
添加模板/虚拟文件
如果需要添加可被用户应用导入的虚拟文件,可使用 addTemplate
工具。
import { defineNuxtModule, addTemplate } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
// 文件会加入 Nuxt 内部虚拟文件系统,可通过 '#build/my-module-feature.mjs' 导入
addTemplate({
filename: 'my-module-feature.mjs',
getContents: () => 'export const myModuleFeature = () => "hello world !"'
})
}
})
服务端使用 addServerTemplate
:
import { defineNuxtModule, addServerTemplate } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
// 文件加入 Nitro 虚拟文件系统,服务器代码中可通过 'my-server-module.mjs' 导入
addServerTemplate({
filename: 'my-server-module.mjs',
getContents: () => 'export const myServerModule = () => "hello world !"'
})
}
})
添加类型定义
你可能想向用户项目添加类型声明(比如扩展 Nuxt 接口或提供全局类型),Nuxt 提供了 addTypeTemplate
工具用于写入文件并添加到生成的 nuxt.d.ts
。
若需扩展 Nuxt 管理的类型,可使用 addTypeTemplate
:
import { defineNuxtModule, addTypeTemplate } from '@nuxt/kit'
export default defineNuxtModule({
setup (options, nuxt) {
addTypeTemplate({
filename: 'types/my-module.d.ts',
getContents: () => `// 由 my-module 生成
interface MyModuleNitroRules {
myModule?: { foo: 'bar' }
}
declare module 'nitropack' {
interface NitroRouteRules extends MyModuleNitroRules {}
interface NitroRouteConfig extends MyModuleNitroRules {}
}
export {}`
})
}
})
如果需要更灵活控制,可以用 prepare:types
钩子注册回调注入类型引用。
const template = addTemplate({ /* template options */ })
nuxt.hook('prepare:types', ({ references }) => {
references.push({ path: template.dst })
})
更新模板
若需要更新模板/虚拟文件,可以使用 updateTemplates
:
nuxt.hook('builder:watch', async (event, path) => {
if (path.includes('my-module-feature.config')) {
// 重新加载已注册的模板
updateTemplates({ filter: t => t.filename === 'my-module-feature.mjs' })
}
})
测试
测试确保你的模块在各种配置下均能正常工作。本节讲述如何对模块进行多种测试。
单元和集成测试
端到端测试
Nuxt 测试工具 是模块端到端测试的首选库。使用流程:
- 在
test/fixtures/*
下创建 Nuxt 应用用作“测试用例” - 在测试文件中用该用例初始化 Nuxt
- 使用
@nuxt/test-utils
的工具对用例操作(例如请求页面) - 进行断言(例如验证 HTML 内容)
- 重复以上步骤
示例用例:
// 1. 创建 Nuxt 应用用作测试用例
import MyModule from '../../../src/module'
export default defineNuxtConfig({
ssr: true,
modules: [
MyModule
]
})
测试代码:
import { describe, it, expect } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, $fetch } from '@nuxt/test-utils/e2e'
describe('ssr', async () => {
// 2. 用此测试用例设置 Nuxt
await setup({
rootDir: fileURLToPath(new URL('./fixtures/ssr', import.meta.url)),
})
it('渲染首页', async () => {
// 3. 使用 `@nuxt/test-utils` 与用例交互
const html = await $fetch('/')
// 4. 执行断言
expect(html).toContain('<div>ssr</div>')
})
})
// 5. 可以重复编写其它测试
describe('csr', async () => { /* ... */ })
使用 Playground 和外部手动测试
拥有 playground Nuxt 应用,方便你开发时测试模块。模块启动模板集成了 playground。
你也可以在其他 Nuxt 应用(非模块仓库内)本地测试模块。使用 npm pack
或包管理器创建模块压缩包,再在测试项目中将模块添加到 package.json
,如 "my-module": "file:/path/to/tarball.tgz"
。
这样,你就可以像普通项目一样引用 my-module
。
最佳实践
强大的工具也意味着责任。模块虽强大,创建模块时请牢记以下最佳实践,以保证应用性能和良好的开发体验。
异步模块
如前所述,Nuxt 模块可以是异步的,方便调用某些 API 或异步函数。
但是要注意,Nuxt 会等待模块完成 setup 后才继续加载下一个模块和启动开发服务器或构建等。因此应尽量将耗时逻辑延迟到 Nuxt 钩子中。
始终给导出接口添加前缀
模块应为所有导出的配置、插件、API、组合函数或组件添加显式前缀,避免与其他模块或内核冲突。
理想情况下,使用你的模块名作为前缀(例如模块名为 nuxt-foo
,则导出 <FooButton>
和 useFooBar()
,而非 <Button>
和 useBar()
)。
支持 TypeScript
Nuxt 对 TypeScript 支持一流。暴露类型和使用 TypeScript 编写模块即使用户未直接使用 TypeScript,也能带来更佳体验。
避免 CommonJS 语法
Nuxt 依赖原生 ESM。请查阅原生 ES 模块获取更多信息。
编写模块使用文档
在 README 中文档化模块使用方式:
- 为什么要用此模块?
- 如何使用此模块?
- 模块实现了什么功能?
并附上集成网站和文档链接,效果更佳。
提供 StackBlitz 示例或样板项目
推荐在模块 README 中附上最小复现示例的 StackBlitz 链接。
这不仅给潜在用户快速体验模块的途径,也方便用户遇到问题时提供最小复现给你。
避免针对特定 Nuxt 版本宣传模块
Nuxt、Nuxt Kit 及其它新工具都考虑了向前和向后兼容。
请使用“X for Nuxt”而非“X for Nuxt 3”,以避免生态碎片化,并使用 meta.compatibility
来设置 Nuxt 版本约束。
保持启动模板默认配置
模块启动模板遵循一套默认工具和配置(如 ESLint),若打算开源模块,建议保持默认设置,使模块与其它社区模块保持一致风格,便于贡献者参与。
生态系统
Nuxt 模块生态累计每月超过 1500 万 NPM 下载量,提供丰富功能和各种工具的集成。你也可以加入这个生态!
模块类型
官方模块 以 @nuxt/
命名空间为前缀(如 @nuxt/content
),由 Nuxt 团队开发维护。社区贡献亦欢迎!
社区模块 以 @nuxtjs/
命名空间为前缀(如 @nuxtjs/tailwindcss
),由社区成员维护,欢迎各方贡献。
第三方及其它社区模块 通常以 nuxt-
前缀命名,任何人均可创建,使用这种前缀便于 npm 上模块的发现。适合试验和构想新思路。
私有或个人模块 针对个人或公司需求创建,不须遵循特定命名规则,通常位于 npm 组织命名空间下(如 @my-company/nuxt-auth
)。
在社区模块列表中注册你的模块
欢迎将社区模块添加到模块列表。提交模块注册请求,请在 Nuxt 官方仓库 nuxt/modules 中创建 issue。Nuxt 团队可帮助你应用最佳实践后再加入列表。
nuxt-modules
和 @nuxtjs/
加入
将模块移至 nuxt-modules 组织,意味着有更多人协助维护,共同打造完美解决方案。
如果你已有公开模块并想移交给 nuxt-modules
,请在 Nuxt 模块仓库开 issue。
加入 nuxt-modules
后,我们可将你的社区模块重命名为 @nuxtjs/
命名空间,并为其提供子域(如 my-module.nuxtjs.org
)方便托管文档。