天天看點

Module Federation你的浪漫我來懂

前言

我們在實際開發中,經曆過許多次的子產品共享的場景。最常見的場景例如我們将代碼封裝後根據版本和環境的不同釋出到公共平台或者私有平台,供不同項目進行使用,npm 工程化就是其中最日常的實踐。

【通關目标: 在頁面中插入輪播圖子產品】

NPM 方式-share lib

将輪播圖代碼打包釋出到 NPM 包。主項目中通過 package.json 依賴加載到本地進行

編譯

打包。 【biu~通關成功,目前一星】

當投入生産時,多個項目對于被引入的輪播圖代碼都沒有進行共享,他們是各自獨立的。如果二次封裝的某個子產品進行更改并且要求全部同步……亦或者後期疊代或者定制化需求——

小孫 : “啊!!那豈不是要手動去一一修改?” 代碼爸爸慈祥一笑,看着二傻子痛苦加班。

Module Federation-share subApp

初初檢視到一些資料的時候,腦海中浮現一些遠古項目:項目中通過 iframe 引入别的網頁作為一個塊進行展示,當然這也是微前端的一種實作,但是受 postMessage 通信機制、重新整理後退、安全問題、SEO、Cookie、速度慢等影響,目前還有許多難以解決的點,一旦複雜度提升,後期疊代就可能需要更多的成本去維護項目或者妥協功能。

Module Federation 的出現使得多部門協作開發變得更便捷。多個單獨的建構形成一個應用程式。這些單獨的建構彼此之間不應該有依賴關系,是以可以單獨開發和部署它們,它們可以随時被應用,再各自成為新的應用塊。官網對于這塊概念拆解成

Low-level concepts

High-level concepts

讓我們來結合配置項來更詳細了解作者的整個設計過程。
Module Federation你的浪漫我來懂

Low-level concepts - 代碼中的分子與原子

Module Federation你的浪漫我來懂

PS: 這裡的引用不是文法中的引用哦

這裡小孫想引用一下化學中的分子與原子的關系。這張圖要側重說明的是

本地子產品

遠端子產品

  • 在圖中每一份 Host 對于它本身來說, 都被了解成

    本地子產品

    ,它是目前應用建構的一部分,對于它本身項目來說,它是分子,是一個容器。但是對于引用它整個項目的 Host 爸爸來說,它整個遠端子產品是原子。
  • 每一份 Romote

    遠端子產品

    是原子,它是運作時從容器加載的子產品,是異步行為。當使用遠端子產品時,這些異步操作将放置在遠端子產品和入口點之間的下一個塊加載操作中。如果沒有塊加載操作,就不可能使用遠端子產品。
A container is created through a container entry, which exposes asynchronous access to the specific modules. The exposed access is separated into two steps:
  1. loading the module (asynchronous) 加載子產品(異步)
  2. evaluating the module (synchronous) 執行子產品(同步)
加載子產品将在 chunk 加載期間完成。執行子產品将在與其他(本地和遠端)的子產品交錯執行期間完成。

我們來找個例子配合看一下整個設計過程:

一個例子介紹 MF 的正常用法

例子把 App3 的元件引入到 App2 的元件,然後 App1 再引入 App2 的這個二次封裝的元件。這個例子還是非常接近目前常見的開發場景。

/* 業務代碼如下 */

// App1 webpack-config
new ModuleFederationPlugin({
    // ...other config
    name: "app1",
    remotes: {
      app2: `app2@${getRemoteEntryUrl(3002)}`,
    },
})

// App2 webpack-config
new ModuleFederationPlugin({
    // ...other config
    name: "app2",
    filename: "remoteEntry.js",
    exposes: {
      "./ButtonContainer": "./src/ButtonContainer",
    },
    remotes: {
      app3: `app3@${getRemoteEntryUrl(3003)}`,
    },
})

// App3 webpack-config
new ModuleFederationPlugin({
  // ...other config
  name: "app3",
  filename: "remoteEntry.js",
  exposes: {
    "./Button": "./src/Button",
  },
}),
           

加載子產品相關代碼解析

