こんにちは!日々のフロントエンド開発、本当にお疲れ様です。
今回は、現代のフロントエンド開発において多くのチームが頭を悩ませる「マイクロフロントエンド」と、その核心である「ビルドツール選定」のお話です。
「SPA(シングルページアプリケーション)が巨大化してビルドが遅くなったから、画面ごとにリポジトリを分割したい」
「でも、共通のコンポーネントやデザインシステムを毎回ビルドし直すのは非効率だし、Webpackの Module Federation のように、ランタイムでモジュールを共有したい!」
そう思ったとき、多くの開発者が Vite の圧倒的な開発スピードの速さに魅了されつつも、Webpackのような枯れたModule Federationの機能がないことに絶望し、「やっぱりWebpackに戻るしかないのか……」とため息をつきます。
ちょっと待ってください。Viteであっても、ロールアップ(Rollup)の仕組みと巧みな型定義(d.ts)の共有を組み合わせれば、Module Federationに負けない「優雅で堅牢な疎結合フロントエンド」が作れるのです。
今回は、Vite単体でこの難題を突破し、毎日のコーディングとビルドを劇的に快適にするアーキテクチャの全貌を、一緒に紐解いていきましょう!
—
なぜViteでの「モジュール共有」は一筋縄ではいかないのか?
まず、敵を知ることから始めましょう。WebpackのModule Federationは、複数の独立したビルド成果物(アプリ)が、ブラウザのランタイム上で互いのモジュールを「非同期読み込み(`import()`)」し合える魔法のような仕組みです。
一方、Viteの裏側で動いているのは、極限まで高速化されたバンドラである Rollup です。
Viteはデフォルトで「すべてをきれいにバンドルして一枚のHTMLに最適化する」ことを得意としています。そのため、別アプリがビルドしたモジュールを、型安全性を保ったままランタイムでダイナミックに引っ張ってこようとすると、以下の2つの壁にぶつかります。
1. コードの共有(Runtime): 相手アプリのコードをビルド時に巻き込まず、ブラウザ上でどうやって外部化(External)するか。
2. 型の共有(Types): TypeScriptを使っている場合、外部化したモジュールの型定義(`.d.ts`)がビルド時にないと、コンパイルエラーになってしまう問題。
この2つを綺麗に解決するのが、今回お伝えする 「Viteの `external` オプション × 外部CDN/ホ스팅 × Federated Types(型定義の同期)」 というアプローチです。これをマスターすれば、あなたのチームの開発環境は次のステージへ進化します。
—
今回目指すアーキテクチャの全体像
今回は分かりやすくするため、以下の2つの独立したViteプロジェクトを想定します。
1. `host-app`(ホストアプリ):
全体を統括するメインの親アプリケーション。
2. `remote-component`(リモートモジュール):
独立してデプロイされるボタンやウィジェットなどの子アプリケーション(今回はこれをホストから動的に呼び出します)。
これらを、「リモート側をCDN等にビルド配置し、ホスト側はそれを読み込みつつ、型安全に開発する」 という環境に仕立て上げていきます。
—
1. リモート側(remote-component)の構築と設定
まずは、他のアプリに提供したいモジュール側(`remote-component`)の準備です。ここでは、Viteを使ってシンプルなUIコンポーネントをライブラリモードでビルドします。
プロジェクトの初期化と設定
Viteのプロジェクトを作成し、`vite.config.ts` を以下のように設定します。ここが最初の心臓部です。
// remote-component/vite.config.ts
import { defineConfig } from ‘vite’;
import react from ‘@vitejs/plugin-react’;
import { resolve } from ‘path’;
export default defineConfig({
plugins: [react()],
build: {
// ライブラリモードを有効化し、単体で動作するJSファイルとして出力する
lib: {
entry: resolve(__dirname, ‘src/index.ts’), // 公開するエントリーポイント
name: ‘RemoteComponent’, // グローバル変数名(UMD等の場合)
fileName: ‘remote-component’, // 出力ファイル名
formats: [‘es’] // モダンブラウザ向けのES Modules形式で出力
},
rollupOptions: {
// ホスト側(Reactなど)と二重にバンドルされるのを防ぐため、共通ライブラリを外部化する
external: [‘react’, ‘react-dom’],
output: {
globals: {
react: ‘React’,
react-dom: ‘ReactDOM’
}
}
}
}
});
公開するコンポーネントの作成
実際にホスト側へ提供するコンポーネントを用意します。
// remote-component/src/Button.tsx
import React from ‘react’;
interface RemoteButtonProps {
label: string;
onClick: () => void;
}
export const RemoteButton: React.FC
return (
);
};
// remote-component/src/index.ts
// 外部へ露出させるモジュールをすべてここでエクスポートする
export { RemoteButton } from ‘./Button’;
【重要】型定義(d.ts)の生成設定
TypeScriptの恩恵を最大限に受けるため、ビルド時に型定義ファイル(`.d.ts`)を一緒に生成させます。これがないと、ホスト側で型エラーが起きてしまいます。
`package.json` のビルドスクリプトを次のように書き換えます。ここでは `vite-plugin-dts` などのプラグインを使うのが最もスマートです。
// remote-component/package.json の抜粋
{
“scripts”: {
“dev”: “vite”,
// 型定義を自動生成しつつ、ライブラリとしてビルドを実行する
“build”: “tsc && vite build”
}
}
これで、`npm run build` を実行すると、`dist/` ディレクトリに `remote-component.js`(JavaScript本体)と、型定義ファイル群が生成されます。これをCDNや専用の静的ストレージにデプロイします。
—
2. ホスト側(host-app)での受け入れと外部モジュール化
次に、このリモートモジュールを自分のアプリに取り込む `host-app` 側を作ります。ここがViteで最も工夫が必要なポイントです。
Viteの `rollupOptions.external` による外部化
ホスト側は、ビルド時にリモートモジュールのコードを自分のバンドルに含めたくありません(含めると、リモート側を更新してもホストを再ビルドしないといけなくなるため、疎結合が崩れます)。
そこで、Viteの `build.rollupOptions.external` に、リモートモジュールのパスを指定します。
// host-app/vite.config.ts
import { defineConfig } from ‘vite’;
import react from ‘@vitejs/plugin-react’;
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
// 外部CDNや別サーバーから読み込むため、バンドル対象から除外する
external: [
‘http://localhost:4173/remote-component.js’ // 開発時はローカルサーバーのURLを指定
]
}
}
});
型定義(Federated Types)の同期
「外部化したはいいが、TypeScriptが `http://…` から読み込まれるモジュールの型を知らない」という問題が発生します。
これを解決するため、リモート側でビルドされた型定義(`.d.ts`)を、ホスト側の開発環境(例: `src/@types/remote/` やモノレポであればワークスペース間)に配置、または型定義用のダッシュボードやS3経由で同期させます。
例えば、ホスト側の `src/@types/remote.d.ts` として以下のように宣言します。
// host-app/src/@types/remote.d.ts
// URLからインポートされるモジュールの型をTypeScriptに教える
declare module ‘http://localhost:4173/remote-component.js’ {
export from ‘../../remote-component/src/Button’; // リモートの型定義を紐付け
}
ホスト側での動的インポート(Dynamic Import)の実装
準備が整いました!あとは、ホスト側のコンポーネントから、ブラウザのランタイムで非同期にリモートモジュールを読み込みます。
// host-app/src/App.tsx
import React, { useEffect, useState } from ‘react’;
// 型安全を保ちつつ、外部URLからモジュールをダイナミックインポート
// ※ 実際の運用では、エラーハンドリングやローディング状態の管理を追加します
const loadRemoteButton = async () => {
const module = await import(/ @vite-ignore / ‘http://localhost:4173/remote-component.js’);
return module.RemoteButton;
};
export function App() {
const [RemoteButton, setRemoteButton] = useState
useEffect(() => {
loadRemoteButton().then((Comp) => {
setRemoteButton(() => Comp);
});
}, []);
return (
Vite Host Application
以下のボタンは、外部から独立して読み込まれています:
{RemoteButton ? (
/>
) : (
リモートコンポーネントを読み込み中…
)}
);
}
export default App;
(※ココがポイント:Viteが静的解析でURLインポートを解決しようとしてエラーを出さないよう、`/ @vite-ignore /` コメントを入れるのが実務における重要なハックです!)
—
精度高い動作確認の流れ
この仕組みが正しく動くか、ローカル環境でシミュレートしてみましょう。
1. リモート側の起動とビルド:
cd remote-component
npm run build
# プレビューサーバーを立ち上げて、4173ポートで配信する
npm run preview — –port 4173
2. ホスト側の起動:
別ターミナルを開き、ホストアプリを起動します。
cd host-app
npm run dev
3. ブラウザでの確認:
`http://localhost:5173`(ホスト側)にアクセスすると、別のポート(4173)でホスティングされているリモートコンポーネントが綺麗に描画され、クリックイベントも正常に発火します。
ネットワークタブを開いてみてください。ホストアプリのバンドルサイズを肥大化させることなく、必要な瞬間にリモートのJSが取得されている様子が確認できるはずです。これこそが、Viteが目指す軽快かつ疎結合なフロントエンドの姿です。
—
現場のシニアが教える「知っておくべき限界と突破口」
このアプローチは非常に強力ですが、アーキテクトとして正直に「限界」もお伝えしておかなければなりません。
- Reactなどの共有(Shared Dependencies)の罠:
ホストとリモートでそれぞれ別のバージョンのReactが読み込まれてしまうと、ReactのHooks(`useState`など)がクラッシュします。これを防ぐため、上記で設定したように `external: [‘react’]` を徹底し、グローバル(ウィンドウオブジェクト経由やCDNの同一インスタンス)でReactを共有する設計上の配慮が実務では必要になります。
- CORSの設定:
リモートモジュールを別ドメイン(CDNや別サブドメイン)でホストする場合、適切な `Access-Control-Allow-Origin` ヘッダーが設定されていないとブラウザにブロックされます。
もし、これらの共有管理やバージョンの自動解決を完全に自動化したい場合は、Vite向けのModule Federationプラグイン(例: `vite-plugin-federation` 等)を導入する選択肢も出てきます。しかし、今回紹介した「Rollupのexternal活用と型定義の同期」の基礎を知っていれば、ブラックボックスなプラグインに頼らずとも、自分たちのチームに合わせた堅牢なマイクロフロントエンド基盤をコントロールできるようになります。
—
まとめ
いかがでしたでしょうか?
「Viteだからマイクロフロントエンドは無理」ではなく、ツールの仕組み(Rollupの外部化とTypeScriptの型システム)を正しく理解し、パズルのピースを組み合わせることで、Webpackに劣らない疎結合なアーキテクチャは構築可能です。
これをマスターすれば、巨大なリポジトリのビルド待ち時間にイライラすることも、チーム間のデプロイ競合に悩まされることもなくなります。
ぜひ、あなたのプロジェクトでもこのアプローチを試して、毎日の開発を圧倒的に軽快で楽しいものにしてくださいね!