Nuxt3のモジュールを作りたくなり、
いろいろ調べてみたときの備忘録(*´ω`*)
公式サイトのドキュメントを見ながら
テンプレートから作成
# テンプレートからmy-moduleディレクトリを作成 $ npx nuxi init -t module my-module $ cd my-module # 開発モードで使う資材の生成 $ npm run dev:prepare # 開発サーバの起動 $ npm run dev
ディレクトリ構成
テンプレートから作成されるディレクトリ構成はこんな感じ
my-module/ # moduleの本体 - src/ - runtime/ - plugin.ts - module.ts # 開発時確認用のNuxtプロジェクト - playground/ - app.vue - nuxt.config.ts # テスト資材(vitest) - test/ - fixtures/ - basic.test.ts - package.json
用意されているコマンド
### 開発系 # 開発時の資材作成 $ npm run dev:prepare # 開発モードでの起動 $ npm run dev # 開発モードでのビルド $ npm run dev:build ### lint / test $ npm run lint $ npm run test $ npm run test:watch ### 本番関係 # 本番時のビルド $ npm run prepack # 本番資材のリリース/公開 $ npm run release
それぞれの中身はこんな感じ。
"scripts": { "prepack": "nuxt-module-build", "dev": "nuxi dev playground", "dev:build": "nuxi build playground", "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", "release": "pnpm run lint && pnpm run test && npm run prepack && changelogen --release && pnpm publish && git push --follow-tags", "lint": "eslint .", "test": "vitest run", "test:watch": "vitest watch" },
モジュールの開発で使うファイル
src/module.ts
モジュールのエントリーポイント。
- モジュールに関する設定(
meta
) - optionのデフォルト値(
default
) - Nuxt hooksで行う処理(
hooks
) - モジュールが読み込まれたときの処理(
setup
)
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit' // Module options TypeScript interface definition export interface ModuleOptions {} export default defineNuxtModule<ModuleOptions>({ meta: { // Usually the npm package name of your module name: 'my-module', // The key in `nuxt.config` that holds your module options configKey: 'myModule' // Compatibility constraints compatibility: { // Semver version of supported nuxt versions nuxt: '^3.0.0' } }, // Default configuration options of the Nuxt module defaults: {}, // Shorthand sugar to register Nuxt hooks hooks: {}, // The function holding your module logic, it can be asynchronous setup (options, nuxt) { const resolver = createResolver(import.meta.url) // Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack` addPlugin(resolver.resolve('./runtime/plugin')) } })
テンプレートの例では、プラグイン('./runtime/plugin')を追加するモジュールの例。
src/runtime/plugins.ts
こっちは、追加されるプラグインのサンプル。
"THE・サンプル"って感じのコンソールログを出すだけのソース。
// src/runtime/plugins.ts import { defineNuxtPlugin } from '#app' export default defineNuxtPlugin((nuxtApp) => { console.log('Plugin injected by my-module!') })
Runtime Directory
Nuxtのモジュールは、基本的にビルドされた資材に含まれないけど、
runtime
ディレクトリ配下のものは含めることはできるらしい。
runtime
ディレクトリ配下に配置するとよいものは、
- Vueコンポーネント/Stylesheets/Image etc..
- Composables / Nuxt Plugins etc..
- API routes / Middlewares / NitroPrugins etc..
モジュール開発用のTool
@nuxt/module-builder
モジュールのビルドツール。
@nuxt/kit
モジュール開発時のUtility。
addPlugin
などの便利関数を提供。
@nuxt/test-utils
モジュールテスト時のUtility。
Nuxt applicationのsetupや実行をなどを便利に。
Recipes
Module Author Guideに書かれてるレシピ/使い方のサンプル。
Nuxt Configの変更
// src/module.ts import { defineNuxtModule } from '@nuxt/kit' export default defineNuxtModule({ setup (options, nuxt) { // We create the `experimental` object if it doesn't exist yet nuxt.options.experimental ||= {} nuxt.options.experimental.componentIslands = true } })
複雑な構成を利用する場合、unjs/defuの利用を推奨
RuntimeConfigにモジュールのデフォルト値をマージ
nuxt.options.runtimeConfig.public
にマージすることで、
useRuntimeConfig()
から使えるようになる。
// src/module.ts 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 }) } })
使うときはこんな感じ。
const options = useRuntimeConfig().public.myModule
プラグインの追加(addPlugin
)
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit' export default defineNuxtModule({ setup (options, nuxt) { // Create resolver to resolve relative paths const { resolve } = createResolver(import.meta.url) addPlugin(resolve('./runtime/plugin')) } })
Vueコンポーネントの追加(addComponent
)
import { defineNuxtModule, addComponent } from '@nuxt/kit' export default defineNuxtModule({ setup(options, nuxt) { const resolver = createResolver(import.meta.url) // From the runtime directory addComponent({ name: 'MySuperComponent', // name of the component to be used in vue templates export: 'MySuperComponent', // (optional) if the component is a named (rather than default) export filePath: resolver.resolve('runtime/components/MySuperComponent.vue') }) // From a library addComponent({ name: 'MyAwesomeComponent', // name of the component to be used in vue templates export: 'MyAwesomeComponent', // (optional) if the component is a named (rather than default) export filePath: '@vue/awesome-components' }) } })
Composablesの追加(addImports
/addImportsDir
)
import { defineNuxtModule, addImports, createResolver } from '@nuxt/kit' export default defineNuxtModule({ setup(options, nuxt) { const resolver = createResolver(import.meta.url) addImports({ name: 'useComposable', // name of the composable to be used as: 'useComposable', from: resolver.resolve('runtime/composables/useComposable') // path of composable }) // or addImportsDir(resolver.resolve('runtime/composables')) } })
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')) } })
画像などのpublicAssetsの追加
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 year }) }) } })
Lifecycle Hookへの処理の追加
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit' export default defineNuxtModule({ // Hook to the `app:error` hook through the `hooks` map hooks: { 'app:error': (err) => { console.info(`This error happened: ${err}`); } }, setup (options, nuxt) { // Programmatically hook to the `pages:extend` hook nuxt.hook('pages:extend', (pages) => { console.info(`Discovered ${pages.length} pages`); }) nuxt.hook('close', async nuxt => { // Your custom code here }) } })
Templates/Virtual Files(addTemplate
)
import { defineNuxtModule, addTemplate } from '@nuxt/kit' export default defineNuxtModule({ setup (options, nuxt) { // The file is added to Nuxt's internal virtual file system and can be imported from '#build/my-module-feature.mjs' addTemplate({ filename: 'my-module-feature.mjs', getContents: () => 'export const myModuleFeature = () => "hello world !"' }) } })
使い時はちょっとわからない。。
型定義の追加(addTypeTemplate
)
モジュールで型定義を拡張する必要がある場合に、
d.ts
ファイルの生成と追加ができる。
import { defineNuxtModule, addTemplate, addTypeTemplate } from '@nuxt/kit' export default defineNuxtModule({ setup (options, nuxt) { const template = 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 {}` }) nuxt.hook('prepare:types', ({ references }) => { references.push({ path: template.dst }) }) } })
Testing
@nuxt/test-utils
のsetup
や$fetch
を使うと、
Nuxtアプリの起動/呼び出しが簡単にできる。
import { describe, it, expect } from 'vitest' import { fileURLToPath } from 'node:url' import { setup, $fetch } from '@nuxt/test-utils' describe('ssr', async () => { // 2. Setup Nuxt with this fixture inside your test file await setup({ rootDir: fileURLToPath(new URL('./fixtures/ssr', import.meta.url)), }) it('renders the index page', async () => { // 3. Interact with the fixture using utilities from `@nuxt/test-utils` const html = await $fetch('/') // 4. Perform checks related to this fixture expect(html).toContain('<div>ssr</div>') }) }) // 5. Repeat describe('csr', async () => { /* ... */ })
Best Practices
非同期のモジュール / Async Modules
- setupアップ内の待ち時間は1秒以下を推奨
- 時間がかかる処理はNuxt hookで処理をするべき
プレフィックスを付ける / Always Prefix Exposed Interfaces
他のモジュールとの競合を避けるため、プレフィックスを付ける。
- OK:
<FooButton>
/useFooBar()
- NG:
<Button>
/useBar()
TypeScriptで / Be TypeScript Friendly
TypeScriptで開発しやすいように、
TypeScriptで開発し、型を公開しよう
CommonJSを避ける / Avoid CommonJS Syntax
Nuxt 3はESMネイティブなので、CommonJS構文は使わない。
その他
- ドキュメントを用意する / Document Module Usage
- デモを用意する / Provide a StackBlitz Demo or Boilerplate
- Nuxtのバージョンを固定しない / Do Not Advertize With a Specific Nuxt Version
- スターターのデフォルトをそのまま使う / Stick With Starter Defaults
モジュールの命名規則
@nuxt/*
... Official module@nuxtjs/*
... Community modulenuxt-*
... Third party and other community modules@my-company/nuxt-*
... Private or personal modules
なので、nuxt-*
や@my-company/nuxt-*
でつけるのが良さそう。
Nuxt Moduleのリストに追加する
PRを出すと、公式サイトのモジュール一覧に掲載してもらえる。
- モジュール一覧: Modules · Nuxt
- 追加依頼のPR: New Issue · nuxt/modules
以上!! ざっととした通し読みだけど、だいぶわかった気がする(*´ω`*)
よく使う設定とかは、モジュールとしてまとめておいてもよいかもしれない。