Webpack 5內建plugin ModuleFederationPlugin,其功能為匯出exposed出專案部分javascript code,供其他專案使用,並指定有哪些package需要共用。如此便可降低程式及library重複。

Module Federation用途

專案可匯出其部分javascript code包括components,並import到其他專案:

  • Host專案使用專案A的App Component
  • Host專案使用專案B的Monitor Component
  • Host專案使用專案C的Map Component
  • Host專案使用專案D的某一個JS util file

Options

name

將專案expose出去後,root路徑的名稱

filename

將專案expose出去後,build出來的檔案名稱

remotes

指定該專案需要用到哪些remote專案。

等同於直接在該專案的index.html加入 <script src="${remote_path_and_file_name}"></script>

example: <script src="http://localhost:8081/remoteEntry.js"></script>

shared

指定哪一些用到的packages需要與其他的專案共享。

例如React在同一個頁面下不能匯入兩次以上,若有兩個federation專案都使用React就必須在shared加入,並加入Shared hints - singleton: true。

Example

Host Setting:

// webpack.config.js
new ModuleFederationPlugin({
  name: 'host',
  // 指定import那些remote
  remotes: {
    qss: 'qss@http://localhost:9001/remoteEntry.js',
  },
  // 指定那些package與其它專案shared
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})

Remote Setting:

new ModuleFederationPlugin({
	name: 'qss',
	filename: 'remoteEntry.js',
  // 指定要expose那些js
	exposes: {
		'./qss': './src/app/App',
	},
	shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})

Host Import Remote:

// App.js
import React, { Suspense } from "react";
// 使用 ${remote_name}/${expose_js_name} import
const RemoteApp = React.lazy(() => import("qss/qss"));

const App = () => {
  return (
    <div>
      <div
        style=
      >
        <h1>App1</h1>
      </div>
      <Suspense fallback={"loading..."}>
        <RemoteApp />
      </Suspense>
    </div>
  );
};

export default App;

實作

Redux 設定

當host與remote皆存在redux時,兩個redux會使用不同的react context,所以會互相隔離。

合併store實驗

以下實作如何將redux merge合併。但其實實務上應該很少會使用到這個方法。 redux reducer injection example

Host的export store需要重新處理,寫一個function,開放merge store。

const createReducer = (asyncReducer) => {
  return combineReducers({
    ...staticReducer,
    ...asyncReducer,
  });
};

export default function handleStore(initialState) {
  const store = configureStore({ reducer: createReducer() });

  store.asyncReducer = {};

  store.injectReducer = (key, asyncReducer) => {
    store.asyncReducer[key] = asyncReducer;
    store.replaceReducer(createReducer(store.asyncReducer));
  };

  return store;
}

export const store = handleStore();

並在import remote時傳進remote components中。

// App.jsx
import React, { Suspense } from 'react';
import { Provider } from 'react-redux';

import 'regenerator-runtime';

import Header from './components/Header';
import Panel from './components/Panel';

import { store } from './store';

const Remote1 = React.lazy(() => import('remote1/App'));

const App = () => {
  return (
    <Provider store={store}>
      <div>
        <Header />
        <Panel />
        <div>
          <h3>From Remote 1</h3>
          <div
            style=
          >
            <Suspense fallback="Loading">
              <Remote1 store={store} />
            </Suspense>
          </div>
        </div>
    </Provider>
  );
};

export default App;

在remote app收到store後,需要將自己的reducer inject進host的store

const AppWrapper = ({ store }) => {
  useEffect(() => {
    store.injectReducer('remote', reducer);
  }, []);

  return <Provider store={store || {}}><App /></Provider>;
};

selector

remote app 需要重新撰寫如:

(state) => state.main.appName;

需改成

(state) => state.remote.main.appName;

Vue 設定

Vue in Vue (vue-cli + Vue 3)

webpack 設定

  • 新增Module Federation Plugin在vue.config.js
  • @vue/cli-service等vue cli tool需使用版本: 5.0.0-beta.3
  • 須注意vue.config.js中publicPath的設定值

必須將vue: singleton加入shared設定中,不然會跳出錯誤 [Vue warn]: Invalid VNode type: Symbol(Text) (symbol)

解法出處: [Vue warn]: Invalid VNode type: Symbol(Text) (symbol) · Issue #2913 · vuejs/vue-next (github.com)

Remote setting:

// vue.config.js
const ModuleFederationPlugin =
  require('webpack').container.ModuleFederationPlugin;

module.exports = {
  publicPath: 'http://localhost:8082/',
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'remote2',
        filename: 'remoteEntry.js',
        exposes: {
          './HelloWorld': './src/components/HelloWorld',
          './Remote': './src/components/Remote',
        },
        shared: {
          vue: { singleton: true },
        },
      }),
    ],
    devServer: {
      port: 8082,
    },
  },
};

Host setting:

// vue.config.js
const ModuleFederationPlugin =
  require('webpack').container.ModuleFederationPlugin;

module.exports = {
  publicPath: 'http://localhost:8083/',
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'host',
        remotes: {
          remote2: 'remote2@http://localhost:8082/remoteEntry.js',
        },
        shared: {
          vue: { singleton: true },
        },
      }),
    ],
    devServer: {
      port: 8083,
    },
  },
};

import至app

在Vue 3,必須使用 defineAsyncComponent 否則會出現警告: [Vue warn]: Invalid VNode type: undefined (undefined)

export default {
  name: 'App',
  components: {
    HelloWorld,
    remote: defineAsyncComponent(() => import('remote2/Remote')),
  }
}

Vue in React (Vue cli + Vue 3)

expose的Vue component,需要export vueApp.mount() method出來,並在react中調用該method。

// bootstrap.js
import { createApp } from 'vue';
import App from './App.vue';

const mount = (el) => {
  const app = createApp(App);
  app.mount(el);
};

export { mount };
// VueApp.jsx
import { mount } from 'remote2/App';
import React, { useRef, useEffect } from 'react';

export default () => {
  const ref = useRef(null);

  useEffect(() => {
    mount(ref.current);
  }, []);

  return <div ref={ref} />;
};

Reference