T-CREATOR

Micro Frontends 設計:`vite-plugin-federation` で分割可能な UI を構築

Micro Frontends 設計:`vite-plugin-federation` で分割可能な UI を構築

大規模な Web アプリケーション開発では、複数のチームが同時に作業を進める必要があります。しかし、単一のモノリシックなフロントエンドでは、コードの競合や依存関係の管理が煩雑になりますよね。そこで注目されているのが、Micro Frontends というアーキテクチャパターンです。

この記事では、Vite と Module Federation を組み合わせた vite-plugin-federation を使って、分割可能な UI を構築する方法を解説します。実際のコード例とともに、段階的に実装を進めていきましょう。

背景

Micro Frontends とは何か

Micro Frontends は、フロントエンドアプリケーションを独立した小さな機能単位(マイクロアプリケーション)に分割するアーキテクチャパターンです。バックエンドにおける「マイクロサービス」の概念をフロントエンドに適用したものと言えるでしょう。

このアプローチでは、各チームが独自の技術スタックやデプロイサイクルを持ちながら、統合されたユーザー体験を提供できます。例えば、EC サイトであれば「商品一覧」「カート」「決済」といった機能単位でアプリケーションを分割し、それぞれ異なるチームが開発を担当できるのです。

Module Federation の役割

Module Federation は、Webpack 5 で導入された革新的な機能です。これにより、異なるビルドから生成されたモジュールを実行時に動的に読み込み、共有できるようになりました。

従来の方法では、共通ライブラリを npm パッケージとして公開し、各アプリケーションでインストールする必要がありました。しかし Module Federation を使えば、実行時に必要なモジュールだけを動的に読み込めるため、ビルドサイズの削減やデプロイの柔軟性が大幅に向上します。

以下の図で、従来のアプローチと Module Federation の違いを確認しましょう。

mermaidflowchart TB
    subgraph traditional["従来の方式"]
        app1["アプリA"] -->|npm install| lib1["共通ライブラリ"]
        app2["アプリB"] -->|npm install| lib2["共通ライブラリ"]
        app3["アプリC"] -->|npm install| lib3["共通ライブラリ"]
    end

    subgraph federation["Module Federation"]
        host["Host アプリ"] -->|実行時読み込み| remote1["Remote アプリA"]
        host -->|実行時読み込み| remote2["Remote アプリB"]
        host -->|実行時読み込み| remote3["Remote アプリC"]
        remote1 -.->|共有| shared["React など<br/>共有依存関係"]
        remote2 -.->|共有| shared
        remote3 -.->|共有| shared
    end

この図から分かるように、Module Federation では各アプリケーションが必要なモジュールを実行時に読み込み、共通の依存関係を効率的に共有できます。

Vite と vite-plugin-federation の登場

Vite は、次世代のフロントエンドビルドツールとして急速に普及しています。ES モジュールを活用した高速な開発サーバーと、最適化されたプロダクションビルドが特徴ですね。

vite-plugin-federation は、この Vite 環境で Module Federation を利用できるようにするプラグインです。Webpack ベースの Module Federation と同様の機能を、Vite のエコシステムで実現できるようになりました。

課題

モノリシックなフロントエンドの問題点

大規模なフロントエンドアプリケーションを単一のコードベースで管理すると、以下のような課題が発生します。

開発効率の低下: 複数のチームが同じリポジトリで作業すると、コードの競合が頻発します。マージの調整だけで多くの時間が失われ、開発スピードが大幅に低下してしまうのです。

ビルド時間の増加: アプリケーションが大きくなるにつれて、ビルドに要する時間も増加します。小さな変更でも全体のビルドが必要になり、CI/CD パイプラインのボトルネックになりがちです。

技術的負債の蓄積: 異なる機能が密結合すると、リファクタリングや技術スタックの更新が困難になります。新しいライブラリの導入も、全体への影響を考慮する必要があり、慎重にならざるを得ません。

チーム間の依存関係管理

複数チームで開発を進める際、共通コンポーネントやライブラリの管理が課題となります。バージョンの不一致や、アップデートのタイミング調整など、調整コストが増大するでしょう。

また、各チームが独自のリリースサイクルを持ちたい場合でも、モノリシックな構造では全体のリリースに合わせる必要があります。これは開発の柔軟性を大きく損なう要因です。