// from App1 的 remoteEntry.js 中__webpack_modules__某個對象的 value
"webpack/container/entry/app3":((__unused_webpack_module, exports, __webpack_require__) => {
  var moduleMap = {
    "./Button": () => {
      return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react-_0085"), __webpack_require__.e("src_Button_js")]).then(() => (() => ((
        __webpack_require__( /*! ./src/Button */
          "./src/Button.js")))));
    }
  };
  // get 方法作用:擷取 Scope
  var get = (module, getScope) => {
    __webpack_require__.R = getScope;
    getScope = (
      __webpack_require__.o(moduleMap, module) ?
      moduleMap[module]() :
      Promise.resolve().then(() => {
        throw new Error('Module "' + module +'" does not exist in container.');
      })
    );
    __webpack_require__.R = undefined;
    return getScope;
  };
  // init 方法作用:初始化作用域對象 并把依賴存儲到 shareScope 中
  var init = (shareScope, initScope) => {
    if (!__webpack_require__.S) return;
    var oldScope = __webpack_require__.S["default"];
    var name = "default"
    if (oldScope && oldScope !== shareScope) throw new Error(
      "Container initialization failed as it has already been initialized with a different share scope"
    );
    __webpack_require__.S[name] = shareScope;
    return __webpack_require__.I(name, initScope);
  };
  // This exports getters to disallow modifications
  __webpack_require__.d(exports, {
    get: () => (get),
    init: () => (init)
  });
})


// App main.js 後面有詳細代碼 這邊簡單介紹一下就是一個 JSONP 的下載下傳
__webpack_require__.I = (name, initScope) => {})

/* 在消費子產品執行的操作 from consumes   */
// 确認好 loaded 以後調用原子的 Scope
var get = (entry) => {
  entry.loaded = 1;
  return entry.get()
};
// 消費子產品再執行原子的異步初始化行為 在這個子產品還會處理後面提到的一個疑問 公共子產品的版本問題
//__webpack_require__.S[scopeName] 取出 scopeName 對應的 scope
var init = (fn) => (function(scopeName, a, b, c) {
  var promise = __webpack_require__.I(scopeName);
  if (promise && promise.then) return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], a, b, c));
  return fn(scopeName, __webpack_require__.S[scopeName], a, b, c);
});
           
  • 執行 init 初始化以後,收集的依賴存儲到 shareScope 中,并初始化作用域。
  • 中間會涉及到 版本号處理,關聯關系,公共子產品等處理,拼資料挂載到__webpack_require__上使用。
  • 調用時通過通過 JSONP 的遠端加載子產品(異步行為),相關代碼如下:
// from App1 main.js
/***/ "webpack/container/reference/app2":
/*!*******************************************************!*\
  !*** external "app2@//localhost:3002/remoteEntry.js" ***!
  \*******************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {

"use strict";
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
 if(typeof app2 !== "undefined") return resolve();
 __webpack_require__.l("//localhost:3002/remoteEntry.js", (event) => {
  //...這個方法根據 JSONP 加載遠端腳本 
 }, "app2");
}).then(() => (app2));
  
// __webpack_require__.l 定義如下
// 對 IE 和 ES module 單獨處理 如果是 ES module,取 module[default]的值
// 這邊特别定義 inProgress 去監控多個 url 的回調狀态,這段設計挺有意思的
__webpack_require__.l = (url, done, key, chunkId) => {
    if(inProgress[url]) { inProgress[url].push(done); return; }
    var script, needAttach;
    if(key !== undefined) {
     var scripts = document.getElementsByTagName("script");
     for(var i = 0; i < scripts.length; i++) {
      var s = scripts[i];
      if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
     }
    }
    if(!script) {
     needAttach = true;
     script = document.createElement('script');
   
     script.charset = 'utf-8';
     script.timeout = 120;
     if (__webpack_require__.nc) {
      script.setAttribute("nonce", __webpack_require__.nc);
     }
     script.setAttribute("data-webpack", dataWebpackPrefix + key);
     script.src = url;
    }
    inProgress[url] = [done];
    var onScriptComplete = (prev, event) => {
     // avoid mem leaks in IE.
     script.onerror = script.onload = null;
     clearTimeout(timeout);
     var doneFns = inProgress[url];
     delete inProgress[url];
     script.parentNode && script.parentNode.removeChild(script);
     doneFns && doneFns.forEach((fn) => (fn(event)));
     if(prev) return prev(event);
    }
    ;
    var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
    script.onerror = onScriptComplete.bind(null, script.onerror);
    script.onload = onScriptComplete.bind(null, script.onload);
    needAttach && document.head.appendChild(script);
   };
  })();

           

前面說了單個具象化的加載子產品和執行子產品的代碼,現在說說分子與原子之間的代碼關系,如何知曉并加載原子代碼:

// 每個分子 main.js 中 例如 App1 隻引入了 App2 的
var __webpack_modules__ = ({
 "webpack/container/reference/app2":": ()
  ....
})

// 如果是有原子代碼的 檢視 remotes loading 子產品
// 執行後找到//localhost:3002/remoteEntry.js 的檔案 再異步執行裡面的原子代碼
__webpack_require__.l("//localhost:3002/remoteEntry.js", (event) => {
  if (typeof app2 !== "undefined") return resolve();
  var errorType = event && (event.type === 'load' ? 'missing' :
                            event.type);
  var realSrc = event && event.target && event.target.src;
  __webpack_error__.message = 'Loading script failed.\n(' +
    errorType + ': ' + realSrc + ')';
  __webpack_error__.name = 'ScriptExternalLoadError';
  __webpack_error__.type = errorType;
  __webpack_error__.request = realSrc;
  reject(__webpack_error__);
}, "app2");

// 然後加載相關 chuck 的時候根據枚舉進行 get 調用 
var chunkMapping = {"app2/ButtonContainer": ["webpack/container/remote/app2/ButtonContainer"]};
var idToExternalAndNameMapping = {
  "webpack/container/remote/app2/ButtonContainer": ["default","./ButtonContainer","webpack/container/reference/app2"]
};

__webpack_require__.f.remotes = (chunkId, promises) => {
  if(__webpack_require__.o(chunkMapping, chunkId)) {
    chunkMapping[chunkId].forEach((id) => {
      var getScope = __webpack_require__.R;
      if(!getScope) getScope = [];
      // 擷取渲染時候的 moduleName
      var data = idToExternalAndNameMapping[id];
      if(getScope.indexOf(data) >= 0) return;
      getScope.push(data);
      if(data.p) return promises.push(data.p);
      var onError = (error) => {
        // 處理錯誤然後給一個标志資料表示錯誤 太長不看
        data.p = 0;
      };
      var handleFunction = (fn, arg1, arg2, d, next, first) => {
        // 異步執行方法 太長不看
      }
      var onExternal = (external, _, first) => (external ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) : onError());
      // 核心代碼 本質是調用 get 方法
      var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));
      var onFactory = (factory) => {
        data.p = 1;
        __webpack_modules__[id] = (module) => {
          module.exports = factory();
        }
      };
      handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
    });
  }
}

// 調用 get 以後 下載下傳下面這個檔案再做具象化的處理
// 在打包後的代碼中 import 相關的原子子產品 異步加載
(self["webpackChunk_nested_app2"] = self["webpackChunk_nested_app2"] || []).push([["src_bootstrap_js"], {
 "./src/App.js": "...",
  "./src/ButtonContainer.js": "...",
  "./src/bootstrap.js":"..."
}])

           

High-level concepts - 雙向共享和推斷

前面說了容器的概念,再深入拓展一個過去常有的場景: 暫不考慮抽離公共邏輯的基礎上,元件 A 群組件 B 都互相需要移植一部分功能,你刷刷刷複制對應代碼過去,後期每次疊代都需要同時更新元件 A 群組件 B 中的對應内容,那如果這個緯度是兩個項目呢?

疑問一:例子中的 import("./bootstrap")作為入口是為什麼

看看打包後做了什麼:

(self["webpackChunk_nested_app2"] = self["webpackChunk_nested_app2"] || []).push([["src_bootstrap_js"], {
 "./src/App.js": "...",
  "./src/ButtonContainer.js": "...",
  "./src/bootstrap.js":"..."
}])

//每個子產品大概處理幾件事
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    // exports module
    __webpack_require__.r(__webpack_exports__)
    // 處理不同 module 類型之間的差異 如果是 ES module 取這個 default 的值
    __webpack_require__.d(__webpack_exports__, {
      /* harmony export */
      "default": () => (__WEBPACK_DEFAULT_EXPORT__)
      /* harmony export */
    });
    //...具體元件邏輯 或者 import 原子部分的代碼~觸發後續的回調鈎子去初始化 scoped
   // 最後挂載
   react_dom__WEBPACK_IMPORTED_MODULE_2___default().render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(
   _App__WEBPACK_IMPORTED_MODULE_0__.default, null), document.getElementById("root"));  
 }),
           
