module Federation 簡介與應用

什麼是 module Federation

module Federation(下面簡稱 MF) 是 webpack5 推出的最新的概念

有用過 webpack 的小夥伴都知道, 在我們打包時, 都會對資源進行分包, 或者使用異步加載路由的方案,
這樣打出來的包(也叫 chunk), 在我們使用時, 就是一個單獨的加載

在過去, chunk 只是在一個項目中使用, webpack5 中, chunk 通過 路徑/fetch 等方式也可以提供給其他應用使用

這便是 MF 的發展由來, 不得不說這是一個很有想象力的API

MF 的粒度, 最小可以是一個組件/組件庫, 最大可以是一個頁面, 取決於你怎樣使用

基礎使用

首先我們來看 ModuleFederationPlugin 的一些基礎配置參數:

const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // options
    }),
  ],
};

參數

file filename

name 是容器的名稱, filename 是具體的文件名入口

如果沒有 filename 則以 name 爲文件名, 需要注意的是 name 需要是唯一值

new ModuleFederationPlugin({
    name: 'app2', // 名稱
    filename: 'remoteEntry.js', // 入口文件
    // 打包後, 就會自動打出 remoteEntry.js 
    // 他的內容就是 exposes 參數中的映射
})

exposes

在這個配置裏你可以暴露你想要的所有內容


new ModuleFederationPlugin({
  name: 'app2', // 名稱
  filename: 'remoteEntry.js', // 入口文件
  exposes: {
    './Widget': './src/Widget',
  },
})

exposeskey 不能是一個直接的名稱, 如 'Widget': './src/Widget', 這樣會報錯

shared

使用 shared 可以最大限度地減少依賴關係的重複,因爲遠程依賴於主機的依賴關係。如果主機缺少一個依賴項,遠程只在必要時下載其依賴項

使用 dependencies 是爲了共享模塊的版本和 package.json 中的版本保持一致。如果不一致則會打印警告

const deps = require('./package.json').dependencies;

new ModuleFederationPlugin({
  name: 'app2',
  filename: 'remoteEntry.js',
  exposes: {
    './Widget': './src/Widget',
  },
  shared: {
    moment: deps.moment,
    react: {
      requiredVersion: deps.react,
      import: 'react', // 所提供的模塊應該被放置在共享範圍內。如果在共享範圍內沒有找到共享模塊或版本無效,這個提供的模塊也作爲後備模塊。
      shareKey: 'react', // 所請求的共享模塊在這個鍵下從共享範圍中被查找出來。
      shareScope: 'default', // 共享範圍的名稱。
      singleton: true, 
    },
    'react-dom': {
      requiredVersion: deps['react-dom'],
      singleton: true,
    },
  },
})

關於 singleton

這個參數只允許在共享範圍內有一個單一版本的共享模塊(默認情況下禁用)。一些庫使用全局的內部狀態(例如 react, react-dom)。
因此,在同一時間只有一個庫的實例在運行是很關鍵的。

remotes

一般來說,remote 是使用 URL 配置的,示例如下

new ModuleFederationPlugin({
  name: "app1",
  remotes: {
    app2: 'app2@http://localhost:3002/remoteEntry.js',
  }
})

當然 remotes 還可以有其他的擴展, 在後面會詳細說明

引用 MF

MF 插件組合了 ContainerPluginContainerReferencePlugin
所以它既是一個入口, 也是一個出口

所以我們再使用 MF 時, 也是需要添加對應插件:

new ModuleFederationPlugin({
  name: 'mainApp',
  remotes: {
    app2: 'app2@http://localhost:3002/remoteEntry.js',
  },
  // 省略 shared
})

運行時截圖:

image

之後我們就可以直接使用組件了:

import App2Widget from 'app2/Widget';

function App() {
  // 當成正常組件一樣使用
  return (
    <div>
      <h1>Dynamic System Host</h1>
      <h2>main App</h2>
      <App2Widget/>
    </div>
  );
}

remotes 的方案

環境變量

在不同的環境中使用不同的鏈接, 可以解決 prodev 的不同環境問題
但是在大型應用中, 環境較多, 配置(添加/修改 url)就比較麻煩了

new ModuleFederationPlugin({
  name: "Host",
  remotes: {
    RemoteA: `RemoteA@${env.A_URL}/remoteEntry.js`,
    RemoteB: `RemoteB@${env.B_URL}/remoteEntry.js`,
  },
})

Webpack External Remotes Plugin