以下の図で、モノリシック構造における課題を可視化しました。

mermaidflowchart TD
    mono["モノリシックアプリ"] --> team1["チームA<br/>商品機能"]
    mono --> team2["チームB<br/>カート機能"]
    mono --> team3["チームC<br/>決済機能"]

    team1 -.->|コード競合| conflict["マージ競合"]
    team2 -.->|コード競合| conflict
    team3 -.->|コード競合| conflict

    mono -->|長いビルド時間| build["ビルド<br/>ボトルネック"]
    mono -->|密結合| debt["技術的負債"]

    conflict -->|調整コスト| delay["開発遅延"]
    build -->|待機時間| delay
    debt -->|リファクタ困難| delay

この図が示すように、モノリシック構造では複数の課題が絡み合い、開発効率を低下させてしまいます。

解決策

vite-plugin-federation による分割戦略

vite-plugin-federation を活用することで、アプリケーションを以下のように分割できます。

Host アプリケーション: メインのアプリケーションで、他のマイクロフロントエンド(Remote)を統合する役割を担います。ルーティングやレイアウトなど、全体の枠組みを提供するのです。

Remote アプリケーション: 独立した機能単位のアプリケーションです。それぞれが独自のリポジトリを持ち、独立してビルド・デプロイできます。必要に応じて Host から動的に読み込まれるでしょう。

この構造により、各チームは自分たちの担当する Remote アプリケーションに集中でき、他チームとの依存関係を最小限に抑えられます。

アーキテクチャの全体像

以下の図で、vite-plugin-federation を使った Micro Frontends アーキテクチャの全体像を示します。

mermaidflowchart TB
    user["ユーザー"] -->|アクセス| host["Host アプリ<br/>localhost:3000"]

    host -->|動的読み込み| remote1["Remote A<br/>商品一覧<br/>localhost:3001"]
    host -->|動的読み込み| remote2["Remote B<br/>カート<br/>localhost:3002"]
    host -->|動的読み込み| remote3["Remote C<br/>決済<br/>localhost:3003"]

    remote1 -->|expose| components1["ProductList<br/>コンポーネント"]
    remote2 -->|expose| components2["Cart<br/>コンポーネント"]
    remote3 -->|expose| components3["Checkout<br/>コンポーネント"]

    host -.->|共有| shared["React, React-DOM<br/>共通依存関係"]
    remote1 -.->|共有| shared
    remote2 -.->|共有| shared
    remote3 -.->|共有| shared

この図から、Host が各 Remote を動的に読み込み、共通の依存関係を効率的に共有している様子が分かります。各 Remote は独立してデプロイでき、Host は必要なタイミングで最新版を取得できるのです。

主要な設定項目

vite-plugin-federation の設定では、以下の項目が重要になります。

name: アプリケーションの一意な名前を指定します。他のアプリケーションから参照される際の識別子となるでしょう。

filename: Module Federation のマニフェストファイル名です。通常は remoteEntry.js を使用します。

exposes: 他のアプリケーションに公開するモジュールを定義します。キーがモジュール名、値がファイルパスとなります。

remotes: 読み込む Remote アプリケーションを定義します。名前と URL を指定し、実行時に動的に読み込まれるのです。

shared: 複数のアプリケーション間で共有する依存関係を指定します。React などのライブラリは重複して読み込まれないよう、ここで管理しましょう。

具体例

それでは、実際に vite-plugin-federation を使った Micro Frontends アプリケーションを構築していきます。EC サイトを例に、「Host アプリ」「商品一覧 Remote」「カート Remote」という 3 つのアプリケーションを作成しましょう。

プロジェクトの初期化

まず、各アプリケーション用のディレクトリを作成し、Vite プロジェクトを初期化します。

bash# プロジェクトルートディレクトリを作成
mkdir micro-frontends-demo
cd micro-frontends-demo

# Host アプリケーション
yarn create vite host --template react-ts

# Remote アプリケーション(商品一覧)
yarn create vite remote-products --template react-ts

# Remote アプリケーション(カート)
yarn create vite remote-cart --template react-ts

次に、各プロジェクトで vite-plugin-federation をインストールします。

bash# Host アプリケーション
cd host
yarn add -D @originjs/vite-plugin-federation

