🗂

マイクロフロントエンド(Microfrontends)とその周辺技術

2023/07/18に公開

マイクロフロントエンドとは

マイクロフロントエンドとはフロントエンドアプリケーションを、機能単位の集まりと解釈しそれぞれの機能を異なるチームが保有・開発できる状態を作ることを目指すフロントエンドの開発手法です。これはコード規約やフレームワークの選定といったコードレベルの話にとどまらず、ビルド・デプロイフローやサーバー構築といったインフラの領域にまで及びます。それぞれが独自にフレームワークやライブラリ技術選定などもチームごとに柔軟に行うことができるという特徴を持っています。

すでにバックエンドの領域ではマイクロサービスというアーキテクチャが提案・実践されていますが、結局フロントエンド側がモノリスな構成を取ることがほとんどであるため、完全な機能単位でのチームの分割はなされていないことがほとんどだと思います。フロントエンドにおける開発でも類似の概念を導入することで、プロダクト全体で俯瞰したときに機能単位で垂直に分離されたチーム開発を実現することをマイクロフロントエンドの目的の一つとする場合も多いようです。


https://micro-frontends.org/
まだそこまで普及している気配はなさそうですが、将来的に一大領域になりそうな感があります。

実現にあたってのトピックス

明確な定義があるわけではないと思うのですが、色々な記事を見て回るとおおまかに要件として以下のような点が挙げられるようです。

ビルド・デプロイフローの独立性


https://martinfowler.com/articles/micro-frontends.html

それぞれの機能ごとに開発を進める上で、それぞれのチームがそれぞれのマイクロフロントエンドを任意のタイミングでビルド・デプロイすることができることが求められます。
そのため破壊的な変更がリリースされた場合でも正常な動作を保証するため、各マイクロフロントエンドのバージョニングを適切に行える機構が必要となります。

提案される技術: Module Federation, ImportMaps etc...

マイクロフロントエンド同士の双方向性

マイクロフロントエンドの考え方は、それぞれのマイクロフロントエンド同士を組み合わせて一つのフロントエンドを構築するというものである以上、各マイクロフロントエンド同士が相互に参照可能である状態を維持するのが望ましいと言えます。モジュールの依存関係を適切に解決する機構が必要となります。

提案される技術 Module Federation etc...

モジュール・コンポネントの抽象化

各マイクロフロントエンドは他のマイクロフロントエンドが利用しているフレームワークやライブラリに左右されずに実装できることが求められます。またモジュール自体がカプセル化されており、参照する側は内部の実装を気にすることなく利用できることが望ましいと言えます。

提案される技術
WebComponent, iframe etc...

他にも考慮・解決すべきトピックは数えきれないほど多くあります。
パッと思いつくものでも、

  • css の衝突の回避
  • リソースのロードの効率化およびリソースの共通化
  • ルーティングとページロードの対応
  • テスト
  • レポジトリの構成
  • TypeScript(型定義)の併用

などが挙げられそうです。

関連する技術・フレームワーク

ここまでマイクロフロントエンドを実現する上で必要な要件について簡単に挙げましたが、
それらを実現するための技術やライブラリについて紹介していきます。

Module Federation

https://webpack.js.org/concepts/module-federation/

webpack5 で追加された機能で、ドメインを跨いだモジュールの参照をサポートするプラグインです。webpack や Rspack のcontributorであるZack Jackson氏が発起人となって実装された機能になります。
https://twitter.com/ScriptedAlchemy

仕様

各フロントエンドのビルド成果物とそれを提供するをコンテナといい、ページロード時に最初に読み込むモジュールを提供するコンテナをhostコンテナ、hostから参照される他のコンテナをremoteコンテナと言います。
ModuleFederationPluginを利用することでそのコンテナが外部に公開するモジュールと、依存するremoteコンテナのエンドポイントを指定することができます。

大きな特徴の一つとして双方向的ホスト(bidirectional host)が実装方針として採用されており、あらゆるコンテナはホストになることが可能で、なおかつホスト同士でモジュールの循環参照を許容するという特徴を持っています。

また共通モジュールの指定も行うことができ、remoteコンテナは依存するモジュールがhostコンテナがsharedに設定しているモジュールと等しい場合、hostコンテナが読み込んだモジュールを利用することでモジュールのロードコストの削減を図ることが可能です。

設定方法

下記にmodule-federationを有効にするwebpack.config.jsの設定例を示します。
app1,app2というマイクロフロントエンドが存在する想定です。

app1/webpack.config.js
const { FederatedTypesPlugin } = require("@module-federation/typescript");

module.exports = {
    ...,

    plugins: [
        new FederatedTypesPlugin({
            federationConfig: {
                name: "app1",
                filename: "remoteEntry.js",
                remotes: {
                    app2: "app2@http://localhost:3002/remoteEntry.js",
                },
                shared: [
                    {
                        react: {
                        singleton: true,
                        requiredVersion: pkg.dependencies.react,
                        },
                    },
                    {
                        "react-dom": {
                        singleton: true,
                        requiredVersion: pkg.dependencies["react-dom"],
                        },
                    },
                ],
            },
        })
    ]
}

module federationの設定はそれぞれのフロントエンドに設定する必要があるため、app2のwebpack.config.jsにも設定を追記します。

app2/webpack.config.js
const { FederatedTypesPlugin } = require("@module-federation/typescript");

module.exports = {
    ...,

    plugins: [
        new FederatedTypesPlugin({
            federationConfig: {
                name: "app2",
                filename: "remoteEntry.js",
                exposes: {
                "./Button": "./src/Button",
                },
                shared: [
                    {
                        react: {
                        singleton: true,
                        requiredVersion: pkg.dependencies.react,
                        },
                    },
                    {
                        "react-dom": {
                        singleton: true,
                        requiredVersion: pkg.dependencies["react-dom"],
                        },
                    },
                ],
            },
        })
    ]
}