有一個方便的 Webpack 插件,由 MF 的創建者之一 Zack Jackson 開發,稱爲 external-remotes-plugin。它允許我們使用模板在運行時解析 URL

plugins: [
  new ModuleFederationPlugin({
    name: "Host",
    remotes: {
      RemoteA: "RemoteA@[window.appAUrl]/remoteEntry.js",
      RemoteB: "RemoteB@[window.appBUrl]/remoteEntry.js",
    },
  }),
  new ExternalTemplateRemotesPlugin(),
]

在從遠程應用程序加載任何代碼之前, 我們加可以定義 window 的屬性來靈活地定義我們的 URL

這種方法是完全動態的,可以解決我們的用例,但這種方法仍有一點限制:
我們不能完全控制加載的生命週期。

Promise

基於 promise 的獲取方案, 此方案在官網也有所提及

但是你也可以向 remote 傳遞一個 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')
      
      const remoteUrlWithVersion = 'http://localhost: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)
      }
      document.head.appendChild(script);
    })
    `,
      },
      // ...
    }),
  ],
};

請注意當使用該 API 時,你 必須 resolve 一個包含 get/init API 的對象。

promise 中我們創建一個 script 標籤, 同時添加動態 URL, 不過此方案是比較死板的, 因爲 url 仍舊是寫在配置中

Dynamic Remote Containers

webpack 官網中有一種方案, 即動態遠程容器

這種方案就是像加載 react 子應用一樣加載 MF 應用

我們的插件可以不用設置 remotes:

plugins: [
  new ModuleFederationPlugin({
    name: "Host",
    remotes: {},
  }),
]

核心加載程序:

function loadComponent(scope, module) {
  return async () => {
    // 初始化共享作用域(shared scope)用提供的已知此構建和所有遠程的模塊填充它
    await __webpack_init_sharing__('default');
    const container = window[scope]; // or get the container somewhere else
    // 初始化容器 它可能提供共享模塊
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}

webpack_init_sharing 是一些 webpack 編譯的變量, 最後運行時都是會轉換成 webpack_require
webpack_require 是 webpack 運行時引用文件內容的方法

container 指的是我們在 webpack 配置的 remotes 中配置的一個應用。

module 指的是另一個 exposes 字段中的定義。

最後封裝獲取到 hooks:

// 這裏的 url, scope, module  都是可以通過接口什麼的異步獲取, 做到完全動態
// 原理還是通過 script 標籤加載js 代碼
const { Component: FederatedComponent, errorLoading } = useFederatedComponent('http://localhost:3002/remoteEntry.js', 'app2', './Widget');

<Suspense fallback={'loading...'}>
  {errorLoading
    ? `Error loading module "${module}"`
    : FederatedComponent && <FederatedComponent />}
</Suspense>

image

其中 remoteEntry 就是我們的入口了, 762 和 700, 這兩個是 moment 的主題和多語言文件
而 630 則是 Widget 文件的內容

子組件單獨加載 moment, 而沒有 react, react-dom, 就是因爲我們的 shared 配置

整個 Dynamic Remote Containers 的加載流:

image

具體例子可點此查看

基於 MF 的框架

這裏也有一個框架是基於 MF 的: EMP

該框架通過靈活的共享配置, 可自定義選擇 MF / CDN / ES import / Dll 多種方式來共享庫

擴展的可能性

MF 目前的 定位在於公共組件庫/業務庫的複用、統一, 但是他能作爲應用級別的載體嗎

qiankun 中, 子應用的獲取, 是通過 fetch 來獲取實例, 並在沙盒中解析的

但是 MF 的組件就不能實現這種場景, 因爲我們一開始引用的是入口文件, 後續具體文件不能通過接口獲取

只能通過 script 來拿, 但是這樣就沒有了 js 環境的隔離

所以在應用級別上, 目前的結論是: 可以當做一個乞丐版微前端的框架

但是也需要注意各個應用直接的重複關係, JS/CSS 的隔離問題, 同時他也缺少對應生命週期來管理

在這方面, qiankun 是一個成熟的案例實現, MF 值得嘗試的場景還是在業務公共組件這一塊

總結

MF 已經引來了許多的嚐鮮者, 他是一種可擴展的解決方案,在獨立的應用程序之間共享代碼,對開發者來說非常方便。

但是也存在以下的問題點:

  1. 需要使用 webpack5, 現在很多老項目是在用 webpack4, 而又有一些新項目使用了 vite
  2. shared 依賴不能 tree sharking
  3. 代碼執行不能使用沙箱隔離, 不太推薦做到應用級別

引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章