Nuxt3 中文课程 《实战全栈开发简书》.

模块作者指南

学习如何创建一个Nuxt模块,以集成、增强或扩展任何Nuxt应用程序。

Nuxt的配置钩子系统使得可以定制Nuxt的每个方面,并添加任何可能需要的集成(Vue插件、CMS、服务器路由、组件、日志记录等)。

Nuxt模块是在使用nuxt dev启动Nuxt开发模式或使用nuxt build构建项目时按顺序运行的函数。通过使用模块,可以将自定义解决方案封装、进行适当的测试并将其共享为npm软件包,而不需要在项目中添加不必要的样板代码或要求对Nuxt本身进行更改。

快速入门

我们建议您使用我们的起始模板来开始使用Nuxt模块:

终端
npx nuxi init -t module my-module

这将创建一个名为my-module的项目,其中包含开发和发布模块所需的所有样板代码。

下一步操作:

  1. 在你选择的IDE中打开my-module
  2. 使用你喜欢的包管理器安装依赖项
  3. 使用npm run dev:prepare为开发准备本地文件
  4. 阅读本文档以了解更多关于Nuxt模块的知识

使用入门

了解如何使用模块入门。

开发流程

尽管你的模块源代码位于src目录中,但大多数情况下,为了开发一个模块,你需要一个Nuxt应用程序。这就是playground目录的作用。它是一个你可以随意调试的Nuxt应用程序,并且已经配置为与你的模块一起运行。

你可以像与任何Nuxt应用程序一样与playground进行交互。

  • 使用npm run dev启动开发服务器,当你在src目录中进行模块更改时,它会自动重新加载
  • 使用npm run dev:build构建它
所有其他nuxi命令都可以针对playground目录使用(例如 nuxi <COMMAND> playground)。你可以在package.json中自由地声明额外的dev:*脚本,以方便使用。

测试流程

模块入门自带了一个基本的测试套件:

  • 使用ESLint提供的代码检查工具,使用npm run lint运行它
  • 使用Vitest提供的测试运行工具,使用npm run testnpm run test:watch运行它
你可以根据需要扩展默认的测试策略,以更好地满足你的需求。

构建流程

Nuxt模块自带了自己的构建工具,由@nuxt/module-builder提供。这个构建工具不需要你进行任何配置,支持TypeScript,并确保你的资源被正确打包以便分发给其他Nuxt应用程序。

你可以通过运行npm run prepack来构建你的模块。

虽然在某些情况下构建模块可能很有用,但大多数情况下你不需要自己构建它:在开发时,playground会自动处理构建,而发布脚本在发布时也会为你提供支持。

发布流程

在将你的模块发布到npm之前,请确保你拥有一个npmjs.com帐户,并且已经通过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模块分为两种类型:

无论哪种情况,它们的结构都是相似的。

模块定义

当使用模块入门时,你的模块定义位于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函数之前应用了默认值和其他必要的步骤:

  • 支持使用defaultsmeta.configKey自动合并模块选项
  • 提供类型提示和自动类型推断
  • 为基本的Nuxt 2兼容性添加shim
  • 使用从meta.namemeta.configKey计算的唯一键确保模块只被安装一次
  • 自动注册Nuxt钩子
  • 基于模块元数据自动检查兼容性问题
  • 提供getOptionsgetMeta供Nuxt内部使用
  • 确保向后和向上兼容性,只要模块使用的是@nuxt/kit的最新版本中的defineNuxtModule
  • 与模块构建工具集成

运行时目录

使用起始器时,运行时目录位于src/runtime

模块和 Nuxt 配置中的所有内容一样,并不包含在应用的运行时中。但是,你可能希望你的模块能够提供或注入运行时代码到安装它的应用程序中。这就是运行时目录的作用。

在运行时目录中,你可以提供与 Nuxt 应用相关的任何类型的资源:

对于 服务器引擎 Nitro 来说:

  • API 路由
  • 中间件
  • Nitro 插件

