モジュール開発
CharlieはMegaShopのトラッキングロジック, エラー報告, パフォーマンス監視があちこちに散在していることに気づきました。AliceとBobの他のプロジェクトも同じ機能を必要としています。これらの共通機能をNuxtモジュールにカプセル化すれば, 一度開発すればどこでも再利用でき, npmに公開してコミュニティに提供することもできます。
1. 学ぶ内容
- モジュールアーキテクチャ:defineNuxtModule() + installModule() + ライフサイクルフック
- モジュール機能:コンポーネント注入 / Composables / プラグイン / ミドルウェア / サーバールーティング / 設定
- モジュール公開:npmパッケージバンドル + TypeScript型
- モジュールテスト:@nuxt/test-utils + fixtures
- MegaShop @megashop/analyticsモジュールの実践ガイド
2. アーキテクトのリアルストーリー
(1) ペインポイント:汎用機能の重複開発
CharlieのMegaShopにはトラッキングが必要です - すべてのページビュー, カートに追加するすべてのアイテム, すべてのクリックを記録する必要があります。彼は5つのコンポーネントで手動でAPIを呼び出しています。Aliceの他のプロジェクトもトラッキングが必要で, Bobはゼロからコードを書き直しました。コードは重複し, 一貫性がありません。
(2) Nuxtモジュールのソリューション
モジュールとしてパッケージ化すれば, すぐに使えます - ComposableとサーバーAPIが自動的に注入されます:
TYPESCRIPT
// nuxt.config.ts
modules: ['@megashop/analytics']
(3) 利点:一度開発すれば, どこでも再利用
すべてのプロジェクトはモジュールをインストールするだけでトラッキング機能を獲得します。AliceはどのプロジェクトでもuseAnalytics()を呼び出すだけでトラッキングを有効にできます - 設定不要です。
3. モジュールアーキテクチャ
(1) Nuxtモジュールライフサイクル
graph TB
A[defineNuxtModule] --> B[Setup関数]
B --> C[installModule - 依存関係]
B --> D[addPlugin - プラグイン登録]
B --> E[addComposable - Composables注入]
B --> F[addServerHandler - APIルート]
B --> G[addLayout - カスタムレイアウト]
B --> H[addComponent - 自動コンポーネント]
B --> I[extendConfig - 設定変更]
J[Nuxtフック] --> K[modules:before]
J --> L[modules:done]
J --> M[build:before]
J --> N[build:done]
(1) ▶サンプル:最小モジュールスケルトン
TYPESCRIPT
// src/module.ts
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
meta: {
name: '@megashop/analytics',
configKey: 'analytics',
compatibility: {
nuxt: '^3.0.0'
}
},
defaults: {
enabled: true,
endpoint: '/api/analytics',
debug: false
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// プラグインの登録
addPlugin(resolve('./runtime/plugin'))
// オプションをランタイムに公開
nuxt.options.runtimeConfig.public.analytics = {
enabled: options.enabled,
endpoint: options.endpoint,
debug: options.debug
}
}
})
出力:
TEXT
// 実行成功
(2) モジュール機能クイックリファレンス
| 機能 | API | 説明 |
|---|---|---|
| プラグイン登録 | addPlugin() | 初期化ロジックを自動実行 |
| コンポーネント注入 | addComponent() | Vueコンポーネントを自動インポート |
| Composable注入 | addImports() | 関数を自動インポート |
| APIルート追加 | addServerHandler() | サーバールートを自動登録 |
| レイアウト追加 | addLayout() | カスタムレイアウトを登録 |
| ミドルウェア追加 | addRouteMiddleware() | ルートミドルウェアを登録 |
| 設定変更 | extendConfig() | Nuxt設定を変更 |
| 依存モジュールインストール | installModule() | 他のモジュールをインストール |
4. 実践モジュール:@megashop/analytics
(1) ▶サンプル:モジュールエントリーポイント
TYPESCRIPT
// src/module.ts
import { defineNuxtModule, addPlugin, addImports, addServerHandler, createResolver } from '@nuxt/kit'
export interface ModuleOptions {
enabled: boolean
endpoint: string
debug: boolean
trackPageViews: boolean
trackClicks: boolean
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@megashop/analytics',
configKey: 'analytics',
compatibility: { nuxt: '^3.0.0' }
},
defaults: {
enabled: true,
endpoint: '/api/analytics',
debug: false,
trackPageViews: true,
trackClicks: true
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// 1. 自動トラッキング用のクライアントプラグインを登録
if (options.trackPageViews || options.trackClicks) {
addPlugin(resolve('./runtime/plugin.client'))
}
// 2. useAnalytics Composableを自動インポート
addImports({
name: 'useAnalytics',
from: resolve('./runtime/composables/useAnalytics')
})
// 3. イベント受信用のサーバーAPIを追加
addServerHandler({
method: 'post',
route: options.endpoint,
handler: resolve('./runtime/server/api/analytics')
})
// 4. 設定をランタイムに公開
nuxt.options.runtimeConfig.public.analytics = {
enabled: options.enabled,
endpoint: options.endpoint,
debug: options.debug
}
}
})
出力:
TEXT
// 実行成功
(2) ▶サンプル:ランタイムプラグイン
TYPESCRIPT
// src/runtime/plugin.client.ts
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig().public.analytics
if (!config.enabled) return
// ページビューの自動トラッキング
if (config.trackPageViews) {
nuxtApp.hook('page:finish', () => {
useAnalytics().trackPageView(window.location.pathname)
})
}
// data-track要素のクリックを自動トラッキング
if (config.trackClicks) {
document.addEventListener('click', (e) => {
const target = (e.target as HTMLElement).closest('[data-track]')
if (target) {
const event = target.getAttribute('data-track') || 'click'
useAnalytics().track(event, { element: target.tagName })
}
})
}
})
出力:
TEXT
// 実行成功
(3) ▶サンプル:ランタイムComposable
TYPESCRIPT
// src/runtime/composables/useAnalytics.ts
export function useAnalytics() {
const config = useRuntimeConfig().public.analytics
async function track(event: string, data?: Record<string, any>) {
if (!config.enabled) return
if (config.debug) console.log('[Analytics]', event, data)
await $fetch(config.endpoint, {
method: 'POST',
body: { event, data, timestamp: Date.now(), url: import.meta.client ? window.location.href : '' }
})
}
function trackPageView(path: string) {
track('page_view', { path })
}
function trackAddToCart(productId: number, productName: string, price: number) {
track('add_to_cart', { productId, productName, price, currency: 'USD' })
}
function trackPurchase(orderId: string, total: number) {
track('purchase', { orderId, total, currency: 'USD' })
}
return { track, trackPageView, trackAddToCart, trackPurchase }
}
出力:
TEXT
// 実行成功
(4) ▶サンプル:ランタイムサーバーAPI
TYPESCRIPT
// src/runtime/server/api/analytics.ts
import { defineEventHandler, readBody, setHeader } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// 必須フィールドの検証
if (!body.event) {
throw createError({ statusCode: 400, message: 'Event name required' })
}
// イベントの保存 (本番では:アナリティクスサービスに送信)
const storage = useStorage('analytics')
const key = `event:${Date.now()}:${Math.random().toString(36).slice(2)}`
await storage.setItem(key, {
event: body.event,
data: body.data || {},
timestamp: body.timestamp || Date.now(),
url: body.url,
userAgent: getHeader(event, 'user-agent')
})
setHeader(event, 'cache-control', 'no-store')
return { success: true }
})
出力:
TEXT
// 実行成功
5. モジュール公開
(1) ▶サンプル:package.json設定
JSON
{
"name": "@megashop/analytics",
"version": "1.0.0",
"type": "module",
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs",
"types": "./dist/types.d.ts"
},
"./runtime/*": "./dist/runtime/*"
},
"files": ["dist"],
"scripts": {
"build": "nuxt-module-build",
"dev": "nuxt-module-build --stub",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"nuxt": "^3.0.0"
},
"devDependencies": {
"@nuxt/module-builder": "^0.6.0",
"@nuxt/test-utils": "^3.0.0",
"nuxt": "^3.12.0"
}
}
出力:
JSON
{
"name": "@megashop/analytics",
"version": "1.0.0",
"type": "module",
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs",
"types": "./dist/types.d.ts"
},
"./runtime/*": "./dist/runtime/*"
},
"files": [
"dist"
],
"scripts": {
"build": "nuxt-module-build",
"dev": "nuxt-module-build --stub",
"test": "vitest run",
"prepublishOnly": "npm run
(2) ▶サンプル:プロジェクトでの使用
TYPESCRIPT
// MegaShopのnuxt.config.ts
export default defineNuxtConfig({
modules: [
// 開発中はローカルモジュール
'~/modules/analytics',
// 本番では公開済みモジュール
// '@megashop/analytics'
],
analytics: {
enabled: true,
endpoint: '/api/analytics',
debug: process.env.NODE_ENV === 'development',
trackPageViews: true,
trackClicks: true
}
})
出力:
TEXT
// 実行成功
6. モジュールテスト
(1) ▶サンプル:モジュールテストフィクスチャ
TYPESCRIPT
// test/module.test.ts
import { setupTest } from '@nuxt/test-utils'
describe('@megashop/analytics module', () => {
setupTest({
fixture: './test/fixtures/basic',
build: true
})
test('アナリティクスプラグインが登録される', () => {
// プラグインは自動登録される, Nuxtプラグインを確認
const nuxt = useNuxt()
const hasPlugin = nuxt.options.plugins.some(p => p.src?.includes('analytics'))
expect(hasPlugin).toBe(true)
})
test('useAnalytics Composableが公開される', async () => {
// 自動インポートが利用可能
const { data } = await useFetch('/api/analytics', {
method: 'POST',
body: { event: 'test', data: {} }
})
expect(data.value).toBeDefined()
})
test('アナリティクスエンドポイントがイベントを受け付ける', async () => {
const response = await $fetch('/api/analytics', {
method: 'POST',
body: { event: 'page_view', data: { path: '/' } }
})
expect(response.success).toBe(true)
})
})
出力:
TEXT
// 実行成功
7. 総合例:@megashop/analyticsの使用
VUE
<!-- pages/products/[id].vue - アナリティクスモジュールの使用 -->
<template>
<div v-if="product">
<h1>{{ product.name }}</h1>
<button
@click="handleAddToCart"
data-track="add_to_cart"
>
カートに追加
</button>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
// useAnalyticsはモジュールによって自動インポートされる
const { trackAddToCart, trackPageView } = useAnalytics()
// 手動トラッキング
onMounted(() => {
trackPageView(`/products/${route.params.id}`)
})
async function handleAddToCart() {
if (!product.value) return
trackAddToCart(product.value.id, product.value.name, product.value.price)
// ... カート追加ロジック
}
</script>
❓よくある質問
Q モジュールとプラグインの違いは何ですか?
A モジュールはビルドフェーズ (設定フェーズ)で実行され, プラグイン, コンポーネント, Composables, ルートを注入できます。プラグインはランタイム (アプリ起動時)に実行され, 初期化ロジックを処理します。モジュールにはプラグインを含めることができます。
Q モジュールの
runtimeディレクトリとは何ですか?A
runtimeにはランタイムコードが含まれます - アプリケーションの実行時にのみ実行され, ビルド設定には含まれないコードです。プラグイン, Composables, サーバーハンドラーはすべてruntime/に配置され, Nuxt Kitでは処理されません。Q ローカルモジュールの開発とデバッグはどのように行いますか?
A プロジェクトの
modules/ディレクトリにモジュールを作成し, nuxt.config.tsで~/modules/xxxとして参照してください。開発中はnuxt-module-build --stubでシンボリックリンクを生成し, コードの変更がリアルタイムで反映されます。Q モジュールは他のモジュールに依存できますか?
A はい。
setup内でinstallModule('@pinia/nuxt')を使用して依存モジュールをインストールしてください。Nuxtは重複を自動的に削除します。Q npmに公開するには何が必要ですか?
A
package.jsonでexportsとtypesを設定し, @nuxt/module-builderでビルドし, npm publishを実行してください。GitHub Actionsでの自動公開をお勧めします。Q モジュールのTypeScript型はどのようにエクスポートしますか?
A @nuxt/module-builderが自動的に型宣言を生成します。
package.jsonのexports.typesをdist/types.d.tsに設定すれば, ユーザーはインストール後に自動的に型ヒントを受け取れます。📖まとめ
- defineNuxtModule:
meta,defaults,setup関数でモジュールを定義 - モジュール機能:addPlugin, addImports, addServerHandler, addComponent等ですべてを注入
- ランタイムコードは
runtime/ディレクトリに配置され, ビルド設定フェーズには含まれない - @megashop/analyticsモジュール:自動トラッキング + useAnalytics Composable + サーバーAPI
- モジュールは@nuxt/module-builderでビルドし,
npm publishで公開
📝練習問題
- 基本問題 (難易度:⭐):最小のモジュールスケルトンを作成し, プラグインを登録して「Module loaded」と出力してください
- 応用問題 (難易度:⭐⭐):@megashop/analyticsモジュールを開発し, useAnalytics ComposableとサーバーAPIを実装してください
- チャレンジ (難易度:⭐⭐⭐):自動ページビュートラッキングと
data-trackクリックトラッキングを追加し, ユニットテストを書いて機能を検証してください
---|



