マイクロフロントエンド① 基礎学習


将来マイクロサービスの案件に携わってみたく、マイクロフロントエンドという言葉にも前々から興味があったので勉強してみることにしました。

マイクロフロントエンドって何?

各マイクロサービスが提供する画面を結合することで単独のアプリケーションのようにふるまうものをマイクロフロントエンドといいます。各画面の開発はそれぞれ別のチームが行います。一方で、UIを単独のアプリケーションが担っている(1つのプロジェクトにすべてのコードが入っている)状態をモノリシックフロントエンドといいます。

eコマースを例にすると、商品リストとショッピングカートの画面を各チームが別々に開発するような状態がマイクロフロントエンドにあたります。

画面間のデータのやりとりは直接ではなくAPI経由で行います。このように各画面は完全に分離されているので、チームごとに使用するフレームワーク(React, Vueなど)が異なっていても問題ありません。

メリットは何?

  • 複数の開発チームが独立して画面を作成できる(異なるフレームワークやライブラリが使える)
  • 機能ごとにアプリケーションを分割するので保守性が高い(理解、修正のコストが低い)

プロジェクト作成

商品リストとショッピングカートの表示を各々のプロジェクトにわけて、簡単なマイクロフロントエンドを実装してみました。

構成

商品リスト(ProductsList)とショッピングカート(Cart)の他に、それらの表示を司るコンテナ(Container)というプロジェクトも作成します。

プロジェクトの統合

コンテナが各プロジェクトを統合する方法にはいくつかあります。それぞれに長短があるので、それらを理解した上で選択する必要があります。

  • Build-Time Integration(Compile-Time Integration): コンテナがブラウザでロードされる前に各プロジェクトのソースコードを取得する。
  • Run-Time Integration(Client-Side Integration): コンテナがブラウザでロードされた後に各プロジェクトのソースコードを取得する。
  • Server Integration: コンテナのコードをサーバでロードしている間にサーバが各プロジェクトのソースコードを取得するか決める。

Build-time integration

各プロジェクト(ProductLists, Cart)をデプロイしてnpmのパッケージとして公開し、それらのdependencyをContainerプロジェクト上でインストールするような流れになります。実装の流れはわかりやすいのですが、プロジェクトとContainerとの結合度が高いことや、コードを更新するごとにContainerも再度デプロイしなければいけないという問題があります。

Run-time integration

まず、各プロジェクト(ProductLists, Cart)がhttps://my-app.com/productslist.jsなどにデプロイされます。ユーザがmy-app.comにアクセスしたときにコンテナがロードされ、productslist.jsを取得します。この方法だとセットアップが少し複雑になりますが、いつでもプロジェクトをデプロイすることができ、いろんなバージョンのプロジェクトをデプロイした場合にも、コンテナ側で取得するコードを選択することができます。

セットアップ

ecommフォルダにproducts cartのプロジェクトとcontainerを作成します。

パッケージ導入

次にプロジェクトに必要なパッケージを導入して、webpack.config.jsファイルを作成します。

products, cart

container

フォルダ構成は以下のようになります。

プロジェクト連携

containerとproducts, cartを連携するために、WebpackのModuleFederationPluginを使用します。
このプラグインでは、プロジェクトにホストかリモートの役割をつけ、リモートのプロジェクトではファイルを外部に公開する設定を行い、ホストのプロジェクトでは指定したリモートのファイルをインポートするように設定することができます。今回はcontainerをホスト、productsとcartをリモートとします。

リモートのindex.jsファイルはModuleFederationPluginによって3つのファイルに出力されます。

  • remoteEntry.js: プロジェクトで利用可能なファイルのリスト+ファイルのロード方法に関する内容を含むファイル
  • src_index.js: ブラウザでロードされるsrc/index.jsを変換したファイル
  • faker.js: ブラウザでロードされるfaker(モックデータ生成用のnpmパッケージ)を変換したファイル

ホストのプロジェクトでは、bootstrap.jsでproductsとcartのファイルをインポートし、index.jsbootstrap.jsをインポートするような構成にします。

bootstrap.js
import 'products/ProductsIndex';
import 'cart/CartShow';
index.js
import('.bootstrap');

すると、index.jsファイルはWebpackによって2つのファイルに出力されます。

  • main.js: index.jsを変換したファイル
  • bootstrap.js: bootstrap.jsを変換したファイル

ファイル(products, cart)を取得する処理をbootstrap.jsに分けたのは、必要なファイルを取得した後に、bootstrap.jsを実行するような順序にするためです。

この流れをまとめると以下のようになります。

また、ModuleFederationPluginの設定はwebpack.config.jsで行います。

ホスト(container)

webpack.config.js
new ModuleFederationPlugin({
  name: 'container', //プロジェクト(PJ)名(任意)
  remotes: { //取得したいリモートのPJのリスト
    products: 'products@http://localhost:8081/remoteEntry.js', //[インポートするときの名前]: [(リモート側で設定したPJ名)@(remoteEntry.jsのURL)]
    cart: 'cart@http://localhost:8082/remoteEntry.js',
  },
}),

リモート(products)

webpack.config.js
new ModuleFederationPlugin({
  name: 'products',
  filename: 'remoteEntry.js',
  exposes: {
    './ProductsIndex': './src/index', //ファイルのエイリアス(名前置き換え)
  },
}),

リモートのファイルをホスト側でインポートする際はimport 'products/ProductsIndex'と記述します。node_modules内に該当する名前のdependenciesがないため、productsのProductIndex(src/index)をインポートすることができています。

container, products, cartでwebpack serveを実行するとlocalhostの各ポート(8080, 8081, 8082)でWebpack DevServerが立ち上がります。localhost:8080にアクセスすると商品リスト(products)とカート(cart)が表示されています。

*productsとcartのプロジェクトにあるindex.htmlは各チームで開発する際の表示確認用にしか使われず、本番では使われません。

ファイルの共有

dev-toolのNetworkタブをみてみると、productsとcartで同じパッケージを使っている都合でfakerが2つロードされています。同じファイルがロードされるとその分重くなるので、ファイルの共有でこの問題を解消します。

shared: ['faker']をproductsとcartのwebpack.config.jsに追記するとfakerが1つしかロードされなくなりました。簡単ですね。

webpack.config.js
new ModuleFederationPlugin({
  name: 'products',
  filename: 'remoteEntry.js',
  exposes: {
    './ProductsIndex': './src/index',
  },
  shared: ['faker'],
}),

しかし、cartのlocalhost:8082にアクセスしようとすると、開けなくなっていました。どうやら、fakerをインポートする前にindex.jsを実行してしまうみたいです。

containerと同様に、index.jsの中でbootstrap.jsを読み込むような構成(非同期処理)にしてみたらちゃんと開けるようになりました。

また、productsとcartsで違うバージョンのfakerを用いたときは、ファイル共有を行っても2つのファイルがロードされるようです。どうしても共有したい場合には、singletonを使えば大丈夫なのですが、この場合、バージョンに関するwarningがでます。

shared: {
  faker: {
    singleton: true,
  },
},

おわりに

将来役立つ日がくると信じて、次はReactとVueでマイクロフロントエンドを実装してみます。

参考資料