# Remote アプリケーション(商品一覧)
cd ../remote-products
yarn add -D @originjs/vite-plugin-federation

# Remote アプリケーション(カート)
cd ../remote-cart
yarn add -D @originjs/vite-plugin-federation

これで各プロジェクトの基盤が整いました。次は設定ファイルを構成していきます。

Remote アプリケーション(商品一覧)の設定

商品一覧機能を提供する Remote アプリケーションを作成します。まず、公開するコンポーネントを実装しましょう。

typescript// remote-products/src/components/ProductList.tsx

import { useState } from 'react';

// 商品データの型定義
interface Product {
  id: number;
  name: string;
  price: number;
  image: string;
}

// 商品一覧コンポーネント
const ProductList = () => {
  // サンプル商品データ
  const [products] = useState<Product[]>([
    { id: 1, name: 'ノートPC', price: 120000, image: '💻' },
    { id: 2, name: 'マウス', price: 3000, image: '🖱️' },
    { id: 3, name: 'キーボード', price: 8000, image: '⌨️' },
    { id: 4, name: 'モニター', price: 35000, image: '🖥️' },
  ]);

  return (
    <div style={{ padding: '20px' }}>
      <h2>商品一覧</h2>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns:
            'repeat(auto-fill, minmax(200px, 1fr))',
          gap: '20px',
          marginTop: '20px',
        }}
      >
        {products.map((product) => (
          <div
            key={product.id}
            style={{
              border: '1px solid #ddd',
              borderRadius: '8px',
              padding: '16px',
              textAlign: 'center',
            }}
          >
            <div
              style={{
                fontSize: '48px',
                marginBottom: '12px',
              }}
            >
              {product.image}
            </div>
            <h3 style={{ margin: '8px 0' }}>
              {product.name}
            </h3>
            <p
              style={{
                fontSize: '18px',
                fontWeight: 'bold',
                color: '#2563eb',
              }}
            >
              ¥{product.price.toLocaleString()}
            </p>
            <button
              style={{
                marginTop: '12px',
                padding: '8px 16px',
                backgroundColor: '#2563eb',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
              }}
            >
              カートに追加
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default ProductList;

このコンポーネントは、商品の一覧を表示し、カートへの追加ボタンを提供します。次に、このコンポーネントを外部に公開するための設定を行いましょう。

typescript// remote-products/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      // アプリケーション名
      name: 'remote_products',
      // マニフェストファイル名
      filename: 'remoteEntry.js',
      // 外部に公開するモジュール
      exposes: {
        './ProductList': './src/components/ProductList.tsx',
      },
      // 共有する依存関係
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
      },
    }),
  ],
  // 開発サーバーのポート設定
  server: {
    port: 3001,
    cors: true,
  },
  // ビルド設定
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

この設定により、ProductList コンポーネントが .​/​ProductList というパスで外部に公開されます。singleton: true を指定することで、React が複数回読み込まれるのを防ぎ、メモリ効率を向上させているのです。

Remote アプリケーション(カート)の設定

次に、カート機能を提供する Remote アプリケーションを作成します。

typescript// remote-cart/src/components/Cart.tsx

import { useState } from 'react';

// カート内アイテムの型定義
interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