It's still a valid a approach to wrap your entry point with import("./bootstrap"). When doing so, make sure to inline the entry chunk into the HTML for best performance (no double round trip).
This is now the recommended approach. The old "fix" no longer works as remotes could provide shared modules to the app, which requires an async step before using shared modules. Maybe we provide some flag for the entry option in future to do this automatically.

文章裡寫到在開啟 MF 中共享子產品時,入口采用異步邊界可以有效規避掉雙重更新造成的性能加載問題。官方文檔對此還提供了這種做法的缺陷案例【以下來自官網】:

1.通過 ModuleFederationPlugin  将依賴的 eager 屬性設定為 true:
new ModuleFederationPlugin({
    // ...other config
    shared: {
        eager: true,
    }
});  
// webpack beta.16 更新到 webpack beta.17 可能類似報錯 Uncaught Error: Module "./Button" does not exist in container.
           
2.更改 exposes:Uncaught TypeError: fn is not a function
new ModuleFederationPlugin({
  exposes: {
-   'Button': './src/Button'
+   './Button':'./src/Button'
  }
});
    
// 此處錯誤可能是丢失了遠端容器,請確定在使用前添加它。
// 如果已為試圖使用遠端伺服器的容器加載了容器,但仍然看到此錯誤,則需将主機容器的遠端容器檔案也添加到 HTML 中。
           

疑問二:包的版本選擇

在我們目前應用到的許多場景中,就對私有庫的自定義元件做過本地的二次封裝,由于代碼是單向更新的,在移植項目的過程中就存在許多難以規避的問題,Module Federation 通過設定

singleton: true 開啟公共子產品

可以一定程度解決這個問題。但是如果兩方項目所需的版本号不一緻是按照什麼依據呢?

// 前提情況 App1 是 host App2 是 remote App1 中引用 App2 的元件
// App1 package.json:
"dependencies": {
  "mf-test-ssy": "^1.0.0"
}
// App2 package.json:
"dependencies": {
  "mf-test-ssy": "^2.0.0"
}

// webpack-config-common 部分:
new ModuleFederationPlugin({
  // ...other config
  shared: { 
    react: { singleton: true }, 
    "react-dom": { singleton: true },
    "mf-test-ssy":{ singleton: true }, 
  },
}),
           

這裡小孫簡單寫了個 demo 嘗試模拟這個問題,以basic-host-remote 案例為基礎,自己釋出了兩個不同版本的 npm 包,分别引入 v1.0.0 和 v2.0.0 檢視一下結果。

Module Federation你的浪漫我來懂
可以看到 host 展示的 Npm 版本雖然低于 remote 中 Npm 的版本,但是展示的還是 remote 中較高的版本的代碼。

然後互換 App1 和 App2 的 npm 版本:

// App1 package.json:
"dependencies": {
  "mf-test-ssy": "^2.0.0"
}
// App2 package.json:
"dependencies": {
  "mf-test-ssy": "^1.0.0"
}
           
Module Federation你的浪漫我來懂
可以看到此時 App2 還是以低版本展示為主,App1 還是以本地的引用版本為主,開啟共享的差異性并不大。 共享中的子產品請求(from 官網中文站):
  • 隻在使用時提供
  • 會比對建構中所有使用的相等子產品請求
  • 将提供所有比對子產品
  • 将從圖中這個位置的 package.json 提取 requiredVersion
  • 當你有嵌套的 node_modules 時,可以提供和使用多個不同的版本

如何解決? => 自動推斷的設定

packageName 選項允許通過設定包名來查找所需的版本。預設情況下,它會自動推斷子產品請求,當想禁用自動推斷時,請将 requiredVersion 設定為 false。

疑問三:共享子產品是什麼程度的共享

借此猜測某些庫是不是也隻會一次執行個體化,實驗繼續 UP!! Npm 中的構造函數邏輯更改如下:初始化成功的例子在 window 下挂載上資料,并且每次初始化後列印值遞增。兩份代碼都更新成 V5.0.0,我們看一下效果:

Module Federation你的浪漫我來懂

看似沒有問題對不對,小孫複檢的時候猛然驚醒,這是兩個項目,window 各自為政,這個例子這樣設計本身就是大錯特錯。但是沒關系,雖然小孫不靠譜,但是 webpack 靠譜呀。

Module Federation你的浪漫我來懂
  • main.js 是應用主檔案每次都先加載這個。
  • remoteEntry.js 在 App1 中先加載,是因為 App1 中依賴于 App2 的一些依賴配置,是以 App1 中的 remoteEntry.js 加載優先級非常高,加載以後它可以知道自己需要遠端加載什麼資源。
  • 可以看到 App2 加載了 mf-test-ssy, App1 并沒有加載 mf-test-ssy,但是直接加載了 App2 的 remoteEntry,說明 remoteEntry.js 是作為 remote 時被引的檔案。
  • 構造函數應該實質上隻是初始化了一次,我們從這個結論出發,看一下 webpack 相關的代碼配置,再逐漸細化到源碼。

從加載檔案到源碼

這裡先貼會影響打包的業務代碼:

/*
** App1 app.js
*/
import React from "react";
import Test from "mf-test-ssy"
// 這裡用了 App2 的 button 元件代碼
const RemoteButton = React.lazy(() => import("app2/Button"));
const App = () => (
  <div>
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);
export default App;
           
/*
** App2 Html-Script 注意這裡是編譯後動态生成的。
*/
<script defer src="remoteEntry.js"></script>
           

把 remoteEntry 打包後的代碼,把 相關部分截取出來:

/* webpack/runtime/sharing */
//前面暫且忽略一些定義以及判空
//存放 scope
__webpack_require__.S = {};
var initPromises = {};
var initTokens = {};
//初始化 scope,最後把資料拼成一個大對象
__webpack_require__.I = (name, initScope) => {
  if(!initScope) initScope = [];
  // handling circular init calls
  var initToken = initTokens[name];
  if(!initToken) initToken = initTokens[name] = {};
  if(initScope.indexOf(initToken) >= 0) return;
  initScope.push(initToken);
  // only runs once
  if(initPromises[name]) return initPromises[name];
  // creates a new share scope if needed
  if(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};
  // runs all init snippets from all modules reachable
  var scope = __webpack_require__.S[name];
  var warn = (msg) => (typeof console !== "undefined" && console.warn && console.warn(msg));
  var uniqueName = "@basic-host-remote/app2";
  //注冊共享子產品
  var register = (name, version, factory, eager) => {
    var versions = scope[name] = scope[name] || {};
    var activeVersion = versions[version];
    if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };
  };
  //初始化遠端外部子產品
  var initExternal = (id) => {
    var handleError = (err) => (warn("Initialization of sharing external failed: " + err));
    try {
      var module = __webpack_require__(id);
      if(!module) return;
      var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
      if(module.then) return promises.push(module.then(initFn, handleError));
      var initResult = initFn(module);
      if(initResult && initResult.then) return promises.push(initResult.catch(handleError));
    } catch(err) { handleError(err); }
  }
  var promises = [];
  //根據 chunkId 的名稱注冊共享子產品
  switch(name) {
    case "default": {
      register("mf-test-ssy", "6.0.0", () => (__webpack_require__.e("node_modules_mf-test-ssy_index_js").then(() => (() => (__webpack_require__(/*! ./node_modules/mf-test-ssy/index.js */ "./node_modules/mf-test-ssy/index.js"))))));
      register("react-dom", "16.14.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_react-dom_index_js"), __webpack_require__.e("webpack_sharing_consume_default_react_react-_76b1")]).then(() => (() => (__webpack_require__(/*! ./node_modules/react-dom/index.js */ "./node_modules/react-dom/index.js"))))));
      register("react", "16.14.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_react_index_js"), __webpack_require__.e("node_modules_object-assign_index_js-node_modules_prop-types_checkPropTypes_js")]).then(() => (() => (__webpack_require__(/*! ./node_modules/react/index.js */ "./node_modules/react/index.js"))))));
    }
      break;
  }
  if(!promises.length) return initPromises[name] = 1;
  return initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));
};
})();
           

