- ※これからかくことは次回リリースで変更される可能性もあります
- ※こちらのPRでoverrides/overridableオプションは廃止され、sharedオプションで調整できるようになりました (追記@2020/06/02)
Module Federation概要
webpack 5でModule Federationという仕組みが入る。
例えば、以下のように書くと、app_01のsharedオプションに記述したモジュールが、app_02のアプリに対して共有されるようになる。
plugins: [
new ModuleFederationPlugin({
name: "app_01",
library: { type: "var", name: "app_01" },
filename: "remoteEntry.js",
remotes: {
app_02: "app_02",
},
exposes: {
SideNav: "./src/SideNav",
Page: "./src/Page"
},
shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
}),
]
plugins: [
new ModuleFederationPlugin({
name: "app_02",
library: { type: "var", name: "app_02" },
filename: "remoteEntry.js",
remotes: {
app_03: "app_03",
},
exposes: {
Dialog: "./src/Dialog",
Tabs: "./src/Tabs"
},
shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
}),
]
<!DOCTYPE html>
<html lang="en">
<head>
<!-- app_02 -->
<script src="http://localhost:3002/remoteEntry.js"></script>
</head>
<body>
<div id="root"></div>
<!-- app_01 -->
<script src="http://localhost:3001/main.js"></script>
<script src="http://localhost:3001/remoteEntry.js"></script>
</body>
</html>
app_02のモジュールは非同期に読み込まれ、実行時にchunkを解決する。実行するまで不明なプログラムであるはずなので、エラーなどもその時までわからない。
しかし、逆に言えば、app_01とapp_02のビルドパイプラインは疎結合にできるので、Micro-Frontendsにおいて活用できそう、というわけだ。
overridablesとoverrides
ModuleFederationPluginでは、ContainerPlugin、ContainerReferencePluginというクラスを内部で扱っている。詳細はこちらのとおりだが、それぞれがoverridables、overridesというオブジェクトを提供している。
-
overridables-remoteとして読み込まれたとき、localによって上書き可能なモジュール -
overrides-localとして読み込まれたとき、remoteを上書きするモジュール
これをハッシュで識別し、実行時に解決・最適化するのがModule Federationの実態だ。前述の例では、sharedオプションに指定されたモジュールが上書き対象となっている。
sharedの問題
しかし、sharedオプションをつかったchunkにはひとつ問題がある。
ModuleFederationPluginでlocalとremoteそれぞれのコードを生成する箇所は以下のようになっている。
compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
if (
options.exposes &&
(Array.isArray(options.exposes)
? options.exposes.length > 0
: Object.keys(options.exposes).length > 0)
) {
// remote 用のコード
new ContainerPlugin({
name: options.name,
library: options.library || compiler.options.output.library,
filename: options.filename,
exposes: options.exposes,
// options.shared その1
overridables: merge(options.shared, options.overridables)
}).apply(compiler);
}
if (
options.remotes &&
(Array.isArray(options.remotes)
? options.remotes.length > 0
: Object.keys(options.remotes).length > 0)
) {
// local 用のコード
new ContainerReferencePlugin({
remoteType:
options.remoteType ||
(options.library && options.library.type) ||
compiler.options.externalsType,
remotes: options.remotes,
// options.shared その2
overrides: merge(options.shared, options.overrides)
}).apply(compiler);
}
});
これをみると、options.sharedは2箇所でつかわれていることがわかる。つまり、sharedを使うと、overridesとoverridablesを同時に提供してしまうので、アプリのユースケースよっては不要なコードが生成されてしまうのだ。
具体的なパターンをみてゆく。
overridablesのみ提供したいパターン
例えば、app_02を以下のように仮定する。
-
app_02はremoteとして参照されることがあり、いくつかのライブラリをlocal(app_01)に上書きしてほしい -
app_02からremote(app_03)を呼ぶケースでは、app_02とremoteで共通するライブラリがない - この場合「
app_02はoverridablesのみを提供し、overridesを提供することはない」ということになる。
sharedを使うと、local用のビルドでoverridesのchunkもつくってしまう。しかし、前述のような想定だと、overridesのchunkが使用されるケースはない。
これを解決するのがoverridablesオプションである。sharedの代わりにoverridablesと明示的に指定することで、overridesが生成されることはなくなる。
plugins: [
new ModuleFederationPlugin({
name: "app_02",
library: { type: "var", name: "app_02" },
filename: "remoteEntry.js",
remotes: {
app_03: "app_03",
},
exposes: {
Dialog: "./src/Dialog",
Tabs: "./src/Tabs"
},
// 変更箇所
overridables: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
}),
]
実際には、以下のようにchunkを削除できる。
overridesのみ提供したいパターン
同様に、例えば、app_01を以下のように仮定してみる。
-
app_01はapp_02をremoteとして参照し、いくつかのライブラリを上書きしたい -
app_01はライブラリのバージョンが古いため、remoteとして参照される場合に、ライブラリを上書きされたくない - この場合「
app_01はoverridesのみを提供し、overridablesを提供することはない」ということになる。
shredを使うと、remote用のビルドでoverridablesのchunkをつくってしまう。前述のような想定だと、overridablesのchunkが使用されることがないし、また、生成されるべきでもない。
これはoverridesオプションで解決できる。sharedの代わりにoverridesを使うことで、overridablesが生成されることはなくなる。
plugins: [
new ModuleFederationPlugin({
name: "app_01",
library: { type: "var", name: "app_01" },
filename: "remoteEntry.js",
remotes: {
app_02: "app_02",
},
exposes: {
SideNav: "./src/SideNav",
Page: "./src/Page"
},
// 変更箇所
overrides: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
}),
]
実際には、以下のようにchunkを削除できる。
雑感
- 実際はこれらを手で管理してゆくのは厳しくないか・・?
- アプリケーションのユースケースと責務がはっきりしていれば、明示的に指定することはできるかもしれない
- 最初から意識しておけば
sharedを使うことこと自体は減りそう -
automatic-vendor-federationというプラグインがあるが、現状は
sharedしか対応していない - chunkが生成されてしまうだけで、fallbackもあるのでバグるわけではない。あくまで最適化したい場合
- 脳死
sharedでもそんなには困らないのかも