« ^ »

モジュールフェデレーション

所要時間: 約 2分

モジュールフェデレーションとは何か

バンドルしたライブラリは重複する

規模の大きいフロントエンドで複数の成果物を生成し、それをフロントエンドで読み込む構成を考える。

成果物としてはmain.jsとsub.jsがあり、それぞれにReactをバンドルしているとする。この場合、通常はmain.jsとsub.jsの両方にReactを含んでいる。

  +-------+                  +-------+
  | React |                  | React |
  +---+---+                  +---+---+
      |                          |
      | Bundle                   | Bundle
      |                          |
+-----+---------+          +-----+---------+
|               |          |               |
| main.js       |          | sub.js        |
|               |          |               |
+---------------+          +---------------+

重複するライブラリをモジュール間で共有する

もし上記のReactのバージョンが同じであれば、両方にバンドルしているのは無駄といえる。sub.jsもmain.jsでバンドルしているReactを使用できれば生成されるJavaScriptのファイルを小さくできる。モジュールフェデレーションはこれを実現する。

  +-------+
  | React |<---------------------+
  +---+---+                      |
      |                          |
      | Module Federation        | 利用
      |                          |
+-----+---------+          +-----+---------+
|               |          |               |
| main.js       |          | sub.js        |
|               |          |               |
+---------------+          +---------------+

いつ使うのか

1つのアプリケーションを複数の成果物で構成する必要のある場合に使用する。特にマイクロフロントエンドを採用する場合、この手法を用いる。それぞれの成果物の間に、互いに依存関係を持たせなければ、個別に開発やデプロイができる。

webpack 5

モジュールフェデレーションはwebpack 5になってサポートされた機能だ。現時点ではwebpackを採用する以外の選択肢は枯れ具合から言って避けたほうが良さそうだ。

実装の例

example-dog/webpack.config.js

const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'dog.js',
    path: path.resolve(__dirname, 'dist'),
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
    compress: false,
    port: 9000,
  },
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        cat: 'cat@http://localhost:9001/cat.js',
      },
    }),
  ],
};

example-dog/public/index.html

example-dog/src/index.js

import axios from "axios";

async function loadCat () {
  const resp = await axios.get("/data.json");
  const { getCat } = await import('cat/cat');
  const catDom = getCat();
  document.body.appendChild(catDom);
}


function component() {
  const button = document.createElement('button', "a", "b");
  button.innerHTML = "Load Cat";
  button.onclick = loadCat;
  return button;
}

document.body.appendChild(component());

example-cat/webpack.config.js

const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    publicPath: 'auto',
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
    compress: false,
    port: 9001,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'cat',
      filename: 'cat.js',
      shared: {
	axios: deps.axios,
      },
      exposes: {
	"./cat": "./src/index",
      },
      // library: {
      //   type: 'var', // scriptタグを経由する、他のオプションはこちら https://github.com/webpack/webpack/blob/dev-1/schemas/plugins/container/ModuleFederationPlugin.json#L155
      //   name: 'cat' // importされるときの名前(この場合は、import('page1/xxxx'))
      // },
      // exposes: {
      //   Cat: './src/index.js',
      // },
    }),
  ],  
};

example-cat/public/index.html

example-cat/src/index.js

export function getCat () {
  const element = document.createElement('div');
  element.innerHTML = "<p>Example Cat</p>"
  return element;
}