模組聯盟

動機

多個獨立的建置應該形成單一的應用程式。這些獨立的建置就像容器,可以在建置之間公開和使用程式碼,建立單一的統一應用程式。

這通常稱為微前端,但並不限於此。

低階概念

我們區分本地和遠端模組。本地模組是常規模組,是目前建置的一部分。遠端模組不是目前建置的一部分,但會在執行階段從遠端容器載入。

載入遠端模組被視為非同步作業。使用遠端模組時,這些非同步作業會放置在遠端模組和進入點之間的下一個區塊載入作業中。沒有區塊載入作業就無法使用遠端模組。

區塊載入作業通常是 import() 呼叫,但 require.ensurerequire([...]) 等較舊的建構也受支援。

容器是透過容器進入點建立的,容器進入點公開對特定模組的非同步存取。公開的存取分為兩個步驟

  1. 載入模組 (非同步)
  2. 評估模組 (同步)。

步驟 1 會在區塊載入期間完成。步驟 2 會在模組評估期間與其他 (本地和遠端) 模組交錯進行。這樣,評估順序就不會受到將模組從本地轉換為遠端或反之亦然而影響。

可以巢狀容器。容器可以使用其他容器的模組。容器之間的循環相依性也是可能的。

高階概念

每個建置都充當容器,並將其他建置當成容器使用。這樣,每個建置都能透過從其容器載入,來存取任何其他公開的模組。

共用模組是既可覆寫,又可作為覆寫提供給巢狀容器的模組。它們通常在每個建置中指向同一個模組,例如同一個函式庫。

packageName 選項允許設定套件名稱以尋找 requiredVersion。預設會自動推斷模組要求,將 requiredVersion 設定為 false 時,會停用自動推斷。

建構區塊

ContainerPlugin (低階)

此外掛程式會建立一個額外的容器項目,其中包含指定的公開模組。

ContainerReferencePlugin (低階)

此外掛程式會將特定參考新增至容器作為外部程式,並允許從這些容器匯入遠端模組。它也會呼叫這些容器的 override API,以提供覆寫給它們。本機覆寫(透過 __webpack_override__override API,當建置也是容器時)和指定的覆寫會提供給所有參考的容器。

ModuleFederationPlugin (高階)

ModuleFederationPlugin 結合了 ContainerPluginContainerReferencePlugin

概念目標

  • 應該可以公開和使用 webpack 支援的任何模組類型。
  • 區塊載入應該並行載入所有需要載入的內容(網路:單次往返伺服器)。
  • 從使用者控制到容器
    • 覆寫模組是單向操作。
    • 同層容器無法覆寫彼此的模組。
  • 概念應該與環境無關。
    • 可以在網路、Node.js 等環境中使用。
  • 在共用中使用相對和絕對要求
    • 即使未使用,也會持續提供。
    • 將解析為相對應的 config.context
    • 預設不使用 requiredVersion
  • 在共用中的模組要求
    • 僅在使用時提供。
    • 將符合組建中所有已使用的相等模組要求。
    • 將提供所有符合的模組。
    • 將從圖表中這個位置的 package.json 中擷取 requiredVersion
    • 當您有巢狀的 node_modules 時,可以提供和使用多個不同版本。
  • 在共用中具有尾端 / 的模組要求將符合所有具有此字首的模組要求。

使用案例

每個頁面有獨立的組建

單頁式應用程式的每個頁面都從容器組建中公開,並在獨立的組建中公開。應用程式外殼也是獨立的組建,將所有頁面視為遠端模組。這樣每個頁面都可以獨立部署。當路由更新或新增新的路由時,會部署應用程式外殼。應用程式外殼會將常用的函式庫定義為共用模組,以避免在頁面組建中重複使用這些函式庫。

元件程式庫作為容器

許多應用程式共用一個元件程式庫,該程式庫可以建置為一個容器,其中會公開每個元件。每個應用程式都會從元件程式庫容器中使用元件。可以個別部署元件程式庫的變更,而不需要重新部署所有應用程式。應用程式會自動使用元件程式庫的最新版本。

動態遠端容器

容器介面支援 `get` 和 `init` 方法。`init` 是 `async` 相容的方法,會呼叫一個引數:共用範圍物件。此物件用作遠端容器中的共用範圍,並會填入主機提供的模組。可以在執行階段動態地將遠端容器連線到主機容器。

init.js

(async () => {
  // Initializes the shared scope. Fills it with known provided modules from this build and all remotes
  await __webpack_init_sharing__('default');
  const container = window.someContainer; // or get the container somewhere else
  // Initialize the container, it may provide shared modules
  await container.init(__webpack_share_scopes__.default);
  const module = await container.get('./module');
})();

容器會嘗試提供共用模組,但如果已使用共用模組,系統會顯示警告,且會忽略提供的共用模組。容器仍可能將其用作備用選項。

如此一來,您可以動態載入提供不同版本共用模組的 A/B 測試。

範例

init.js

function loadComponent(scope, module) {
  return async () => {
    // Initializes the shared scope. Fills it with known provided modules from this build and all remotes
    await __webpack_init_sharing__('default');
    const container = window[scope]; // or get the container somewhere else
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}

loadComponent('abtests', 'test123');

請參閱完整實作

基於 Promise 的動態遠端

一般而言,遠端會使用 URL 組態,如下例所示

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: 'app1@https://127.0.0.1:3001/remoteEntry.js',
      },
    }),
  ],
};