// カートコンポーネント
const Cart = () => {
  // カート内のアイテム(サンプルデータ)
  const [items, setItems] = useState<CartItem[]>([
    { id: 1, name: 'ノートPC', price: 120000, quantity: 1 },
    { id: 2, name: 'マウス', price: 3000, quantity: 2 },
  ]);

  // 合計金額の計算
  const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  // アイテムの削除処理
  const removeItem = (id: number) => {
    setItems(items.filter((item) => item.id !== id));
  };

  return (
    <div
      style={{
        padding: '20px',
        maxWidth: '600px',
        margin: '0 auto',
      }}
    >
      <h2>ショッピングカート</h2>
      {items.length === 0 ? (
        <p
          style={{
            textAlign: 'center',
            color: '#666',
            marginTop: '40px',
          }}
        >
          カートが空です
        </p>
      ) : (
        <>
          <div style={{ marginTop: '20px' }}>
            {items.map((item) => (
              <div
                key={item.id}
                style={{
                  display: 'flex',
                  justifyContent: 'space-between',
                  alignItems: 'center',
                  padding: '16px',
                  border: '1px solid #ddd',
                  borderRadius: '8px',
                  marginBottom: '12px',
                }}
              >
                <div>
                  <h3 style={{ margin: '0 0 8px 0' }}>
                    {item.name}
                  </h3>
                  <p style={{ margin: 0, color: '#666' }}>
                    ¥{item.price.toLocaleString()} ×{' '}
                    {item.quantity}
                  </p>
                </div>
                <div style={{ textAlign: 'right' }}>
                  <p
                    style={{
                      margin: '0 0 8px 0',
                      fontSize: '18px',
                      fontWeight: 'bold',
                    }}
                  >
                    ¥
                    {(
                      item.price * item.quantity
                    ).toLocaleString()}
                  </p>
                  <button
                    onClick={() => removeItem(item.id)}
                    style={{
                      padding: '4px 12px',
                      backgroundColor: '#ef4444',
                      color: 'white',
                      border: 'none',
                      borderRadius: '4px',
                      cursor: 'pointer',
                      fontSize: '12px',
                    }}
                  >
                    削除
                  </button>
                </div>
              </div>
            ))}
          </div>
          <div
            style={{
              marginTop: '24px',
              padding: '20px',
              backgroundColor: '#f3f4f6',
              borderRadius: '8px',
            }}
          >
            <div
              style={{
                display: 'flex',
                justifyContent: 'space-between',
                fontSize: '20px',
                fontWeight: 'bold',
                marginBottom: '16px',
              }}
            >
              <span>合計:</span>
              <span style={{ color: '#2563eb' }}>
                ¥{total.toLocaleString()}
              </span>
            </div>
            <button
              style={{
                width: '100%',
                padding: '12px',
                backgroundColor: '#2563eb',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
                fontSize: '16px',
                fontWeight: 'bold',
              }}
            >
              購入手続きへ
            </button>
          </div>
        </>
      )}
    </div>
  );
};

export default Cart;

このコンポーネントは、カート内のアイテム一覧と合計金額を表示し、削除機能を提供します。次に設定ファイルを作成しましょう。

typescript// remote-cart/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      // アプリケーション名
      name: 'remote_cart',
      // マニフェストファイル名
      filename: 'remoteEntry.js',
      // 外部に公開するモジュール
      exposes: {
        './Cart': './src/components/Cart.tsx',
      },
      // 共有する依存関係
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
      },
    }),
  ],
  // 開発サーバーのポート設定
  server: {
    port: 3002,
    cors: true,
  },
  // ビルド設定
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

カート Remote も商品一覧と同様の構成で、ポート番号を 3002 に設定しています。これで 2 つの Remote アプリケーションが準備できました。

Host アプリケーションの設定

Host アプリケーションは、これら 2 つの Remote を統合する役割を担います。まず設定ファイルを作成しましょう。

typescript// host/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      // アプリケーション名
      name: 'host',
      // 読み込む Remote アプリケーション
      remotes: {
        // 商品一覧 Remote
        remoteProducts:
          'http://localhost:3001/assets/remoteEntry.js',
        // カート Remote
        remoteCart:
          'http://localhost:3002/assets/remoteEntry.js',
      },
      // 共有する依存関係
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
      },
    }),
  ],
  // 開発サーバーのポート設定
  server: {
    port: 3000,
  },
  // ビルド設定
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

この設定では、2 つの Remote アプリケーションを読み込むよう指定しています。次に、これらを実際に使用するコンポーネントを作成しましょう。

typescript// host/src/App.tsx

import { lazy, Suspense, useState } from 'react';

// Remote コンポーネントを遅延読み込み
// @ts-ignore - Module Federation の型定義が不完全なため
const ProductList = lazy(
  () => import('remoteProducts/ProductList')
);
// @ts-ignore
const Cart = lazy(() => import('remoteCart/Cart'));

// タブの種類を定義
type Tab = 'products' | 'cart';

