自定义 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 devnuxi 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 函数都做了两件事:

  1. 正确地修改 Nuxt 配置(处理路径解析、去重、合并等细节)
  2. 保持与 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.vue

3. 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:configNitro 配置阶段修改 server 配置
vite:extendConfigVite 配置阶段注入 Vite 插件
build:before构建开始前预处理
build:done构建完成后后处理、统计
closeNuxt 实例关闭清理资源

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(引用你的模块)和一个使用 &lt;VideoPlayer&gt; 的页面。

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.tsimportruntime/ 的代码会导致构建错误。只传路径字符串给 Kit 函数。
  • 忘记 createResolver:硬编码相对路径在 npm 发布后会失效,必须用 createResolver 解析绝对路径。
  • 配置合并冲突:修改 nuxt.options 时用 push/merge 而非直接赋值,避免覆盖其他模块的配置。
  • 模块间依赖:如果模块 A 依赖模块 B 的注入,用 installModule('module-b') 确保 B 先加载,或在 modules:done Hook 中读取 B 的配置。

本章小结

  • 模块是构建阶段的 setup 函数,通过 @nuxt/kit 工具函数注入组件、composables、plugins、server routes
  • runtime vs moduleruntime/ 是运行时代码(浏览器/服务端),module.ts 是构建时代码(Node.js),不能交叉 import
  • Kit 核心 APIaddComponentaddImportsaddServerHandleraddPluginaddTemplatecreateResolver
  • Hooksnuxt.hook() 介入 Nuxt 生命周期,在关键阶段修改配置或执行逻辑
  • runtimeConfig:构建时配置传递到运行时的标准桥梁
  • 发布流程nuxt-module-build 构建 → npm publish,零配置可用 + 渐进式配置