或者任何其他你想要注入到用户的 Nuxt 应用程序中的资源:

  • 样式表
  • 3D 模型
  • 图片
  • 等等

然后,你就可以从你的模块定义中将所有这些资源注入到应用程序中。

教程部分了解有关资源注入的更多信息。
已发布的模块不能使用运行时目录内的资源的自动导入功能。相反,它们必须从#imports或类似的位置显式地导入这些资源。确实,出于性能原因,自动导入对于node_modules中的文件(发布的模块最终将位于此位置)是禁用的。这就是为什么模块起始器在开发模块时有意禁用它们的原因。如果你使用的是模块起始器,在你的 playground 中也不会启用自动导入。

工具

模块提供了一组官方工具,以帮助你进行模块的开发。

@nuxt/module-builder

Nuxt Module Builder 是一个零配置的构建工具,负责处理构建和发布模块的大部分繁重工作。它确保你的模块构建产物与 Nuxt 应用程序具有适当的兼容性。

@nuxt/kit

Nuxt Kit 提供了一组可组合的实用工具,帮助你的模块与 Nuxt 应用程序进行交互。建议尽量使用 Nuxt Kit 提供的工具,以确保更好的兼容性和代码可读性。

Read more in Docs > Guide > Going Further > Kit.

@nuxt/test-utils

Nuxt Test Utils 是一组实用工具,可帮助你在模块测试中设置和运行 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
请注意,不要在公共运行时配置中暴露任何敏感的模块配置,比如私有 API 密钥,因为它们将最终出现在公共捆绑包中。
Read more in Docs > Guide > Going Further > Runtime Config.

使用 addPlugin 注入插件

插件是模块添加运行时逻辑的常见方式。你可以使用 addPlugin 工具从模块中注册插件。

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'
    })
  }
})

使用 addImportsaddImportsDir 注入组合式函数

如果你的模块需要提供组合式函数,你可以使用 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.ts')
    })
  }
})

你也可以添加一个动态的服务器路由:

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.ts')
    })
  }
})

注入其他资源

如果你的模块需要提供其他类型的资源,也可以进行注入。这里有一个简单的示例模块通过 Nuxt 的 css 数组注入样式表。

import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    nuxt.options.css.push(resolve('./runtime/style.css'))
  }
})

还有一个更高级的示例,通过 NitropublicAssets 选项公开一个资源文件夹:

import { defineNuxtModule, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    nuxt.hook('nitro:config', async (nitroConfig) => {
      nitroConfig.publicAssets ||= []
      nitroConfig.publicAssets.push({
        dir: 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 { resolve } = createResolver(import.meta.url)

    // 我们可以注入包含 Tailwind 指令的 CSS 文件
    nuxt.options.css.push(resolve('./runtime/assets/styles.css'))

    await installModule('@nuxtjs/tailwindcss', {
      // 模块配置
      exposeConfig: true,
      config: {
        darkMode: 'class',
        content: {
          files: [
            resolve('./runtime/components/**/*.{vue,mjs,ts}'),
            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(`This error happened: ${err}`);
    }
  },
  setup (options, nuxt) {
    // 通过编程方式钩住 `pages:extend` 钩子
    nuxt.hook('pages:extend', (pages) => {
      console.info(`Discovered ${pages.length} pages`);
    })
  }
})
Read more in Docs > API > Advanced > Hooks.
模块清理如果你的模块打开、处理或启动了一个观察器,应该在 Nuxt 生命周期结束时关闭它。可以使用 close 钩子来实现这一点。
import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    nuxt.hook('close', async nuxt => {
      // Your custom code here
    })
  }
})

添加模板/虚拟文件

如果你需要添加一个可以导入到用户应用程序中的虚拟文件,可以使用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 !"'
    })
  }
})

添加类型声明

