CRA + Module Federation + TypeScriptの設定
最近、React + TypeScriptでプラグインベースのWebアプリケーションを作成していて、プラグインの仕組みにModule Federationが使えないかを試していた。ひとまず、動作する設定にたどり着いたので、要点をメモとして残しておく。
前提条件
- TypeScript
- Create React App
- CRACO
この記事では、ReactアプリケーションはCRAを使って実装する。Module Federationを設定するにはWebpackの設定をする必要があるが、CRAをejectしたりせずにCRACOを使って設定を行う。
サンプルコード
実際のサンプルコードはこちら。
プロジェクトの生成
まずCRAを使ってプロジェクトを生成し、それからreact-scripts
をCRACOに置き換えるところまでは省略。以下の公式ドキュメントを参照。
Webpackの設定
Webpackにはこんな感じで、ホスト側とリモート側の両方にModuleFederationPlugin
を設定していく。
ホスト側
const { ModuleFederationPlugin } = require('webpack').container
const { dependencies } = require('./package.json')
module.exports = {
webpack: {
plugins: {
add: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remote1: 'remote1@http://localhost:3001/remoteEntry.js',
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies['react'],
},
'react-dom': {
singleton: true,
requiredVersion: dependencies['react-dom'],
},
},
}),
],
},
},
}
リモート側
const { ModuleFederationPlugin } = require('webpack').container
const { dependencies } = require('./package.json')
module.exports = {
webpack: {
plugins: {
add: [
new ModuleFederationPlugin({
name: 'remote1',
filename: 'remoteEntry.js',
exposes: {
'./component1': './src/component1',
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies['react'],
},
'react-dom': {
singleton: true,
requiredVersion: dependencies['react-dom'],
},
},
}),
],
},
},
}
ホスト側remotesの設定
間違える人は少ないと思うが自分は最初にここでハマったので書いておくと、remotes
エントリーのremote1@...
の部分はリモート側のModuleFederationPlugin
設定でname
に指定した名前を設定する。
remotes: {
remote1: 'remote1@http://localhost:3001/remoteEntry.js',
},
ここを間違えて、サンプルから取ってきた名前のままにしておくと当然ながら動かない。
キー側のremote1
は、ホスト側のソースでインポートするときの名前になる。
import { Component } from 'remote1/component1'
output.publicPath = 'auto'
Webpackでもう1つ重要な設定は、output
のpublicPath
をauto
に設定すること。これを設定しないとチャンクされたCSSが読み込まれない。
module.exports = {
webpack: {
// ...省略...
configure: {
output: {
publicPath: 'auto',
},
},
Webpackではデフォルトがauto
のはずなので、これはCRACOを使う場合限定の話かもしれない。
ソースコードの修正
index.tsとbootstrap.tsxを分ける
Module Federationを使う上で、とにかく一番重要なのはこの変更。これを忘れていると、画面に何も表示されず、Webコンソールに以下のエラーが出る問題に悩まされることになる。
Uncaught Error: Shared module is not available for eager consumption: ...
これを解決するには、以下のようなindex.tsx
をそのままbootstrap.tsx
にリネームする。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
代わりに、新たに以下のようなindex.ts
を作ってそこからbootstrap.tsx
を読み込むようにする。
import('./bootstrap')
export {}
Module Federationではホストとリモートで共通するライブラリのインスタンス共有化などの処理を行うので、モジュールのインポートを非同期に行う必要があるため。
逆に、もしこれでホスト/リモートいずれかのCSSの読み込みなどに問題があった場合は、bootstrap.tsx
でCSSをインポートするのを止めてindex.ts
から直接インポートすることで解決することがある。
declare module '<remote>/*'
先程remotes
の設定で、キー側はソースでインポートするときの名前になると書いたが、TypeScriptではそのままではModule Federationで読み込むリモートの型定義を認識できない。
import { Component } from 'remote1/component1'
そのため、コンパイル時にこのようなエラーが出る。
TS2307: Cannot find module 'remote1/component1' or its corresponding type declarations.
1 | import React from 'react'
> 2 | import { Plugin } from 'remote1/component1'
| ^^^^^^^^^^^^^^^^^^^^
これを回避するには、以下のような.d.ts
ファイルを用意する。
declare module 'remote1/*'
動的リモートローディング
Module Federationは外部モジュールを動的に読み込めるための仕組みだが、ドキュメントやサンプルを読む限りだとリモートのエントリーポイントを直接Webpackの設定やindex.html
に埋め込んだりしているものばかりで、設定時に静的にリモートを決定している例しか見当たらない。
しかし、Module Federationはマイクロフロントエンドを実現するための仕組みなので、本来読み込み先のリモートモジュールを動的に構成できないといけない。
幸い、そのような動的リモートローディングはModule Federationのユーティリティパッケージ@module-federation/utilities
を使って簡単に実現できる。
使い方はNPMパッケージのREADMEにもあるように、以下のようにimportRemote
をインポートして使う。importRemote
に渡すパラメータは動的に変えられるので、どこかから読み込むリモートのセットを動的に渡してあげればいい。
import { importRemote } from '@module-federation/utilities'
importRemote({
url: 'http://localhost:3001',
scope: 'remote1',
module: './component1',
}).then(component => {
...
})
@module-federation/utilities
を使った動的リモートローディングのやり方は、公式のサンプルにも見つかる。[1]
参考資料
最後に、Module Federationを使っていて分からないことがあったら、とりあえずこのプロジェクトのサンプルを漁ってみるとだいたい答えが見つかる。
-
この公式サンプルを
@module-federation/utilities
を使うようにプルリク投げたのは自分 😛 ↩︎
Discussion