404 Not Found

404 Not Found


nginx

コンポーネントとレイアウト

Charlieのチームは5人の開発者で構成されていますが, コンポーネントの書き方がバラバラです。手動でインポートする人もいれば, グローバル登録する人もいて, 命名規約も統一されていません。Aliceはページが不要なコンポーネントを大量に読み込んでおり, ファーストビューの読み込みが遅くなっています。Bobは管理ダッシュボードに別のレイアウトを使いたいと思っていますが, 切り替え方がわかりません。

1. 学ぶ内容


2. チームの実話

(1) ペインポイント:乱雑なコンポーネント管理

Charlieの5人チームはバラバラに作業しています。Aliceは各コンポーネントを手動インポートし, Bobはグローバル登録を使ってバンドルサイズを肥大化させ, 新入社員はコンポーネント名がわかりません。1つのProductCardを異なるファイルで3つの異なる方法で参照しています。

(2) Nuxt 3自動インポートによる解決策

Nuxt 3はcomponents/ディレクトリを自動スキャンし, ルールに従ってコンポーネント名を生成し, 全員が一貫した方法で参照できるようにします:

VUE
<!-- インポート不要, そのまま使う -->
<ProductCard :product="item" />

(3) 効果:標準化+サイズ削減

チームのコンポーネント参照方法が標準化されました。「Lazy」プレフィックスにより, 可視範囲のコンポーネントのみがファーストビューで読み込まれ, MegaShopのファーストビューJavaScriptサイズが40%削減されました。


3. 自動コンポーネントインポート

(1) ディレクトリスキャンと命名規約

100%
graph TB
    A[components/] --> B[ルートレベル]
    A --> C[サブディレクトリ]
    A --> D[ネストディレクトリ]
    B --> E[ProductCard.vue → ProductCard]
    C --> F[product/Card.vue → ProductCard ①]
    D --> G[shop/product/Card.vue → ShopProductCard]
    style G fill:#ffe,stroke:#f90
⚠️ 注意:pathPrefix: falseを設定するとproduct/Card.vueCardとなり, 名前の競合が発生する可能性があります。デフォルトのプレフィックスを維持することを推奨します。

(2) 命名規約の比較

ファイルパス デフォルトコンポーネント名 pathPrefix: false
components/AppHeader.vue AppHeader AppHeader
components/product/Card.vue ProductCard Card
components/shop/ProductCard.vue ShopProductCard ProductCard
components/admin/user/Table.vue AdminUserTable Table

(1) ▶ サンプル:自動コンポーネントインポート

VUE
<!-- components/ProductCard.vue -->
<template>
  <div class="product-card">
    <img :src="product.image" :alt="product.name" />
    <h3>{{ product.name }}</h3>
    <p>${{ product.price }} USD</p>
    <button @click="$emit('add-to-cart', product)">カートに追加</button>
  </div>
</template>

<script setup lang="ts">
interface Product {
  id: number
  name: string
  price: number
  image: string
}

defineProps<{ product: Product }>()
defineEmits<{ 'add-to-cart': [product: Product] }>()
</script>

出力:

TEXT
// 実行成功

(2) ▶ サンプル:ページで自動インポートコンポーネントを使用

VUE
<!-- pages/products/index.vue -->
<template>
  <div>
    <h1>全商品</h1>
    <div class="grid">
      <!-- ProductCardは自動インポート -->
      <ProductCard
        v-for="p in products"
        :key="p.id"
        :product="p"
        @add-to-cart="handleAdd"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
const { data: products } = await useFetch('/api/products')
function handleAdd(product: any) {
  console.log('追加:', product.name)
}
</script>

出力:

TEXT
// 実行成功

4. レイアウトシステム

(1) レイアウトの仕組み

100%
graph TB
    A[app.vue] --> B[NuxtLayout]
    B --> C{現在のレイアウト}
    C -->|default| D[layouts/default.vue]
    C -->|sidebar| E[layouts/sidebar.vue]
    D --> F[NuxtPage - ページコンテンツ]
    E --> F

(2) app.vueとレイアウトの関係

VUE
<!-- app.vue - レイアウト対応のルートコンポーネント -->
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

(1) ▶ サンプル:デフォルトレイアウト