但您也可以將 Promise 傳遞給此遠端,它將在執行期間解析。您應該使用符合上述所述的 get/init 介面的任何模組來解析此 Promise。例如,如果您想透過查詢參數傳入應使用的聯合模組版本,您可以執行類似下列的動作

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: `promise new Promise(resolve => {
      const urlParams = new URLSearchParams(window.location.search)
      const version = urlParams.get('app1VersionParam')
      // This part depends on how you plan on hosting and versioning your federated modules
      const remoteUrlWithVersion = 'https://127.0.0.1:3001/' + version + '/remoteEntry.js'
      const script = document.createElement('script')
      script.src = remoteUrlWithVersion
      script.onload = () => {
        // the injected script has loaded and is available on window
        // we can now resolve this Promise
        const proxy = {
          get: (request) => window.app1.get(request),
          init: (arg) => {
            try {
              return window.app1.init(arg)
            } catch(e) {
              console.log('remote container already initialized')
            }
          }
        }
        resolve(proxy)
      }
      // inject this script with the src set to the versioned remoteEntry.js
      document.head.appendChild(script);
    })
    `,
      },
      // ...
    }),
  ],
};

請注意,在使用此 API 時,您必須解析包含 get/init API 的物件。

動態 Public Path

提供主機 API 來設定 publicPath

透過公開遠端模組的方法,主機可以設定遠端模組的 publicPath。

當您在主機網域的子路徑上掛載獨立部署的子應用程式時,此方法特別有用。

場景

您有一個主機應用程式,其網址為 https://my-host.com/app/*,而子應用程式則位於 https://foo-app.com。子應用程式也掛載在主機網域上,因此預期 https://foo-app.com 可透過 https://my-host.com/app/foo-app 存取,而 https://my-host.com/app/foo-app/* 要求會透過代理伺服器重新導向至 https://foo-app.com/*

範例

webpack.config.js (遠端)

module.exports = {
  entry: {
    remote: './public-path',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote', // this name needs to match with the entry name
      exposes: ['./public-path'],
      // ...
    }),
  ],
};

public-path.js (遠端)

export function set(value) {
  __webpack_public_path__ = value;
}

src/index.js (主機)

const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');

//bootstrap app  e.g. import('./bootstrap.js')

從指令碼推論 publicPath

可以從 document.currentScript.src 中的腳本標籤推論出 publicPath,並在執行階段使用 __webpack_public_path__ 模組變數設定它。

範例

webpack.config.js (遠端)

module.exports = {
  entry: {
    remote: './setup-public-path',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote', // this name needs to match with the entry name
      // ...
    }),
  ],
};

setup-public-path.js(遠端)

// derive the publicPath with your own logic and set it with the __webpack_public_path__ API
__webpack_public_path__ = document.currentScript.src + '/../';

疑難排解

Uncaught Error: 共享模組無法急切使用

應用程式急切執行一個作為全方位主機運作的應用程式。有以下選項可供選擇

您可以在 Module Federation 的進階 API 中將相依性設定為急切,這不會將模組放入非同步區塊中,而是同步提供它們。這讓我們可以在初始區塊中使用這些共享模組。但請小心,因為所有提供的和後備模組都將永遠下載。建議僅在應用程式的某一點提供它,例如 shell。

我們強烈建議使用非同步邊界。它會分割較大區塊的初始化程式碼,以避免任何額外的往返行程,並在一般情況下提升效能。

例如,您的入口看起來像這樣

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

讓我們建立 bootstrap.js 檔案,並將入口的內容移入其中,然後將該 bootstrap 匯入入口

index.js

+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));

bootstrap.js

+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />, document.getElementById('root'));

這個方法有效,但可能會有限制或缺點。

透過 ModuleFederationPlugin 設定相依性的 eager: true

webpack.config.js

// ...
new ModuleFederationPlugin({
  shared: {
    ...deps,
    react: {
      eager: true,
    },
  },
});

Uncaught Error: 模組「./Button」在容器中不存在。

它可能不會顯示為 "./Button",但錯誤訊息看起來會很類似。如果你從 webpack beta.16 升級到 webpack beta.17,通常會看到這個問題。

在 ModuleFederationPlugin 中,將 exposes 從下列變更:

new ModuleFederationPlugin({
  exposes: {
-   'Button': './src/Button'
+   './Button':'./src/Button'
  }
});

Uncaught TypeError: fn 不是函式

你可能缺少遠端容器,請務必將其加入。如果你已為你嘗試使用的遠端載入容器,但仍然看到這個錯誤,請將主機容器的遠端容器檔案也加入 HTML。

來自不同遠端的模組之間的衝突

如果你要從不同遠端載入多個模組,建議為你的遠端組建設定 output.uniqueName 選項,以避免多個 webpack 執行時間之間的衝突。

10 貢獻者

sokrachenxsanEugeneHlushkojamesgeorge007ScriptedAlchemysnitin315XiaofengXie16KyleBastienAlevaleburhanuday