上記の設定を入れることでapp1からapp2が公開しているButtonコンポネントを利用することが可能になります。

// @mf-types directory is autogenerated when you start the server
import React, { lazy, Suspense } from "react";
import RemoteButtonType from "../@mf-types/app2/Button";

const RemoteButton = lazy(
  () => import("app2/Button")
) as unknown as typeof RemoteButtonType;

const App = () => (
  <div>
    <h1>Typescript</h1>
    <h2>App 1</h2>
    <Suspense fallback="Loading Button">
      <RemoteButton size="large" />
      <br />
      <RemoteButton size="small" />
    </Suspense>
  </div>
);

export default App;

さらっとTypeScriptの対応を施しています。FederatedTypesPluginを利用することで各マイクロフロントエンドが公開するモジュール・コンポネントの型情報をそれぞれのフロントエンドにコピーしてくれます。

他にもサンプルが山のように用意されているので詳しく見たい場合は以下を参照
https://github.com/module-federation/module-federation-examples/tree/master

マイクロフロントエンドでの利用

Module Federationを利用することで、マイクロフロントエンド同士の参照(モジュールの双方向性)を行うことが可能です。
またremoteコンテナのエンドポイントにバージョニングの情報を付与することでhostコンテナが依存するモジュールのバージョニングを行うこともできそうです。(もちろんremoteコンテナのデプロイでURLによるバージョニングが行われていることが前提ですが)

ImportMap

https://developer.mozilla.org/ja/docs/Web/HTML/Element/script/type/importmap

仕様

ImportMapは、スクリプト内で利用されるimportで、keyに一致するパスが指定された場合、マッピングされたURLからモジュールを読み込ませるように指示できる機能です。
全ての主要ブラウザで対応が完了しています。

<script type="importmap">
  {
    "imports": {
      "square": "./module/shapes/square.js",
      "circle": "https://example.com/shapes/circle.js"
    }
  }
</script>

この時、circleモジュールをimportする部分ではhttps://example.com/shapes/circle.js を利用するようになります。

Module Federationとの関係

最近全ての主要ブラウザが対応したため、利用に際する懸念が無くなった感があるImportMapですが、Module Federationを置き換えるかも?という見方が出ています。

https://www.mercedes-benz.io/2023/01/05/you-might-not-need-module-federation-orchestrate-your-microfrontends-at-runtime-with-import-maps/

そのほか、Module Federationライクに設定できるnative-federationも提案されているようです。これを利用することで各コンポネントのimportをremoteコンテナのURLに置き換えるImportMapを埋め込むことが可能になるようです。

https://www.npmjs.com/package/@softarc/native-federation?activeTab=readme

webコンポーネント(Web Component)

最後はマイクロフロントエンドを実現する上で核になるであろう webコンポネント です。
https://developer.mozilla.org/ja/docs/Web/API/Web_components

webコンポーネントは、再利用可能かつ外部から秘匿された UI 要素を作成するための技術です。独自のカスタム要素としてブラウザ上で宣言することができるため、別フレームワークやライブラリで宣言したコンポーネントを webコンポーネントに変換すれば、マイクロフロントエンド間でコンポネントを容易に統合することができます。

仕様

webコンポーネントには、主要な機能があります。1つ目は「カプセル化」です。コンポーネントは自己完結型であり、外部からの影響を受けずに機能します。2つ目は「再利用性」であり、同じコンポーネントを複数の場所で使用できます。3つ目は「拡張性」であり、コンポーネントを継承してカスタマイズしたり、他のコンポーネントと組み合わせたりできます。

以下は、TypeScript を使用して Web コンポーネントを定義する例です。

// カスタム要素の定義
class MyComponent extends HTMLElement {
  constructor() {
    super();
    // シャドウDOMを作成
    const shadow = this.attachShadow({ mode: "open" });

    // コンポーネントの表示内容を定義
    const text = document.createElement("p");
    text.textContent = "Hello, webコンポネント!";

    // シャドウDOMに要素を追加
    shadow.appendChild(text);
  }
}

// カスタム要素を登録
customElements.define("my-component", MyComponent);

上記のコードでは、MyComponentというカスタム要素を定義しています。MyComponentはHTMLElement を継承しており、コンストラクタ内でshadow DOMを作成し、表示内容を定義しています。最後に、customElements.define を使用してmy-componentという名前でカスタム要素を登録しています。

このようにして定義された webコンポーネントは、HTML内で以下のように使用することができます。

<my-component></my-component>

フレームワーク間の対応

最近登場するフレームワークでは、定義したコンポーネントを web コンポーネントへ変換できる機能やパッケージが用意されていることが多いように思います。

vue ではdefineCustomElementを使うことでコンポネントをwebコンポネントとして定義することが可能です。

litに至っては定義されるコンポネントは web コンポネントそのものとして扱うことができます。
他にも svelte や solidjs といったフレームワークでも custom element への変換をサポートしています。

課題

webコンポネントは期待値・注目度ともに高い技術ですが、まだまだブラウザ間の挙動の差異や、不具合らしき挙動も多いようです。webコンポネントの改善や発展次第ではマイクロフロントエンドにも大きな影響を与えることが予想されます。

今後の動向に注目といったところでしょう。

参考文献

https://micro-frontends.org/
https://martinfowler.com/articles/micro-frontends.html
https://engineering.mercari.com/blog/entry/2018-12-06-162827/
https://zenn.dev/moneyforward/articles/2022-12-14-micro-frontends-in-moneyforward
https://www.youtube.com/watch?v=poMrzn_2wLY&t=1s

Discussion