こんにちは、テックリードの皆さん。日々のビルド待ち時間、あるいはモノレポの肥大化に伴うCIの遅延にイライラしていませんか?
フロントエンド開発において「マイクロフロントエンド」や「複数チームによる独立デプロイ」を志向した瞬間、私たちは決まってひとつの壁にぶつかります。それは、「Viteを使いたいのに、WebpackのModule Federationのような強力なランタイムモジュール共有機構がない」というジレンマです。
Vite(その背後にあるRollup)は、その圧倒的な開発サーバーの起動速度とビルドの効率性で私たちの開発体験を劇的に変えてくれました。しかし、標準ではWebpackのような動的なコード共有を苦手としています。
今回は、Viteの思想を曲げずに、Rollupの `external` オプション、CDN、そしてFederated Types(型定義の動的共有)を高度に組み合わせることで、「Vite単体でWebpackのModule Federationに匹敵する、いや、それ以上にクリーンで高速な疎結合フロントエンド環境」を構築する方法を、実践的なコードとアーキテクチャの解説とともに徹底伝授します。
ネットの海を漂う「マニュアルを翻訳しただけの記事」はここで終わりです。実務の現場で即座にチームを救う、プロフェッショナルの知見を見ていきましょう。
—
なぜViteでModule Federationが難しいのか?
WebpackのModule Federationは、ランタイム(ブラウザ上)でリモート側のコンテナを非同期ロードし、ホスト側とモジュールを共有します。このとき、Webpackは独自のマニフェスト生成と依存関係の解決を行います。
一方、Vite(Rollup)はES Modules(ESM)をベースにしており、基本的にはビルド時に依存関係が静的に解決されることを前提としています。Viteでこれをやろうとすると、以下の2つの壁に阻まれます。
1. ビルド時の解決とランタイムの乖離: ホスト側がビルドされる時点で、リモート側のモジュールが存在しない、あるいは型が一致しない。
2. 型(TypeScript)の断絶: ホストとリモートが別リポジトリの場合、リモート側のコードを変更した瞬間にホスト側のビルドが型エラーで壊れるか、逆に型チェックが効かなくなる。
この問題を解決する鍵が、「ビルド時外部化(Externalization)」と「Federated Types(型定義の独立配信)」の融合です。
—
アーキテクチャ全体像:Viteにおける「仮想Federation」
今回構築する仕組みのデータフローは以下の通りです。
[Remote App (Host/MFA)]
──(ビルド)──> [CDN / S3] (JSファイル + d.ts)
│
▼ (ランタイムでロード)
[Host App (Consumer)]
──(開発時)──> [Vite DevServer] (rollup-plugin-external-globals等で解決)
ホストアプリケーションは、リモートアプリケーションの実体をビルドに含めません。代わりに、CDN等にホストされたJSを指すようにし、TypeScriptの型定義はCI/CDパイプラインを通じて共有・取得します。
—
1. 実践:Vite設定の極意(Host & Remote)
まずは、リモートモジュールを消費する「Host側」と、モジュールを提供する「Remote側」のVite設定を見ていきます。
Remote側の `vite.config.ts`
リモート側は、自身を単一のエントリーポイントを持つライブラリとしてビルドし、さらに型定義を出力するように設定します。
import { defineConfig } from ‘vite’;
import react from ‘@vitejs/plugin-react’;
import dts from ‘vite-plugin-dts’;
import { resolve } from ‘path’;
export default defineConfig({
plugins: [
react(),
// TypeScriptの型定義(.d.ts)を一括生成して出力する神プラグイン
dts({
insertTypesEntry: true,
rollupTypes: true, // 複数の型定義を単一のファイルにバンドルする
}),
],
build: {
lib: {
// 共有するコンポーネントのエントリーポイント
entry: resolve(__dirname, ‘src/exposing/RemoteButton.tsx’),
name: ‘RemoteModule’,
formats: [‘es’], // 現代のブラウザ標準であるESM形式を指定
fileName: ‘remote-button’,
},
rollupOptions: {
// React本体などはホスト側と共有するため、リモート側のバンドルから除外する
external: [‘react’, ‘react-dom’],
output: {
globals: {
react: ‘React’,
‘react-dom’: ‘ReactDOM’,
},
},
},
},
});
Host側の `vite.config.ts`
ホスト側は、リモートモジュールを `external` に指定し、ブラウザのグローバル変数やCDN経由のESMとして解決させます。ここでは `vite-plugin-cdn-import` や独自のプラグインを組み合わせることで、開発環境と本番環境でシームレスに切り替えます。
import { defineConfig } from ‘vite’;
import react from ‘@vitejs/plugin-react’;
import externalGlobals from ‘rollup-plugin-external-globals’;
export default defineConfig({
plugins: [
react(),
{
…externalGlobals({
// 外部化されたモジュール名を、グローバル変数またはURLにマッピング
‘remote-app/RemoteButton’: ‘window.RemoteModule.RemoteButton’,
}),
enforce: ‘pre’, // 他のプラグインより先に解決させる
},
],
build: {
rollupOptions: {
// ビルド時にもリモートモジュールをバンドルに含めない
external: [‘remote-app/RemoteButton’],
},
},
});
—
2. チーム開発の生命線:Federated Typesの共有自動化
コードのビルドが通っても、TypeScriptの型が通らなければモダンなフロントエンド開発とは言えません。別リポジトリにあるリモートの型(`.d.ts`)を、ホスト側へどのように安全かつリアルタイムに届けるべきでしょうか?
実務では、npmプライベートレジストリを使うのが最も堅牢ですが、小中規模のマイクロフロントエンドであれば、GitHub Actions等のCI/CDとS3/CDNを組み合わせた「型定義の自動同期パイプライン」が圧倒的に軽量で高速です。
チーム開発用シェルスクリプト:型のプル・適用 (`sync-remote-types.sh`)
リモート側のビルド時に生成された `d.ts` を、ホスト側のプロジェクトへ自動で取り込むためのスクリプトをリポジトリの `scripts/` 配下に配置します。
!/bin/bash
set -e
リモートコンポーネントの型定義が置かれているCDN/S3のURL
REMOTE_TYPES_URL=”https://cdn.example.com/remote-app/remote-button.d.ts”
TARGET_DIR=”./src/@types/remote”
echo “==> リモートアプリの型定義を取得中: $REMOTE_TYPES_URL”
ディレクトリが存在しない場合は作成
mkdir -p “$TARGET_DIR”
型定義ファイルをダウンロードし、ホスト側の型定義領域に配置
curl -s “$REMOTE_TYPES_URL” -o “$TARGET_DIR/remote-button.d.ts”
echo “==> 型定義の同期が完了しました。ホスト側の型チェックが更新されます。”
これをホスト側の `package.json` の `prepare` スクリプトや、CIのビルド前ステップに組み込みます。
—
3. 実務で役立つ設定ファイル・ベストプラクティス構成
複数チームがバラバラにVite製マイクロフロントエンドを開発する際、設定の乖離を防ぎ、ガバナンスを効かせるためのモノレポ構成(Turborepo / pnpmワークスペース前提)のベストプラクティスを示します。
ルートの `pnpm-workspace.yaml`
packages:
- ‘apps/’ # ホストおよび独立したリモートアプリケーション群
- ‘packages/’ # 共通のデザインシステムやユーティリティ
チーム間で共有するVite設定のベース(`packages/vite-config-base/index.ts`)
各アプリでバラバラのVite設定を書かせず、共通のベース設定を強制します。
import { UserConfig } from ‘vite’;
// 組織全体で共通化するViteのセキュリティ・パフォーマンス最適化設定
export function createBaseConfig(appName: string): UserConfig {
return {
define: {
‘process.env.APP_NAME’: JSON.stringify(appName),
},
build: {
target: ‘esnext’,
sourcemap: true, // 本番環境でのデバッグを容易にするため常に有効化
minify: ‘esbuild’, // 高速なesbuildによるミニファイ
},
};
}
—
4. プロの隠しコマンド&開発効率を爆上げするVSCode設定
最後に、Viteベースのマイクロフロントエンド開発を極限まで加速させる、現場のテックリードがこっそり使っているテクニックを公開します。
VSCode推奨設定 (`.vscode/settings.json`)
複数のViteインスタンス(ホストとリモート)を同時に立ち上げて開発する際、TypeScriptの言語サーバーが混乱しないようにするための設定です。
{
// モノレポ環境において、個別のワークスペースごとに独立したTSサーバーを立てる
“typescript.tsserver.experimental.enableProjectDiagnostics”: true,
// 保存時の自動フォーマットを強制し、コードレビューの無駄な指摘をゼロにする
“editor.formatOnSave”: true,
“editor.defaultFormatter”: “esbenp.prettier-vscode”,
// ViteのHMR(Hot Module Replacement)を邪魔しないファイル監視の除外設定
“files.watcherExclude”: {
“/dist/“: true,
“/node_modules/“: true,
“/.git/objects/“: true
}
}
開発体験を高めるカスタムnpmスクリプト (`package.json`)
ホストとリモートを同時に起動し、かつ型定義の監視まで行うためのconcurrentlyを活用した定義例です。
{
“scripts”: {
“dev:all”: “concurrently \”pnpm –filter host dev\” \”pnpm –filter remote dev\””,
“types:sync”: “bash ./scripts/sync-remote-types.sh”,
“build:verify”: “pnpm types:sync && tsc –noEmit && vite build”
}
}
—
まとめ:Viteの速度を殺さずに、疎結合を手に入れろ
WebpackのModule Federationは強力ですが、設定の複雑さやビルドの重さに悩まされることも事実です。Viteの軽快な開発体験を手放したくない私たちにとって、「Rollupの外部化機能 + CDN + Federated Typesの組み合わせ」は、極めてクリーンで理解しやすい現代的な代替案となります。
ランタイムの複雑なマジックに頼るのではなく、ビルド成果物と型定義という「明確な境界線」を定義すること。これこそが、スケールするフロントエンドアーキテクチャの真髄です。
あなたのチームでも、この仕組みを取り入れて、ビルド待ち時間と依存関係の呪縛から解放された極上の開発環境を手に入れてください。