マイクロフロントエンドを、読むことではなく作ることで理解したいと思いました。そこで自分に一つの制約を課しました。実験はこのサイトの実際のページの中に置くこと、そしてそのページ自体を証拠にすること。この記事がそのページです。
このサイトはNext.js 16とReact 19で動いています。下のパネルは違います。独自のリポジトリを持ち、独自にビルドとデプロイが行われ、実行時に別のドメインから読み込まれるAngular 22のアプリです。URLが一つあるだけで、このサイトのビルドはその存在を何も知りません。
Angularのマイクロフロントエンドを読み込み中…
マイクロフロントエンドからのイベントはまだありません。
試してみてください。このサイトのテーマや言語を切り替えると、パネルも追従します。パネルのボタンをクリックすると、そのすぐ下の行(Angularではなくサイト側のもの)がイベントを表示します。パネルの内容はすべて実行時に読み取られています。Angularのバージョン、読み込み元のドメイン、ビルドされたコミット、そしてダウンロードにかかったコストです。
フロントエンドを組み合わせる三つの方法
マイクロフロントエンドに唯一の標準はありません。よく使われるものが三つあり、それぞれ異なる問題を解決します。
- ルートによる分割。 プロダクトの各領域が別々のアプリで、プロキシやエッジが各パスを適切なアプリに振り分けます。Next.jsではマルチゾーンと呼ばれます。運用はシンプルですが、アプリをまたぐたびにページ全体の読み込みが発生します。
- Module Federation。 ホストが実行時に他のデプロイからモジュールを読み込み、依存関係を共有します。Angularの世界ではNative Federationが事実上の選択肢です。すべての部品が同じフレームワークを使うときに真価を発揮します。
- Web Components。 マイクロフロントエンドをカスタム要素としてパッケージし、ホストはタグを一つ描画するだけです。ReactとAngularの両方がネイティブに理解できる唯一のインターフェースです。
ここではホストがReactで、マイクロフロントエンドがAngularなので、フレームワークをまたいだ組み合わせになります。その場合の定番がWeb Componentsで、Angular Elementsはまさにそのためにあります。
コントラクト
マイクロフロントエンドについて考えるうえで一番役に立ったのは、それをコントラクト、つまり一方が他方について知っていることのすべてとして捉えることでした。ここでは三つです。
**入力はlocaleとtheme。** このサイトはそれらを要素のプロパティとして設定します。普通のHTMLページなら属性として設定でき、Angular Elementsはその両方を対応付けます。パネルはこのサイトが対応する五つの言語の辞書と、テーマごとの配色を自分で持っています。サイトのCSSから何も継承しません。
出力はDOMイベント。 パネルはReactの存在を知りません。CustomEventを発行し、サイトがそれを受け取ります。
this.host.nativeElement.dispatchEvent(
new CustomEvent("mfe:ping", {
detail: { angularVersion: this.angularVersion, at: new Date().toISOString() },
bubbles: true,
composed: true,
}),
)分離はShadow DOM。 パネルのスタイルはページに漏れず、サイトのTailwindもパネルに入り込みません。
Angular側では、マイクロフロントエンド全体がタグを一つ登録することに集約されます。
import { provideZonelessChangeDetection } from "@angular/core"
import { createApplication } from "@angular/platform-browser"
import { createCustomElement } from "@angular/elements"
import { InspectorComponent } from "./app/inspector.component"
export const TAG = "mfe-inspector"
export async function registerInspector(): Promise<void> {
if (customElements.get(TAG)) return
const app = await createApplication({ providers: [provideZonelessChangeDetection()] })
// Checked again: two evaluations of this file can both pass the first check.
if (customElements.get(TAG)) return
customElements.define(TAG, createCustomElement(InspectorComponent, { injector: app.injector }))
}サイト側では、読み込みとは、バンドラーが触れてはいけないURLをimportし、タグが定義されるのを待つことです。
loadRemoteModule(async () => {
// The bundler must not resolve this: the URL belongs to another deploy.
await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ src)
await customElements.whenDefined(tag)
})かかったコスト
以下の数値は推定ではなく、公開されたファイルで計測したものです。
- パネルは119 KB(圧縮後42 KB)の単一ファイルとして配信されます。見出しと短いリストとボタンだけのパネルにとって、これが二つ目のフレームワークをページに載せる代償です。
- 私の回線では、コールドロード5回の中央値で、ファイルのダウンロードに44 ms、要素が使える状態になるまで50 msでした。
- ファイルが読み込めない場合、サイトは約3分の1秒でパネルの代わりに短いお知らせを表示し、記事の残りはそのまま動作します。サーバーがまったく応答しない場合に備えて、8秒のタイムアウトもあります。
ファイルはパネルが画面に近づいたときにだけリクエストされるので、ここまでスクロールしない読者はAngularのコストを払いません。
チュートリアルが飛ばすこと
作業の大半はコンポーネントではありませんでした。境界の部分でした。
- 別ドメインのコードを信頼すること。 サイトは自分でビルドしていないJavaScriptを実行するようになりました。記事のデータは、許可リストにあるオリジンからHTTPSで配信されるマイクロフロントエンドだけを受け入れ、それ以外はページの描画前に破棄します。
- 意図的に一つのファイル。 サイトは一つのURLだけをimportします。もしAngularのビルドがチャンクに分かれたら、このコントラクトは黙って壊れるので、代わりにビルドを明示的に失敗させます。
- 二重登録。 ページを離れて戻るとモジュールが再実行され、タグの二度目の定義でエラーになると予想していました。実際にはそうなりません。ブラウザはモジュールをURLごとに一度しか評価しないからです。エラーになるのは同じファイルが二つのURLから読み込まれた場合で、そこでは単純なチェックでも足りませんでした。どちらかが登録する前に、二つの評価の両方がチェックを通過してしまうからです。そこで、Angularの起動後にもう一度チェックしています。
- オリジンをまたいだ計測。
Timing-Allow-Originヘッダーがないと、ブラウザは他ドメインのファイルを0バイトと報告し、パネルは自分の重さとキャッシュされたコピーを区別できません。304の再検証にも同じ落とし穴があります。やり取りされるのはヘッダーだけなので、パネルはモジュールが0.3 KBだと主張する代わりに「キャッシュ」と表示します。 - 独立したデプロイ。 サイトはファイルを実行時に読み込むので、マイクロフロントエンドの新しいデプロイは、サイトを再ビルドせずにこの記事に届きます。パネルだけに目に見える変更を公開して確かめました。短いキャッシュ(60秒、さらに再検証中は最大5分間前のバージョンを配信)は、再訪した人が古いバージョンを見る時間の上限を決めているだけです。
割に合わないとき
このサイトにとってマイクロフロントエンドはやりすぎで、それこそが狙いでした。払う必要が出てくる前に、そのコストを知りたかったのです。二つ目のフレームワーク、二つ目のパイプライン、同期を保つべきコントラクト、そしてページ上で動く別ドメインのコード。チームが一つでコードベースも一つなら、よく整理されたモノリスが、その何分の一かのコストでほぼすべての利点をもたらします。
割に合い始めるのは、問題がコードではなく人にあるときです。それぞれ独自のリリースサイクルを持つ複数のチームが、同じフロントエンドの中で互いの足を踏んでいる状態。このサイトにその問題はありません。でも今は、それを解決するのにいくらかかるのかを知っています。
マイクロフロントエンドのコードは公開しています:github.com/ViniAguiar1/mfe-angular-inspector