你可能还想在用户项目中添加类型声明(例如,增强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: () => `// Generated by 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模块的单元测试和集成测试。点击这里查看RFC,参与讨论

端到端测试

Nuxt Test Utils 是用来帮助你以端到端方式测试你的模块的首选库。下面是使用它的工作流程:

  1. 创建一个Nuxt应用程序,用作test/fixtures/*中的"fixture"
  2. 在测试文件中使用这个fixture来设置Nuxt
  3. 使用@nuxt/test-utils中的工具与fixture进行交互(例如,获取页面)
  4. 对fixture进行相关的检查(例如,"HTML包含...")
  5. 重复上述步骤

在实践中,fixture如下所示:

test/fixtures/ssr/nuxt.config.ts
// 1. 创建一个Nuxt应用程序,用作"fixture"
import MyModule from '../../../src/module'

export default defineNuxtConfig({
  ssr: true,
  modules: [
    MyModule
  ]
})

以及它的测试:

test/rendering.ts
import { describe, it, expect } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, $fetch } from '@nuxt/test-utils'

describe('ssr', async () => {
  // 2. 在测试文件中使用这个fixture来设置Nuxt
  await setup({
    rootDir: fileURLToPath(new URL('./fixtures/ssr', import.meta.url)),
  })

  it('renders the index page', async () => {
    // 3. 使用`@nuxt/test-utils`中的工具与fixture进行交互
    const html = await $fetch('/')

    // 4. 对fixture进行相关的检查
    expect(html).toContain('<div>ssr</div>')
  })
})

// 5. 重复
describe('csr', async () => { /* ... */ })
有关此类工作流程的示例,请参考模块入门示例

使用Playground和外部进行手动QA

在开发模块时,拥有一个用于测试模块的Playground Nuxt应用程序非常有用。模块入门示例集成了一个用于这个目的的Playground应用程序

你可以在本地使用其他Nuxt应用程序(不是模块仓库的一部分的应用程序)来测试你的模块。为此,你可以使用npm pack命令或等效的包管理器命令,从你的模块创建一个tarball。然后在测试项目中,你可以将你的模块添加到package.jsonpackages中,如:"my-module": "file:/path/to/tarball.tgz"

之后,你就可以像在普通项目中一样引用my-module

最佳实践

伴随着强大的功能,也要有伟大的责任。在编写模块时,请记住以下一些最佳实践,以保持应用程序的性能和开发者体验。

异步模块

如前所述,Nuxt模块可以是异步的。例如,你可能想开发一个需要从API获取数据或调用异步函数的模块。

然而,要小心异步行为,因为Nuxt会在继续下一个模块和启动开发服务器、构建过程等之前等待你的模块设置完成。建议将耗时的逻辑延迟到Nuxt钩子中执行。

如果你的模块设置超过1秒,Nuxt会发出警告。

始终为暴露的接口添加前缀

Nuxt模块应为任何暴露的配置、插件、API、可组合项或组件提供明确的前缀,以避免与其他模块和内部功能冲突。

理想情况下,你应该使用你的模块名称作为前缀(例如,如果你的模块叫做nuxt-foo,则应该暴露<FooButton>useFooBar(),而不是<Button>useBar())。

支持TypeScript

Nuxt 3对TypeScript进行了全面集成,以提供最佳的开发者体验。

暴露类型并使用TypeScript开发模块,即使不直接使用TypeScript,也会使用户受益。

避免使用CommonJS语法

Nuxt 3依赖于原生的ES模块。请阅读原生ES模块获取更多信息。

文档化模块使用方法

考虑在自述文件中记录模块的使用方法:

  • 为什么要使用该模块?
  • 如何使用该模块?
  • 该模块的功能是什么?

提供与集成网站和文档的链接总是一个好主意。

提供StackBlitz演示或示例代码

在模块的自述文件中,创建一个与你的模块相关的最小化示例,并使用StackBlitz进行演示。

这不仅为潜在的模块用户提供了一个快速和简单的方式来尝试模块,还为他们提供了一个简单的方式来构建最小化的示例,当他们遇到问题时,可以将示例发送给你。

不要限定于特定的Nuxt版本进行广告宣传

Nuxt 3、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)。