- ※これからかくことは次回リリースで変更される可能性もあります
- ※こちらの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
でもそんなには困らないのかも