VUE
<!-- layouts/default.vue -->
<template>
  <div class="layout-default">
    <AppHeader />
    <main class="content">
      <!-- ページコンテンツはここにレンダリング -->
      <slot />
    </main>
    <AppFooter />
  </div>
</template>

出力:

TEXT
// 実行成功

(2) ▶ サンプル:サイドバーレイアウト

VUE
<!-- layouts/sidebar.vue -->
<template>
  <div class="layout-sidebar">
    <AppHeader />
    <div class="main-wrapper">
      <aside class="sidebar">
        <nav>
          <NuxtLink to="/admin">ダッシュボード</NuxtLink>
          <NuxtLink to="/admin/products">商品</NuxtLink>
          <NuxtLink to="/admin/orders">注文</NuxtLink>
        </nav>
      </aside>
      <main class="content">
        <slot />
      </main>
    </div>
  </div>
</template>

出力:

TEXT
// 実行成功

(3) ▶ サンプル:ページでのレイアウト切替

VUE
<!-- pages/admin/index.vue -->
<template>
  <div>
    <h1>管理ダッシュボード</h1>
    <p>ようこそ, Bobさん</p>
  </div>
</template>

<script setup lang="ts">
// このページでサイドバーレイアウトに切り替え
definePageMeta({
  layout: 'sidebar'
})
</script>

出力:

TEXT
// 実行成功

(3) レイアウトとネストルートの比較