function App() {
  // 現在のアクティブタブを管理
  const [activeTab, setActiveTab] =
    useState<Tab>('products');

  return (
    <div
      style={{
        minHeight: '100vh',
        backgroundColor: '#f9fafb',
      }}
    >
      {/* ヘッダー */}
      <header
        style={{
          backgroundColor: '#1e293b',
          color: 'white',
          padding: '20px',
          boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
        }}
      >
        <h1 style={{ margin: 0, fontSize: '24px' }}>
          Micro Frontends EC サイト
        </h1>
        <p
          style={{
            margin: '8px 0 0 0',
            opacity: 0.8,
            fontSize: '14px',
          }}
        >
          vite-plugin-federation デモアプリケーション
        </p>
      </header>

      {/* ナビゲーションタブ */}
      <nav
        style={{
          backgroundColor: 'white',
          borderBottom: '1px solid #e5e7eb',
          padding: '0 20px',
        }}
      >
        <div style={{ display: 'flex', gap: '8px' }}>
          <button
            onClick={() => setActiveTab('products')}
            style={{
              padding: '16px 24px',
              border: 'none',
              borderBottom:
                activeTab === 'products'
                  ? '3px solid #2563eb'
                  : '3px solid transparent',
              backgroundColor: 'transparent',
              color:
                activeTab === 'products'
                  ? '#2563eb'
                  : '#6b7280',
              fontWeight:
                activeTab === 'products'
                  ? 'bold'
                  : 'normal',
              cursor: 'pointer',
              fontSize: '16px',
            }}
          >
            商品一覧
          </button>
          <button
            onClick={() => setActiveTab('cart')}
            style={{
              padding: '16px 24px',
              border: 'none',
              borderBottom:
                activeTab === 'cart'
                  ? '3px solid #2563eb'
                  : '3px solid transparent',
              backgroundColor: 'transparent',
              color:
                activeTab === 'cart'
                  ? '#2563eb'
                  : '#6b7280',
              fontWeight:
                activeTab === 'cart' ? 'bold' : 'normal',
              cursor: 'pointer',
              fontSize: '16px',
            }}
          >
            カート
          </button>
        </div>
      </nav>

      {/* メインコンテンツ */}
      <main style={{ padding: '20px' }}>
        <Suspense
          fallback={
            <div
              style={{
                textAlign: 'center',
                padding: '60px 20px',
                color: '#6b7280',
              }}
            >
              <p style={{ fontSize: '18px' }}>
                読み込み中...
              </p>
            </div>
          }
        >
          {activeTab === 'products' && <ProductList />}
          {activeTab === 'cart' && <Cart />}
        </Suspense>
      </main>

      {/* フッター */}
      <footer
        style={{
          marginTop: '60px',
          padding: '20px',
          textAlign: 'center',
          color: '#6b7280',
          borderTop: '1px solid #e5e7eb',
        }}
      >
        <p style={{ margin: 0, fontSize: '14px' }}>
          Powered by vite-plugin-federation
        </p>
      </footer>
    </div>
  );
}

export default App;

このコードでは、lazy() を使って Remote コンポーネントを遅延読み込みしています。Suspense でラップすることで、読み込み中の表示を制御できるのです。

TypeScript の型定義

Module Federation で読み込むモジュールの型定義を追加します。これにより、TypeScript のエラーを回避できます。

typescript// host/src/vite-env.d.ts

/// <reference types="vite/client" />

// Module Federation の型定義
declare module 'remoteProducts/ProductList' {
  const ProductList: React.ComponentType;
  export default ProductList;
}

declare module 'remoteCart/Cart' {
  const Cart: React.ComponentType;
  export default Cart;
}

これで、TypeScript がインポート文を正しく認識できるようになりました。

アプリケーションの起動

それでは、実際にアプリケーションを起動してみましょう。3 つのターミナルウィンドウを開き、それぞれのアプリケーションを起動します。

bash# ターミナル 1: Remote(商品一覧)を起動
cd remote-products
yarn install
yarn dev

# ターミナル 2: Remote(カート)を起動
cd remote-cart
yarn install
yarn dev

# ターミナル 3: Host アプリケーションを起動
cd host
yarn install
yarn dev

すべてのアプリケーションが起動したら、ブラウザで http:​/​​/​localhost:3000 にアクセスしてください。商品一覧とカートのタブを切り替えることで、それぞれの Remote コンポーネントが動的に読み込まれる様子を確認できます。

以下の図で、実行時のモジュール読み込みフローを示します。

