マイクロフロントエンドの実装:モジュールフェデレーション、iFrame、Webコンポーネントを解剖
Daniel Hayes
Full-Stack Engineer · Leapcell

はじめに
現代のWeb開発の状況は常に進化しており、スケーラブルで保守可能、かつ独立してデプロイ可能なアプリケーションを構築することへの関心が高まっています。フロントエンドアプリケーションが複雑化し、チームの規模が大きくなるにつれて、モノリスはしばしばボトルネックとなり、アジリティと開発者の生産性を妨げます。この課題は、マイクロサービス原則をフロントエンドに拡張するパラダイムであるマイクロフロントエンドアーキテクチャの採用を促進しました。大規模なフロントエンドアプリケーションをより小さく自律的な単位に分割することで、マイクロフロントエンドはより高い柔軟性、より速い開発サイクル、および改善されたチームの自律性を約束します。しかし、マイクロフロントエンドの実装戦略を正しく選択することが重要です。この記事では、マイクロフロントエンドを構築するための3つの著名なテクノロジー、すなわちモジュールフェデレーション、iFrame、Webコンポーネントについて、それらのアプローチ、ユースケースを比較し、この重要な決定をナビゲートするのに役立ちます。
マイクロフロントエンドにおけるコアコンセプト
各テクノロジーの詳細に入る前に、議論全体で参照されるマイクロフロントエンドアーキテクチャに関連するいくつかのコアコンセプトについて共通の理解を確立しましょう。
マイクロフロントエンド: Webアプリケーションが、独立して開発、デプロイ、管理できる多くの独立したフロントエンドアプリケーションで構成されるアーキテクチャスタイル。
ホストアプリケーション: 様々なマイクロフロントエンドをオーケストレーションおよび統合するメインアプリケーション。アプリケーション全体のエクスペリエンスのシェルまたはレイアウトを提供します。
リモートアプリケーション(または子マイクロフロントエンド): ホストアプリケーション内でロードおよび表示される独立したフロントエンドアプリケーション。
分離: マイクロフロントエンドが、他のマイクロフロントエンドやホストに意図せず影響を与えたり、影響を受けたりすることなく、独立して動作する程度。これには、JavaScript、CSS、グローバル状態の分離が含まれます。
ランタイム統合: アプリケーションがブラウザで実行されている間に、ホストアプリケーション内でマイクロフロントエンドをロードおよび表示するプロセス。
ビルドタイム統合: マイクロフロントエンドが、デプロイ前にビルドステップ中に結合およびバンドルされるプロセス。
共有依存関係: 複数のマイクロフロントエンドが必要とする可能性のある共通のライブラリまたはフレームワーク(例:React、Vue、Lodash)であり、理想的にはパフォーマンスの低下を避けるために一度だけロードされるべきものです。
モジュールフェデレーション
Webpack 5で導入されたモジュールフェデレーションは、独立してビルドされたアプリケーション間でコードと依存関係を共有するという課題に対処するために設計された、強力で比較的新しい機能です。これにより、異なるWebpackビルドが実行時に互いからモジュールを公開および消費できます。
仕組み
その核心において、モジュールフェデレーションは、「ホスト」アプリケーションが「リモート」アプリケーションからコードを動的にロードできるようにします。ホストとリモートの両方のアプリケーションは、本質的にはこれらの役割を果たすように設定されたWebpackビルドです。リモートアプリケーションは一部のモジュールを「フェデレーションモジュール」として公開し、ホストアプリケーションはそれらをローカルモジュールのように消費できます。これにとって重要なのは「共有モジュール」という概念であり、共通の依存関係(Reactやデザインシステムなど)は、複数のフェデレーションモジュールがそれらに依存していても、一度だけロードされるように分離できます。
実装例
ホストアプリケーションがリモートのProductApp
からProductDetail
コンポーネントをロードする必要があるシナリオを考えてみましょう。
リモート(ProductApp
のwebpack.config.js
):
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:8081/', // このアプリが提供される公開URL }, devServer: { port: 8081, }, plugins: [ new ModuleFederationPlugin({ name: 'productApp', filename: 'remoteEntry.js', exposes: { './ProductDetail': './src/components/ProductDetail', }, shared: { react: { singleton: true, requiredVersion: '^17.0.2' }, 'react-dom': { singleton: true, requiredVersion: '^17.0.2' } }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], // ... その他のWebpack設定 };
ホスト(HostApp
のwebpack.config.js
):
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:8080/', // このアプリが提供される公開URL }, devServer: { port: 8080, }, plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { productApp: 'productApp@http://localhost:8081/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: '^17.0.2' }, 'react-dom': { singleton: true, requiredVersion: '^17.0.2' } }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], // ... その他のWebpack設定 };
ホスト(HostApp
のsrc/App.js
):
import React, { Suspense } from 'react'; const ProductDetail = React.lazy(() => import('productApp/ProductDetail')); const App = () => { return ( <div> <h1>Host Application</h1> <Suspense fallback={<div>Loading Product Detail...</div>}> <ProductDetail productId="123" /> </Suspense> </div> ); }; export default App;
この例では、HostApp
は実行時にProductApp
からProductDetail
コンポーネントを動的にロードします。Reactなどの共有依存関係は、一度だけロードされるように最適化されます。
アプリケーションシナリオ
モジュールフェデレーションは、次のようなシナリオで優れています。
- 大規模で複雑なコンポーネントまたはサブアプリケーション全体を共有する必要がある場合。
- 共有ライブラリの最適化されたロードがパフォーマンスにとって重要である場合。
- チームがフロントエンド開発とデプロイメントに対して高いレベルの自律性を必要とする場合。
- フレームワークが異なるマイクロフロントエンド間で同じ、または互換性がある場合(モジュールフェデレーションは一般的にフレームワークの違いをうまく処理しますが、特にラッパーを使用する場合)。
- 複数のチームからの機能を組み合わせた単一ページアプリケーション(SPA)を構築している場合。
iFrames
iFrame(インラインフレーム)は、現在のHTMLドキュメント内に別のHTMLドキュメントを埋め込むことができる、従来のHTML要素です。各iFrameは独自のブラウザコンテキスト内で動作するため、高いレベルの分離を提供します。
仕組み
iFrameは基本的に独立したブラウジングコンテキストを作成します。これは、iFrame内のJavaScript、CSS、およびローカルストレージが、親ドキュメントや他のiFrameから完全に分離されていることを意味します。親と子(または子同士)の間の通信は、通常postMessage
APIに依存しており、これは異なるオリジン間で安全にメッセージを送信します。
実装例
ホスト(index.html
):
<!DOCTYPE html> <html> <head> <title>Host App with iFrame</title> </head> <body> <h1>Host Application</h1> <iframe id="productIframe" src="http://localhost:8082/product-app.html" style="width: 100%; height: 400px; border: 1px solid blue;" ></iframe> <script> const iframe = document.getElementById('productIframe'); // iframeからのメッセージをリッスン window.addEventListener('message', (event) => { if (event.origin !== 'http://localhost:8082') { // セキュリティのためのオリジン検証 return; } console.log('Message from iframe:', event.data); // 例:iframeメッセージに基づいてホストUIを更新 }); // iframeロード後にiframeにメッセージを送信 iframe.onload = () => { iframe.contentWindow.postMessage({ type: 'INIT_DATA', payload: { userId: 'abc' } }, 'http://localhost:8082'); }; </script> </body> </html>
リモート(http://localhost:8082
で実行されるproduct-app.html
):
<!DOCTYPE html> <html> <head> <title>Product App (in iFrame)</title> </head> <body> <h2>Product Details</h2> <div id="product-info">Loading...</div> <script> const productInfoDiv = document.getElementById('product-info'); // 親ウィンドウからのメッセージをリッスン window.addEventListener('message', (event) => { if (event.origin !== 'http://localhost:8080') { // オリジン検証 return; } console.log('Message from host:', event.data); if (event.data.type === 'INIT_DATA') { productInfoDiv.innerHTML = ``; } }); // 例:ホストにメッセージを送信 setTimeout(() => { window.parent.postMessage({ type: 'PRODUCT_LOADED', payload: { productId: '456' } }, 'http://localhost:8080'); }, 2000); </script> </body> </html>
アプリケーションシナリオ
iFrameは、次のような場合に適しています。
-
サードパーティのコンテンツや潜在的に安全でないアプリケーションを埋め込む場合など、最大限の分離が必要な場合。
-
異なるテクノロジースタックで構築された異なるコンポーネントが、干渉なしに独立して動作する必要がある場合。
-
レガシーアプリケーションをモダンなシェルに埋め込む必要がある場合。
-
厳密な環境分離(例:決済ゲートウェイ)を要求するセキュリティ上の懸念がある場合。
-
SEOが埋め込みコンテンツの主要な関心事ではない場合(検索エンジンは歴史的にiFrameコンテンツに苦労していましたが、これは改善されています)。
-
複数のブラウザコンテキストによるパフォーマンスオーバーヘッドが許容できる場合。
Webコンポーネント
Webコンポーネントは、開発者がカスタムで再利用可能でカプセル化されたHTMLタグを作成できるようにする一連のW3C標準です。これらは、さまざまなフレームワークやバニラJavaScriptを横断して使用できるモジュラーコンポーネントを構築するためのネイティブな方法を提供します。
仕組み
Webコンポーネントは4つの主要なテクノロジーで構成されています。
- カスタム要素: 新しいHTMLタグを定義できるようにします。
- Shadow DOM: コンポーネントのDOMとスタイルをカプセル化し、メインドキュメントのスタイルとスクリプトから分離します。
- HTML Templates: インスタンス化されるまでレンダリングされないマークアップフラグメントを宣言できるようにします。
- ES Modules: モジュールのインポートとエクスポート用。
これらは、マークアップとスタイルの両方に対して強力なカプセル化を提供します。つまり、コンポーネントの内部構造とスタイリングは外部に漏れ出したり、外部CSSから影響を受けたりせず、その逆も同様です。通信は通常、標準のDOMイベントとプロパティ/属性を介して行われます。
実装例
カスタム要素の定義(product-card.js
):
class ProductCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); // Shadow DOMを作成 const template = document.createElement('template'); template.innerHTML = ` <style> .card { border: 1px solid #ccc; padding: 16px; margin: 16px; border-radius: 8px; font-family: sans-serif; } h3 { color: #333; } .price { font-weight: bold; color: green; } </style> <div class="card"> <h3></h3> <p class="description"></p> <p class="price"></p> <button>Add to Cart</button> </div> `; this.shadowRoot.appendChild(template.content.cloneNode(true)); this.titleElement = this.shadowRoot.querySelector('h3'); this.descriptionElement = this.shadowRoot.querySelector('.description'); this.priceElement = this.shadowRoot.querySelector('.price'); this.button = this.shadowRoot.querySelector('button'); this.button.addEventListener('click', () => { this.dispatchEvent(new CustomEvent('add-to-cart', { detail: { productId: this.getAttribute('product-id') }, bubbles: true, composed: true // イベントがShadow DOM境界を通過できるようにする })); }); } // 監視する属性を定義 static get observedAttributes() { return ['product-id', 'title', 'description', 'price']; } // 属性変更に応答 attributeChangedCallback(name, oldValue, newValue) { if (name === 'title') { this.titleElement.textContent = newValue; } else if (name === 'description') { this.descriptionElement.textContent = newValue; } else if (name === 'price') { this.priceElement.textContent = `$` + parseFloat(newValue).toFixed(2); } } } // カスタム要素を定義 customElements.define('product-card', ProductCard);
ホスト(index.html
):
<!DOCTYPE html> <html> <head> <title>Host App with Web Components</title> <script type="module" src="./product-card.js"></script> </head> <body> <h1>Host Application</h1> <div id="product-list"> <product-card product-id="P001" title="Super Widget" description="The best widget you'll ever own." price="29.99" ></product-card> <product-card product-id="P002" title="Mega Gadget" description="A revolutionary device for modern living." price="99.00" ></product-card> </div> <script> document.getElementById('product-list').addEventListener('add-to-cart', (event) => { console.log('Product added to cart:', event.detail.productId); // ホストアプリケーションでカートロジックを処理 }); </script> </body> </html>
アプリケーションシナリオ
Webコンポーネントは、次のような場合に最適です。
- 異なるプロジェクト、チーム、さらにはフレームワークを横断して共有できる、再利用可能なUIコンポーネントを作成する場合。
- フレームワークに依存しないコンポーネント開発を実現する場合。
- iFrameのオーバーヘッドなしに、強力なUIおよびスタイルのカプセル化が必要な場合。
- 個々のコンポーネントが高くポータブルで保守可能である必要があるデザインシステムを構築する場合。
- 異なるフレームワーク(例:ReactホストとVue Webコンポーネント)で一部が構築されているハイブリッドアプリケーションの場合。
マイクロフロントエンド実装の比較
各アプローチの主な特性、長所、短所をまとめましょう。
特徴/基準 | モジュールフェデレーション | iFrames | Webコンポーネント |
---|---|---|---|
分離レベル | 高(ランタイム)、JSとCSSには共有コンテキストが多い | 非常に高い(ブラウザコンテキスト) | 高(CSSにはShadow DOM、ネイティブJS分離) |
ランタイムパフォーマンス | 良好(共有依存関係、遅延ロード) | 中程度から不良(iFrameあたりの新しいブラウジングコンテキスト) | 良好(ネイティブブラウザ機能) |
開発の複雑さ | 中程度(Webhook設定が複雑になる可能性あり) | 低〜中程度(標準HTML、postMessage ) | 中程度(ネイティブAPI、冗長になる可能性あり) |
依存関係の共有 | 優秀(ネイティブWebpack機能) | 不良(各iFrameが独自の依存関係をロード) | 中程度(グローバルスコープ、外部メカニズム) |
フレームワーク非依存性 | 良好(異なるフレームワークをラップ可能) | 優秀(完全に分離されたアプリ) | 優秀(ネイティブ標準) |
ルーティング | 非常に柔軟(ホストルーターに統合) | 複雑(各iFrameは独自の履歴/URLを持つ) | 柔軟(ホストルーターに統合) |
通信 | 直接JS呼び出し、共有状態管理 | postMessage API | カスタムイベント、プロパティ/属性 |
SEOへの影響 | 一般的に良好(動的ロード、統一DOM) | 潜在的に不良(別コンテキスト内のコンテンツ) | 一般的に良好(統一DOM) |
ユースケース | ルーズに結合された機能を持つSPA、共有ライブラリ、チーム自律性 | サードパーティコンテンツの埋め込み、レガシーアプリ、高セキュリティ分離 | 再利用可能なUIコンポーネント、デザインシステム、フレームワーク非依存ライブラリ |
スタイリング | 共有スタイルシート、CSS-in-JS、CSS Modules | 完全に分離(別々スタイルシート) | カプセル化(Shadow DOM CSS) |
バンドルサイズ | 共通ライブラリを共有することで最適化される | 大きい可能性あり(各アプリが独自の依存関係をバンドル) | 比較的少量(ネイティブAPI) |
結論
モジュールフェデレーション、iFrame、Webコンポーネントの間でのマイクロフロントエンド実装の選択は、多くの場合、特定のプロジェクト要件、既存のインフラストラクチャ、予算、および望ましい分離レベルに依存します。モジュールフェデレーションは、特にJavaScriptエコシステム内での、高度に統合されパフォーマンスの高いマイクロフロントエンドを実現するための、洗練されたWebpack駆動のソリューションを提供します。iFrameは比類のない分離を提供し、パフォーマンスや統合の複雑さのコストで、異なるアプリケーションや信頼できないコンテンツを埋め込むのに理想的です。Webコンポーネントは、ネイティブでフレームワークに依存しない、再利用可能なUIコンポーネントを構築するための方法を提供し、共有デザインシステムやクロスフレームワーク統合のギャップを埋めます。各アプローチは独自の強みをもたらし、それぞれのトレードオフを明確に理解することが、堅牢でスケーラブルなマイクロフロントエンドアーキテクチャを構築する上で不可欠です。