模块作者指南
Nuxt 的配置和钩子系统使得自定义 Nuxt 的每个方面以及添加任何所需的集成(Vue 插件、CMS、服务端路由、组件、日志记录等)成为可能。
Nuxt 模块是函数,在使用 nuxt dev
启动开发模式或使用 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
构建您的模块。
playground
在开发时会处理构建,发布脚本也会在发布时覆盖。如何发布
npm login
进行身份验证。虽然您可以通过提升版本并使用 npm publish
命令发布模块,但模块启动模板提供了一个发布脚本,帮助您确保发布到 npm 的模块版本正常工作。
要使用发布脚本,首先提交所有更改(我们建议遵循 Conventional Commits 以利用自动版本提升和变更日志更新),然后运行发布脚本 npm run release
。
运行发布脚本时,将发生以下操作:
- 首先,它会运行您的测试套件,包括:
- 运行代码检查器(
npm run lint
) - 运行单元测试、集成测试和端到端测试(
npm run test
) - 构建模块(
npm run prepack
)
- 运行代码检查器(
- 然后,如果测试套件运行正常,它将继续发布您的模块,包括:
- 根据您的 Conventional Commits 提升模块版本并生成变更日志
- 将模块发布到 npm(为此,模块将再次构建,以确保发布的工件中包含更新的版本号)
- 将表示新发布版本的 git 标签推送到您的 git 远程源
package.json
中微调默认的 release
脚本。开发模块
Nuxt 模块提供了多种强大的 API 和模式,允许以几乎任何方式修改 Nuxt 应用。本节将教您如何利用这些功能。
模块结构
我们可以考虑两种 Nuxt 模块:
- 已发布的模块通过 npm 分发 - 您可以在 Nuxt 网站 上查看一些社区模块的列表。
- “本地”模块,它们存在于 Nuxt 项目本身中,可以是内联在 Nuxt 配置中或作为
modules
目录的一部分。
在任一情况下,它们的结构相似。
模块定义
src/module.ts
。模块定义是模块的入口点。当您的模块在 Nuxt 配置中被引用时,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 版本的 Semver 版本
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 组件
- 组合式函数
- Nuxt 插件
对于服务端引擎,Nitro:
- API 路由
- 中间件
- Nitro 插件
或您希望注入用户 Nuxt 应用的任何其他类型的资产:
- 样式表
- 3D 模型
- 图片
- 等等
然后,您可以从模块定义中将所有这些资产注入应用。
#imports
或类似方式显式导入。
确实,出于性能原因,
node_modules
(已发布模块最终所在位置)中的文件未启用自动导入。工具
模块附带了一套一级工具来帮助您进行开发。
@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 } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
// 从运行时目录
addComponent({
name: 'MySuperComponent', // 在 Vue 模板中使用的组件名称
export: 'MySuperComponent', // (可选)如果组件是命名导出(而非默认导出)
filePath: resolver.resolve('runtime/components/MySuperComponent.vue')
})
// 从库
addComponent({
name: 'MyAwesomeComponent', // 在 Vue 模板中使用的组件名称
export: 'MyAwesomeComponent', // (可选)如果组件是命名导出(而非默认导出)
filePath: '@vue/awesome-components'
})
}
})
或者,您可以使用 addComponentsDir
添加整个目录。
import { defineNuxtModule, addComponentsDir } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
addComponentsDir({
path: resolver.resolve('runtime/components')
})
}
})
使用 addImports
和 addImportsDir
注入组合式函数
如果您的模块需要提供组合式函数,您可以使用 addImports
工具函数将它们添加为 Nuxt 的自动导入组合式函数。
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, addPlugin, 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<ModuleOptions>({
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
映射挂钩到它们。
import { defineNuxtModule, addPlugin, createResolver } 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 => {
// 您的自定义代码在此处
})
}
})
自定义钩子
模块还可以定义并调用自己的钩子,这是一个使模块可扩展的强大模式。
如果您希望其他模块能够订阅您的模块钩子,您应在 modules:done
钩子中调用它们。这可确保所有其他模块都有机会在自己的 setup
函数中完成设置并注册对您钩子的监听器。
// my-module/module.ts
import { defineNuxtModule } from '@nuxt/kit'
export interface ModuleHooks {
'my-module:custom-hook': (payload: { foo: string }) => void
}
export default defineNuxtModule({
setup (options, nuxt) {
// 在 `modules:done` 中调用您的钩子
nuxt.hook('modules:done', async () => {
const payload = { foo: 'bar' }
await nuxt.callHook('my-module:custom-hook', payload)
})
}
})
添加模板/虚拟文件
如果您需要添加一个可导入到用户应用中的虚拟文件,可以使用 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, addTemplate, 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/types' {
interface NitroRouteRules extends MyModuleNitroRules {}
interface NitroRouteConfig extends MyModuleNitroRules {}
}
export {}`
})
}
})
如果您需要更细粒度的控制,可以使用 prepare:types
钩子注册一个回调来注入您的类型。
const template = addTemplate({ /* 模板选项 */ })
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 和外部进行手动 QA
在开发模块时,拥有一个用于测试的 playground Nuxt 应用非常有用。模块启动模板为此集成了一个。
您还可以使用其他 Nuxt 应用(非模块仓库中的应用)在本地测试您的模块。为此,您可以使用 npm pack
命令或您的包管理器的等效命令,从模块创建一个 tarball。然后在您的测试项目中,可以将模块添加到 package.json
的包中,如:"my-module": "file:/path/to/tarball.tgz"
。
之后,您应该能够像在任何常规项目中一样引用 my-module
。
最佳实践
能力越大,责任越大。虽然模块功能强大,但在编写模块时需要牢记一些最佳实践,以保持应用的性能和出色的开发者体验。
异步模块
正如我们所见,Nuxt 模块可以是异步的。例如,您可能希望开发一个需要获取某些 API 或调用异步函数的模块。
然而,需谨慎处理异步行为,因为 Nuxt 会在您的模块设置完成之前等待,然后才进入下一个模块并启动开发服务器、构建过程等。优先将耗时逻辑推迟到 Nuxt 钩子中。
始终为暴露的接口添加前缀
Nuxt 模块应为任何暴露的配置、插件、API、组合式函数或组件提供明确的前缀,以避免与其他模块和内部内容的冲突。
理想情况下,您应该以模块名称为前缀(例如,如果您的模块名为 nuxt-foo
,应暴露 <FooButton>
和 useFooBar()
,而不是 <Button>
和 useBar()
)。
友好的 TypeScript 支持
Nuxt 提供了一流的 TypeScript 集成,以获得最佳的开发者体验。
暴露类型并使用 TypeScript 开发模块,即使不直接使用 TypeScript,也能为用户带来好处。
避免使用 CommonJS 语法
Nuxt 依赖于原生 ESM。请阅读原生 ES 模块以获取更多信息。
记录模块使用方法
考虑在 readme 文件中记录模块使用方法:
- 为什么使用此模块?
- 如何使用此模块?
- 此模块做什么?
链接到集成网站和文档始终是一个好主意。
提供 StackBlitz 示例或样板
为您的模块创建一个最小的重现,并使用 StackBlitz 添加到模块的 readme 文件中,这是一个很好的做法。
这不仅为模块的潜在用户提供了快速、简单的方式来试验模块,还为他们在遇到问题时提供了一种简便的方式来创建最小的重现并发送给您。
不要宣传特定 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 上可被发现。这是尝试和起草想法的最佳起点!
私有或个人模块是为您自己的用例或公司创建的模块。它们无需遵循任何命名规则即可与 Nuxt 一起工作,通常在 npm 组织下使用作用域(例如 @my-company/nuxt-auth
)。
列出您的社区模块
欢迎任何社区模块在模块列表上列出。要列出,请在 nuxt/modules 存储库中开启一个问题。Nuxt 团队可以帮助您在列出之前应用最佳实践。
nuxt-modules
和 @nuxtjs/
加入
通过将您的模块移至 nuxt-modules,总会有人提供帮助,这样我们就可以共同打造一个完美的解决方案。
如果您有一个已发布且正常工作的模块,并希望将其转移到 nuxt-modules
,请在 nuxt/modules 中开启一个问题。
通过加入 nuxt-modules
,我们可以将您的社区模块重命名为 @nuxtjs/
作用域,并为其文档提供一个子域名(例如 my-module.nuxtjs.org
)。