404 Not Found

404 Not Found


nginx

モジュール開発

CharlieはMegaShopのトラッキングロジック, エラー報告, パフォーマンス監視があちこちに散在していることに気づきました。AliceとBobの他のプロジェクトも同じ機能を必要としています。これらの共通機能をNuxtモジュールにカプセル化すれば, 一度開発すればどこでも再利用でき, npmに公開してコミュニティに提供することもできます。

1. 学ぶ内容


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モジュールライフサイクル

100%
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.jsonexportstypesを設定し, @nuxt/module-builderでビルドし, npm publishを実行してください。GitHub Actionsでの自動公開をお勧めします。
Q モジュールのTypeScript型はどのようにエクスポートしますか?
A @nuxt/module-builderが自動的に型宣言を生成します。package.jsonexports.typesdist/types.d.tsに設定すれば, ユーザーはインストール後に自動的に型ヒントを受け取れます。

📖まとめ


📝練習問題

  1. 基本問題 (難易度:⭐):最小のモジュールスケルトンを作成し, プラグインを登録して「Module loaded」と出力してください
  2. 応用問題 (難易度:⭐⭐):@megashop/analyticsモジュールを開発し, useAnalytics ComposableとサーバーAPIを実装してください
  3. チャレンジ (難易度:⭐⭐⭐):自動ページビュートラッキングとdata-trackクリックトラッキングを追加し, ユニットテストを書いて機能を検証してください

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%