mermaidsequenceDiagram
    participant User as ユーザー
    participant Host as Host アプリ<br/>(localhost:3000)
    participant RemoteP as Remote 商品一覧<br/>(localhost:3001)
    participant RemoteC as Remote カート<br/>(localhost:3002)

    User->>Host: ページアクセス
    Host->>Host: Host アプリ初期化

    User->>Host: 商品一覧タブクリック
    Host->>RemoteP: remoteEntry.js 要求
    RemoteP->>Host: マニフェスト返却
    Host->>RemoteP: ProductList モジュール要求
    RemoteP->>Host: コンポーネント返却
    Host->>User: 商品一覧表示

    User->>Host: カートタブクリック
    Host->>RemoteC: remoteEntry.js 要求
    RemoteC->>Host: マニフェスト返却
    Host->>RemoteC: Cart モジュール要求
    RemoteC->>Host: コンポーネント返却
    Host->>User: カート表示

この図から、ユーザーの操作に応じて必要なモジュールだけが動的に読み込まれることが分かります。初回アクセス時には全モジュールを読み込む必要がなく、パフォーマンスが向上するのです。

本番環境向けビルド

開発環境での動作を確認したら、本番環境向けにビルドしてみましょう。

bash# Remote アプリケーション(商品一覧)をビルド
cd remote-products
yarn build

# Remote アプリケーション(カート)をビルド
cd ../remote-cart
yarn build

# Host アプリケーションをビルド
cd ../host
yarn build

ビルド後のファイルは各プロジェクトの dist ディレクトリに生成されます。それぞれを静的ホスティングサービスにデプロイすれば、本番環境で利用できるようになります。

デプロイ時の注意点

本番環境にデプロイする際は、以下の点に注意してください。

Remote の URL を環境変数化: Host アプリケーションの設定で、Remote の URL を環境変数として管理しましょう。開発環境と本番環境で異なる URL を使用できるようになります。

typescript// host/vite.config.ts(環境変数を使用した例)

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'host',
      remotes: {
        remoteProducts:
          process.env.VITE_REMOTE_PRODUCTS_URL ||
          'http://localhost:3001/assets/remoteEntry.js',
        remoteCart:
          process.env.VITE_REMOTE_CART_URL ||
          'http://localhost:3002/assets/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
  server: {
    port: 3000,
  },
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

CORS 設定: 異なるドメインから Remote を読み込む場合、CORS(Cross-Origin Resource Sharing)の設定が必要です。サーバー側で適切なヘッダーを返すよう設定してください。

キャッシュ戦略: remoteEntry.js ファイルは頻繁に更新される可能性があるため、キャッシュ期間を短く設定するか、ファイル名にバージョン番号を含めることをお勧めします。

実装のポイント

この実装例から、以下のポイントが重要だと分かります。

独立したビルド: 各 Remote アプリケーションは独立してビルドでき、他のアプリケーションへの影響を最小限に抑えられます。チームごとに異なるリリースサイクルを持てるのは大きなメリットですね。

共有依存関係の管理: React などの共通ライブラリを shared に指定することで、重複を避け、バンドルサイズを削減できます。singleton: true を設定すると、バージョンの不一致を防げるでしょう。

遅延読み込み: lazy()Suspense を組み合わせることで、必要なタイミングでモジュールを読み込めます。初期ロード時間が短縮され、ユーザー体験が向上するのです。

まとめ

vite-plugin-federation を使った Micro Frontends アーキテクチャにより、大規模フロントエンドアプリケーションの開発効率を大幅に向上できます。

各チームが独立して開発・デプロイできるため、コードの競合やビルド時間の増加といった課題を解決できるでしょう。Module Federation による実行時の動的読み込みは、従来の npm パッケージ方式と比べて、デプロイの柔軟性やバンドルサイズの最適化において優れています。

ただし、この手法にも注意点があります。アプリケーション間の通信やステート管理には別途設計が必要ですし、Remote の URL 管理やバージョン管理も慎重に行う必要があるでしょう。

それでも、複数チームでの並行開発や、段階的な技術スタック移行を実現したい場合には、非常に有効なアプローチと言えます。まずは小規模なプロジェクトで試してみて、チームの開発スタイルに合うか検証してみてはいかがでしょうか。

関連リンク