項目 レイアウト ネストルーティング
定義場所 layouts/*.vue pages/parent.vue
コンテンツスロット <slot /> <NuxtPage />
切替方法 definePageMeta 自動 (ルーティング階層)
データ取得 非推奨 各自が必要なものを取得
ユースケース 全体的なページレイアウト ルーティング階層間のUI共有

5. コンポーネントの遅延読み込み

(1) 遅延読み込みの仕組み

Nuxt 3は各コンポーネントの「Lazy」バージョンを自動登録し, コンポーネントが初めてレンダリングされる時点でのみコードを読み込みます。

(1) ▶ サンプル:Lazyプレフィックスによる遅延読み込み

VUE
<template>
  <div>
    <!-- 即時読み込み - 初期バンドルに含まれる -->
    <AppHeader />

    <!-- Lazy読み込み - コード分割, 初回レンダリング時に読み込み -->
    <LazyProductCard
      v-for="p in products"
      :key="p.id"
      :product="p"
    />

    <!-- v-if付きLazy - 条件が真の時のみ読み込み -->
    <LazyProductReviewModal
      v-if="showReviewModal"
      :product-id="selectedProductId"
      @close="showReviewModal = false"
    />
  </div>
</template>

出力:

TEXT
// 実行成功

(2) ▶ サンプル:Suspenseによる非同期読み込み

VUE
<template>
  <div>
    <Suspense>
      <template #default>
        <LazyHeavyChart :data="chartData" />
      </template>
      <template #fallback>
        <div class="loading">チャートを読み込み中...</div>
      </template>
    </Suspense>
  </div>
</template>

出力:

TEXT
// 実行成功

(2) 遅延読み込み戦略の比較

戦略 実装方法 読み込みタイミング 適用シナリオ
即時読み込み <ProductCard /> ページ読み込み時 ファーストビューのコアコンポーネント
Lazyプレフィックス <LazyProductCard /> 初回レンダリング時 フォールド下のコンポーネント
v-if + Lazy <LazyModal v-if="show" /> 条件が真の時 ポップアップ/ドロワー
Suspense <Suspense>でラップ 非同期操作の準備完了時 「読み込み中」状態が必要な場合

6. PropsとEmitsのベストプラクティス

(1) ▶ サンプル:型安全なPropsとEmits

VUE
<!-- components/ProductCard.vue -->
<script setup lang="ts">
interface Product {
  id: number
  name: string
  price: number
  image: string
  inStock: boolean
}

// デフォルト値付きの型安全なprops
const props = withDefaults(
  defineProps<{
    product: Product
    showStock?: boolean
    currency?: string
  }>(),
  {
    showStock: true,
    currency: 'USD'
  }
)

// 型安全なemits
const emit = defineEmits<{
  'add-to-cart': [product: Product]
  'toggle-wishlist': [productId: number]
}>()
</script>

<template>
  <div class="card">
    <img :src="product.image" :alt="product.name" />
    <h3>{{ product.name }}</h3>
    <p>{{ product.price }} {{ currency }}</p>
    <p v-if="showStock">
      {{ product.inStock ? '在庫あり' : '在庫切れ' }}
    </p>
    <button @click="emit('add-to-cart', product)">カートに追加</button>
    <button @click="emit('toggle-wishlist', product.id)">お気に入り</button>
  </div>
</template>

出力:

TEXT
// 実行成功

7. 総合例:MegaShopレイアウトコンポーネントシステム

VUE
<!-- components/AppHeader.vue -->
<template>
  <header class="app-header">
    <NuxtLink to="/" class="logo">MegaShop</NuxtLink>
    <nav>
      <NuxtLink to="/products">商品</NuxtLink>
      <NuxtLink to="/categories">カテゴリ</NuxtLink>
    </nav>
    <div class="user-area">
      <NuxtLink to="/cart">カート ({{ cartCount }})</NuxtLink>
      <NuxtLink v-if="isLoggedIn" to="/admin">管理</NuxtLink>
      <NuxtLink v-else to="/login">ログイン</NuxtLink>
    </div>
  </header>
</template>

<script setup lang="ts">
const cart = useState<any[]>('cart', () => [])
const cartCount = computed(() => cart.value.length)
const isLoggedIn = useState('isLoggedIn', () => false)
</script>
VUE
<!-- components/AppFooter.vue -->
<template>
  <footer class="app-footer">
    <p>&copy; 2026 MegaShop. All rights reserved.</p>
    <nav>
      <NuxtLink to="/about">About</NuxtLink>
      <NuxtLink to="/contact">お問い合わせ</NuxtLink>
      <NuxtLink to="/privacy">プライバシーポリシー</NuxtLink>
    </nav>
  </footer>
</template>
VUE
<!-- layouts/default.vue -->
<template>
  <div class="layout-default">
    <AppHeader />
    <main>
      <slot />
    </main>
    <AppFooter />
  </div>
</template>

❓ よくある質問

Q コンポーネントが自動インポートされないのはなぜですか?
A ファイルがcomponents/ディレクトリにあるか, ファイル名が大文字で始まっているか, nuxt.config.tscomponents設定でパスが除外されていないかを確認してください。開発サーバーの再起動も試してみてください。
Q Lazyプレフィックスはコンポーネントのレンダリングを遅らせますか?
A いいえ。LazyはコンポーネントのJavaScriptコードの読み込みを遅らせるだけで, レンダリング後は正常に動作します。コード分割を行うものであり, レンダリングの遅延ではありません。
Q レイアウトでデータを取得できますか?
A 技術的には可能ですが, 推奨されません。レイアウトはUIフレームワークであり, データ取得はページまたはコンポーザブルで処理するべきです。レイアウトはレイアウト構造のみを担当します。
Q 1つのページで複数のレイアウトを使用できますか?
A いいえ。各ページはdefinePageMetaで1つのレイアウトのみ指定できます。異なるセクションで異なるレイアウトが必要な場合は, コンポーネントの組み合わせで実現してください。
Q definePageMetaはscript setupの最上位になければなりませんか?
A はい。definePageMetaはコンパイラマクロであり, <script setup>の最上位で直接呼び出す必要があります。条件文や関数の中に置くことはできません。
Q components/の下に深すぎるサブディレクトリにコンポーネントを配置するとパフォーマンスに影響しますか?
A ランタイムパフォーマンスには影響しません。Nuxt 3は実際に使用されるコンポーネントのみをバンドルするからです。ただし, ディレクトリ階層が深すぎると命名が複雑になるため, 2階層までに制限することを推奨します。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度:⭐):AppHeaderとAppFooterコンポーネントを作成し, デフォルトレイアウトで使用してください。
  2. 応用問題 (難易度:⭐⭐):サイドバーレイアウトを作成し, 管理ページでdefinePageMetaを使って2つのレイアウトを切り替え, 結果を比較してください。
  3. チャレンジ (難易度:⭐⭐⭐):Lazyプレフィックスとv-ifを使って, クリック時のみ読み込まれる商品詳細ポップアップコンポーネントを実装し, Suspenseで読み込み中状態を表示してください。

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%