這段代碼所做的就是根據配置項将子產品生成内部對應的 modules,定義了一個 scope 去存儲所有的 module,然後注冊了共享子產品等操作。全部挂載在__webpack_require__上,這樣處理以友善後續 require 的方式引入進來。對應最最最核心的源碼:

// 四大天王鎮宅 
sharing: {
   // 處理分子原子關系的依賴
  get ConsumeSharedPlugin() {
   return require("./sharing/ConsumeSharedPlugin");
  },
    // 處理 provide 依賴
  get ProvideSharedPlugin() {
   return require("./sharing/ProvideSharedPlugin");
  },
    // 我是入口 讓我來調用 并且我實作了共享
  get SharePlugin() {
   return require("./sharing/SharePlugin");
  },
  get scope() {
   return require("./container/options").scope;
  }
},
           
// from /webpack-master/lib/sharing/SharePlugin.js
class SharePlugin {
 /**
  * @param {SharePluginOptions} options options
  */
 constructor(options) {
  /** @type {[string, SharedConfig][]} */
    // 處理 options 格式 子產品二次封裝 
  const sharedOptions = parseOptions(...太長不看);
  /** @type {Record<string, ConsumesConfig>[]} */
    // 定義 Host 消費 remote 的資訊 後面會根據這個關聯去加載前面說的原子的初始化以及 scoped
  const consumes = sharedOptions.map(([key, options]) => ({
   [key]: {
    import: options.import,
    shareKey: options.shareKey || key,
    shareScope: options.shareScope,
    requiredVersion: options.requiredVersion,
    strictVersion: options.strictVersion,
    singleton: options.singleton,
    packageName: options.packageName,
    eager: options.eager
   }
  }));
  /** @type {Record<string, ProvidesConfig>[]} */
    // 核心代碼 處理
  const provides = sharedOptions
   .filter(([, options]) => options.import !== false)
   .map(([key, options]) => ({
    [options.import || key]: {
     shareKey: options.shareKey || key,
     shareScope: options.shareScope,
     version: options.version,
     eager: options.eager
    }
   }));
  this._shareScope = options.shareScope;
  this._consumes = consumes;
  this._provides = provides;
 }

 /**
  * Apply the plugin
  * @param {Compiler} compiler the compiler instance
  * @returns {void}
  */
 apply(compiler) {
    // 處理分子原子關系的依賴
  new ConsumeSharedPlugin({
   shareScope: this._shareScope,
   consumes: this._consumes
  }).apply(compiler);
    // 處理 provider 依賴
  new ProvideSharedPlugin({
   shareScope: this._shareScope,
   provides: this._provides
  }).apply(compiler);
 }
}
module.exports = SharePlugin;
           

總結

Module Federation你的浪漫我來懂

每一個分子跟原子的愛恨糾葛終有一個檔案去劃分好主次,雖然異步加載分離打包,但是愛永不失聯。 每一個公共分享的時刻,runtime 在各自心中,就像共同孕育同一個孩子,生了一次不會生第兩次。 但是—— 共享子產品中 remote 版本大,按照較大的算,如果 remote 版本小,按照我本地說了算。

其他:MF 生态

ExternalTemplateRemotesPlugin

有需求在建構中使用上下文處理處理動态 Url 的,且需要解決緩存失效問題的,可以看一下這個插件。

from https://github.com/module-federation/module-federation-examples/issues/566
  • Dynamic URL, have the ability to define the URL at runtime instead of hard code at build time.
  • Cache invalidation.
// from webpack.config
plugins: [
    new ModuleFederationPlugin({
        //...config
        remotes: {
          'my-remote-1': 'my-remote-1@[window.remote-1-domain]/remoteEntry.js?[getRandomString()]',
        },
    }),
    new ExternalTemplateRemotesPlugin(), //no parameter,
]
           

參考資料

  • https://webpack.js.org/concepts/module-federation/#building-blocks
  • https://github.com/sokra/slides/blob/master/content/ModuleFederationWebpack5.md
  • https://www.youtube.com/watch?v=x22F4hSdZJM
  • https://github.com/module-federation/module-federation-examples
  • https://segmentfault.com/a/1190000039031505
  • http://img.iamparadox.club/img/mf2.jpg
  • https://developer.aliyun.com/article/755252

繼續閱讀