原文:探索webpack5新特性Module federation在騰訊文檔的應用 | AlloyTeam
做者:TAT.jayjavascript
前言:html
webpack5的使人激動的新特性Module federation可能並不會讓不少開發者激動,可是對於深受多應用傷害的騰訊文檔來講,倒是着實讓人眼前一亮,這篇文章就帶你瞭解騰訊文檔的困境以及Module federation能夠如何幫助咱們走出這個困境。前端
騰訊文檔從功能層面上來講,用戶最熟悉的可能就是word、excel、ppt、表單這四個大品類,四個品類彼此獨立,可能由不一樣的團隊主要負責開發維護,那從開發者角度來講,四個品類四個倉庫各自獨立維護,好像事情就很簡單,可是現實狀況實際上卻複雜不少。咱們來看一個場景:java
對於複雜的權限場景,爲了讓使用者能快速能得到最新狀態,咱們實際上有一個通知中心的需求,在pc的樣式大體就是上圖裏面的樣子。這裏是在騰訊文檔的列表頁看到的入口,實際上在上面提到的四大品類裏面,都須要嵌入這樣的一個頁面。node
那麼問題來了,爲了最小化這裏的開發和維護成本,確定是各個品類公用一套代碼是最好的,那最容易想到的就是使用獨立npm包的方式來引入。確實,騰訊文檔的內部不少功能如今是使用npm包的方式來引入的,可是實際上這裏會遇到一些問題:react
騰訊文檔的歷史很複雜,簡而言之,在剛開始的時候,代碼裏面是不支持寫ES6的,因此沒辦法引入npm包。一時半會想改造完成是不現實的,產品需求也不會等你去完成這樣的改造jquery
這裏的問題實際上也是如今咱們使用npm包的問題,其實仍是咱們懶,想投機取巧。以npm包的方式引入的話,一旦有改動,你須要改5個倉庫(四個品類+列表頁)去升級這裏的版本,實際上發佈成本是蠻大的,對於開發者來講其實也很痛苦webpack
爲了能在不支持ES6代碼的環境下快速引入React來加速需求開發,咱們想出了一個所謂的Script-Loader(下面會簡稱SL)的模式。git
總體架構如圖:github
簡單來講就是,參考jquery的引入方式,咱們用另一個項目去實現這些功能,而後把代碼打包成ES5代碼,對外提供不少接口,而後在各個品類頁,引入咱們提供的加載腳本,內部會自動去加載文件,獲取每一個模塊的js文件的CDN地址而且加載。這樣作到各個模塊各自獨立,而且全部模塊和各個品類造成獨立。
在這種模式下,每次發佈,咱們只須要去發佈各個改動的模塊以及最新的配置文件,其餘品類就能得到自動更新。
這個模式並不必定適合全部項目,也不必定是最好的解決方案,從如今的角度來看,有點像微前端的概念,可是實際上卻也是有區別的,這裏就不展開了。這種模式目前確實能解決騰訊文檔這種多應用複用代碼的需求。
這種模式本質上目前沒有很嚴重的問題,可是有一個很痛點一直困擾咱們,那就是品類代碼和SL的代碼共享問題。舉個例子:
Excel品類改造後使用了React,SL的模塊A、模塊B、模塊C引入了React
由於SL的模塊之間是各自獨立的,因此React也是各自打包的,那就是說當你打開Excel的時候,若是你用了模塊A、B、C,那你最終頁面會加載四份React代碼,雖然不會帶上什麼問題,可是對於有追求的前端來講,咱們仍是想去解決這樣的問題。
對於React來講,咱們能夠默認品類是加載了React,因此咱們直接把SL裏面的React配置爲External,這樣就不會打包了,可是實際上狀況沒有這麼簡單:
就以上面的通知中心來講,在移動端上面就不是嵌入的了,並且獨立頁面,因此這個獨立頁面須要你手動引入React
簡單來講,就是SL依賴的包,在品類裏面可能並無使用,例如Mobx或者Redux
這裏的問題是說像React這種包咱們能夠經過配置External爲window.React來達到共用,可是不是全部包均可以這樣的,那對於不能配置爲全局環境的包來講,還無法解決這裏的代碼共享問題
基於這些問題,咱們目前的選擇是一種折中方案,咱們把能夠配置全局環境的包提取出來,每一個模塊指明依賴,而後在SL內部,加載模塊代碼以前會去檢測依賴,依賴加載完成纔會加載執行實際模塊代碼。
這種方式有很大問題,你須要手動去維護這樣的依賴,每一個共享包實際上你都是須要單獨打包成一個CDN文件,爲的是當依賴檢測失敗的時候,能夠有一個兜底加載文件。所以,實際上目前也只有React包作了這個共享。
那麼到這裏,核心問題就變成了品類代碼和SL如何作到代碼共享
。對於其餘項目來講,其實也就是多應用如何作到代碼共享
。
爲了解決上面的問題,咱們實際上想從webpack入手,去實現這樣的一個插件幫咱們解決這個問題。核心思路就是hook webpack的內部require函數,在這以前咱們先來看一下webpack打包後的一些原理,這個也是後面理解Module federation的核心。若是這裏你比較熟悉,也能夠快速跳過到第三節,可是不熟悉的同窗仍是建議認真瞭解一下。
webpack裏面有兩個很核心的概念,叫chunk和module,這裏爲了簡單,只看js相關的,用筆者本身的理解去解釋一下他們直接的區別:
module:每個源碼js文件其實均可以當作一個module
chunk:每個打包落地的js文件其實都是一個chunk,每一個chunk都包含不少module
默認的chunk數量其實是由你的入口文件的js數量決定的,可是若是你配置動態加載或者提取公共包的話,也會生成新的chunk。
有了基本理解後,咱們須要去理解webpack打包後的代碼在瀏覽器端是如何加載執行的。爲此咱們準備一個很是簡單的demo,來看一下它的生成文件。
src
---main.js
---moduleA.js
---moduleB.js
/** * moduleA.js */
export default function testA() {
console.log('this is A');
}
/** * main.js */
import testA from './moduleA';
testA();
import('./moduleB').then(module => {
});
複製代碼
很是簡單,入口js是main.js
,裏面就是直接引入moduleA.js
,而後動態引入 moduleB.js
,那麼最終生成的文件就是兩個chunk,分別是:
main.js
和moduleA.js
組成的bundle.js
組成的
0.bundle.js`若是你瞭解webpack底層原理的話,那你會知道這裏是用mainTemplate和chunkTemplate分別渲染出來的,不瞭解也不要緊,咱們繼續解讀生成的代碼
整個main.js
的代碼打包後是下面這樣的
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */
var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./moduleA */ "./src/moduleA.js");
Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__["default"])();
__webpack_require__.e( /*! import() */ 0).then(__webpack_require__.bind(null, /*! ./moduleB */ "./src/moduleB.js")).then(module => {
});
})
複製代碼
能夠看到,咱們的直接import moduleA最後會變成webpack_require,而這個函數是webpack打包後的一個核心函數,就是解決依賴引入的。
那咱們看一下webpack_require它是怎麼實現的:
function __webpack_require__(moduleId) {
// Check if module is in cache
// 先檢查模塊是否已經加載過了,若是加載過了直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
// 若是一個import的模塊是第一次加載,那以前必然沒有加載過,就會去執行加載過程
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
複製代碼
若是簡化一下它的實現,其實很簡單,就是每次require,先去緩存的installedModules這個緩存map裏面看是否加載過了,若是沒有加載過,那就從modules
這個全部模塊的map裏去加載。
那相信不少人都有疑問了,modules這麼個相當重要的map是從哪裏來的呢,咱們把bundle.js
生成的js再簡化一下:
(function (modules) {})({
"./src/main.js": (function (module, __webpack_exports__, __webpack_require__) {}),
"./src/moduleA.js": (function (module, __webpack_exports__, __webpack_require__) {})
});
複製代碼
因此能夠看到,這實際上是個當即執行函數,modules
就是函數的入參,具體值就是咱們包含的全部module,到此,一個chunk是如何加載的,以及chunk如何包含module,相信你們必定會有本身的理解了。
上面的chunk就是一個js文件,因此維護了本身的局部modules
,而後本身使用沒啥問題,可是動態引入咱們知道是會生成一個新的js文件的,那這個新的js文件0.bundle.js
裏面是否是也有本身的modules
呢?那bundle.js
如何知道0.bundle.js
裏面的modules
呢?
先看動態import的代碼變成了什麼樣:
__webpack_require__.e( /*! import() */ 0).then(__webpack_require__.bind(null, /*! ./moduleB */ "./src/moduleB.js")).then(module => {
});
複製代碼
從代碼看,實際上就是外面套了一層webpck_require.e,而後這是一個promise,在then裏面再去執行webpack_require。
實際上webpck_require.e就是去加載chunk的js文件0.bundle.js
,具體代碼就不貼了,沒啥特別的。
等到加載回來後它認爲**bundle.js
裏面的modules
就必定會有了0.bundle.js
包含的那些modules
**,這是如何作到的呢?
咱們看0.bundle.js
究竟是什麼內容,讓它擁有這樣的魔力:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push(
[
[0],
{
"./src/moduleB.js": (function (module, __webpack_exports__, __webpack_require__) {})
}
]
);
複製代碼
拿簡化後的代碼一看,你們第一眼想到的是jsonp,可是很遺憾的是它不是一個函數,卻只是向一個全局數組裏面push了本身的模塊id以及對應的modules
。那看起來魔法的核心應該是在bundle.js
裏面了,事實的確也是如此。
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
複製代碼
在bundle.js
的裏面,咱們看到這麼一段代碼,其實就是說咱們劫持了push函數,那0.bundle.js
一旦加載完成,咱們豈不是就會執行這裏,那不就能拿到全部的參數,而後把0.bundle.js
裏面的全部module加到本身的modules
裏面去!
若是你沒有很理解,能夠配合下面的圖片,再把上面的代碼讀幾遍。
其實簡單來講就是,對於mainChunk文件,咱們維護一個modules
這樣的全部模塊map,而且提供相似webpack_require這樣的函數。對於chunkA文件(多是由於提取公共代碼生成的、或者是動態加載)咱們就用相似jsonp的方式,讓它把本身的全部modules
添加到主chunk的modules
裏面去。
基於這樣的一個理解,咱們就在思考,那騰訊文檔的多應用代碼共享能不能解決呢?
具體到騰訊文檔的實際場景,就是以下圖:
由於是獨立的項目,因此webpack打包也是有兩個mainChunk,而後有各自的chunk(其實這裏會有chunk覆蓋或者chunk裏面的module覆蓋問題,因此id要採用md5)。
那問題的核心就是如何打通兩個mainChunk的modules
?
若是是自由編程,我想你們的實現方式可就太多了,可是在webpack的框架限制下面,如何快速的實現這個,咱們也一直在思考方案,目前想到的方案以下:
SL模塊內部的webpack_require被咱們hack,每次在
modules
裏面找不到的時候,咱們去Excel的modules
裏面去找,這樣須要把Excel的modules
做爲全局變量
可是對於Excel不存在的模塊咱們須要怎麼處理?
這種很明顯就是運行時環境,咱們須要作好加載時的失敗降級處理,可是這樣就會遇到同步轉異步的問題,原本你是同步引入一個模塊的,可是若是它在Excel的modules不存在的時候,你就須要先一步加載這個module對應的chunk,變成了相似動態加載,可是你的代碼仍是同步的,這樣就會有問題。
因此咱們須要將依賴前置,也就是說在加載SL模塊後,它知道本身依賴哪些共享模塊,而後去檢測是否存在,不存在則依次去加載,全部依賴就位後纔開始執行本身。
說實話,webpack底層仍是很複雜的,在不熟悉的狀況下並且定製程度也不能肯定,因此咱們也是遲遲沒有去真正作這個事情。可是偶然的機會了解到了webpack5的Module federation,經過看描述,感受和咱們想要的東西很像,因而咱們開始一探究竟!
關於Module federation是什麼,有什麼做用,如今已經有一些文章去說明,這裏貼一篇,你們能夠先去了解一下
簡單來講就是容許運行時動態決定代碼的引入和加載。
咱們最關心的仍是Module federation的的實現方式,才能決定它是否是真的適合騰訊文檔。
這裏咱們用已有的demo:
module-federation-examples/basic-host-remote
在此以前,仍是須要向你們介紹一下這個demo作的事情
app1
---index.js 入口文件
---bootstrap.js 啓動文件
---App.js react組件
app2
---index.js 入口文件
---bootstrap.js 啓動文件
---App.js react組件
---Button.js react組件
複製代碼
這是文件結構,其實你能夠當作是兩個獨立應用app1和app2,那他們以前有什麼愛恨情仇呢?
/** app1 **/
/** * index.js **/
import('./bootstrap');
/** * bootstrap.js **/
import('./bootstrap');
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
/**
* App.js
**/
import('./bootstrap');
import React from "react";
import RemoteButton from 'app2/Button';
const App = () => (
<div>
<h1>Basic Host-Remote</h1>
<h2>App 1</h2>
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);
export default App;
複製代碼
我這裏只貼了app1的js代碼,app2的代碼你不須要關心。代碼沒有什麼特殊的,只有一點,app1的App.js裏面:
import RemoteButton from 'app2/Button';
複製代碼
也就是關鍵來了,跨應用複用代碼來了!app1的代碼用了app2的代碼,可是這個代碼最終長什麼樣?是如何引入app2的代碼的?
先看咱們的webpack須要如何配置:
/** * app1/webpack.js */
{
plugins: [
new ModuleFederationPlugin({
name: "app1",
library: {
type: "var",
name: "app1"
},
remotes: {
app2: "app2"
},
shared: ["react", "react-dom"]
})
]
}
複製代碼
這個其實就是Module federation的配置了,大概能看到想表達的意思:
remotes和shared仍是有一點區別的,咱們先來看效果。
生成的html文件:
<html>
<head>
<script src="app2/remoteEntry.js"></script>
</head>
<body>
<div id="root"></div>
<script src="app1/app1.js"></script><script src="app1/main.js"></script></body>
</html>
複製代碼
ps:這裏的js路徑有修改,這個是能夠配置的,這裏只是代表從哪裏加載了哪些js文件
app1打包生成的文件:
app1/index.html
app1/app1.js
app1/main.js
app1/react.js
app1/react-dom.js
app1/src_bootstrap.js
複製代碼
ps: app2你也須要打包,只是我沒有貼app2的代碼以及配置文件,後面須要的時候會再貼出來的
最終頁面表現以及加載的js:
從上往下加載的js時序實際上是頗有講究的,後面將會是解密的關鍵:
app2/remoteEntry.js
app1/app1.js
app1/main.js
app1/react.js
app1/react-dom.js
app2/src_button_js.js
app1/src_bootstrap.js
複製代碼
這裏最須要關注的其實仍是每一個文件從哪裏加載,在不去分析原理以前,看文件加載咱們至少有這些結論:
在講解原理以前,我仍是放出以前的一張圖,由於這是webpack的文件模塊核心,即便升級5,也沒有發生變化
app1和app2仍是有本身的modules
,因此實現的關鍵就是兩個modules
如何同步,或者說如何注入,那咱們就來看看Module federation如何實現的。
// import源碼
import RemoteButton from 'app2/Button';
// import打包代碼 在app1/src_bootstrap.js裏面
/* harmony import */
var app2_Button__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__( /*! app2/Button */ "?ad8d");
/* harmony import */
var app2_Button__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/ __webpack_require__.n(app2_Button__WEBPACK_IMPORTED_MODULE_1__);
複製代碼
從這裏來看,咱們好像看不出什麼,由於仍是正常的webpack_require,難道說它真的像咱們以前所設想的那樣,重寫了webpack_require嗎?
遺憾的是,從源碼看這個函數是沒有什麼變化的,因此核心點不是這裏。
可是你注意看加載的js順序:
app2/remoteEntry.js
app1/app1.js
app1/main.js
app1/react.js
app1/react-dom.js
app2/src_button_js.js // app2的button居然先加載了,比咱們的本身啓動文件還前面
app1/src_bootstrap.js
複製代碼
回想上一節咱們本身的分析
因此咱們須要將依賴前置,也就是說在加載SL模塊後,它知道本身依賴哪些共享模塊,而後去檢測是否存在,不存在依次去加載,因此依賴就位後纔開始執行本身。
因此它是否是經過依賴前置來解決的呢?
由於html裏面和app1相關的只有兩個文件:app1/app1.js以及app1/main.js
那咱們看看main.js到底寫了啥
(() => { // webpackBootstrap
var __webpack_modules__ = ({})
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
__webpack_require__.m = __webpack_modules__;
__webpack_require__("./src/index.js");
})()
複製代碼
能夠看到區別不大,只是把以前的modules
換成了webpack_modules
,而後把這個modules
的初始化由參數改爲了內部聲明變量。
那咱們來看看webpack_modules內部的實現:
var __webpack_modules__ = ({
"./src/index.js": ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
__webpack_require__.e( /*! import() */ "src_bootstrap_js").then(__webpack_require__.bind(__webpack_require__, /*! ./bootstrap */ "./src/bootstrap.js"));
}),
"container-reference/app2": ((module) => {
"use strict";
module.exports = app2;
}),
"?8bfd": ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
var external = __webpack_require__("container-reference/app2");
module.exports = external;
})
});
複製代碼
從代碼看起來就三個module:
./src/index.js 這個看起來就是咱們的app1/index.js,裏面去動態加載bootstrap.js對應的chunk src_bootstrap_js
container-reference/app2 直接返回一個全局的app2,這裏感受和咱們的app2有關係
?8bfd 這個字符串是咱們上面提到的app2/button對應的文件引用id
複製代碼
那在加載src_bootstrap.js以前加載的那些react文件還有app2/button文件都是誰作的呢?經過debug,咱們發現祕密就在webpack_require__.e("src_bootstrap_js")這句話
在第二節解析webpack加載的時候,咱們得知了:
實際上webpck_require.e就是去加載chunk的js文件
0.bundle.js
,等到加載回來後它認爲bundle.js
裏面的modules
就必定會有了0.bundle.js
包含的那些modules
也就是說原來的webpack_require__.e平淡無奇,就是加載一個script,以至於咱們都不想去貼出它的代碼,可是此次升級後一切變的不同了,它成了關鍵中的關鍵!
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
複製代碼
看代碼,的確發生了變化,如今底層是去調用webpack_require.f上面的函數了,等到全部函數都執行完了,才執行promise的then
那問題的核心又變成了webpack_require.f上面有哪些函數了,最後發現有三個函數:
一:overridables
/* webpack/runtime/overridables */
__webpack_require__.O = {};
var chunkMapping = {
"src_bootstrap_js": [
"?a75e",
"?6365"
]
};
var idToNameMapping = {
"?a75e": "react-dom",
"?6365": "react"
};
var fallbackMapping = {
"?a75e": () => {
return __webpack_require__.e("vendors-node_modules_react-dom_index_js").then(() => () => __webpack_require__("./node_modules/react-dom/index.js"))
},
"?6365": () => {
return __webpack_require__.e("vendors-node_modules_react_index_js").then(() => () => __webpack_require__("./node_modules/react/index.js"))
}
};
__webpack_require__.f.overridables = (chunkId, promises) => {}
複製代碼
二:remotes
/* webpack/runtime/remotes loading */
var chunkMapping = {
"src_bootstrap_js": [
"?ad8d"
]
};
var idToExternalAndNameMapping = {
"?ad8d": [
"?8bfd",
"Button"
]
};
__webpack_require__.f.remotes = (chunkId, promises) => {}
複製代碼
三:jsonp
/* webpack/runtime/jsonp chunk loading */
var installedChunks = {
"main": 0
};
__webpack_require__.f.j = (chunkId, promises) => {}
複製代碼
這三個函數我把核心部分節選出來了,其實註釋也寫得比較清楚了,我仍是解釋一下:
知道了核心在webpack_require.e以及內部實現後,不知道你腦子裏是否是對整個加載流程有了必定的思路,若是沒有,容我來給你解析一下
到此就一切都正常啓動了,其實就是咱們以前提到的依賴前置,先去分析,而後生成配置文件,再去加載。
看起來一切都很美好,但其實仍是有一個關鍵信息沒有解決!
上面的第4步加載react的時候,由於咱們本身實際上也打包了react文件,因此當沒有加載的時候,咱們能夠去加載一份,也知道地址
可是第五步的時候,當頁面歷來沒有加載過app2/Button的時候,咱們去什麼地址加載什麼文件呢?
這個時候就要用到前面咱們提到的main.js裏面的webpack_modules
了
var __webpack_modules__ = ({
"container-reference/app2":
((module) => {
"use strict";
module.exports = app2;
}),
"?8bfd":
((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
var external = __webpack_require__("container-reference/app2");
module.exports = external;
})
});
複製代碼
這裏面有三個module,咱們還有 ?8bfd、container-reference/app2 沒有用到,咱們再看一下remotes的實現
/* webpack/runtime/remotes loading */
var chunkMapping = {
"src_bootstrap_js": [
"?ad8d"
]
};
var idToExternalAndNameMapping = {
"?ad8d": [
"?8bfd",
"Button"
]
};
__webpack_require__.f.remotes = (chunkId, promises) => {
if (__webpack_require__.o(chunkMapping, chunkId)) {
chunkMapping[chunkId].forEach((id) => {
if (__webpack_modules__[id]) return;
var data = idToExternalAndNameMapping[id];
promises.push(Promise.resolve(__webpack_require__(data[0]).get(data[1])).then((factory) => {
__webpack_modules__[id] = (module) => {
module.exports = factory();
}
}))
});
}
}
複製代碼
當咱們加載src_bootstrap_js這個chunk時,通過remotes,發現這個chunk依賴了?ad8d,那在運行時的時候:
id = "?8bfd"
data = [
"?8bfd",
"Button"
]
// 源碼
__webpack_require__(data[0]).get(data[1])
// 運行時
__webpack_require__('?8bfd').get("Button")
複製代碼
結合main.js的module ?8bfd的代碼,那最終就是app2.get("Button")
這不就是個全局變量嗎?看起來有些蹊蹺啊!
咱們好像一直忽略了這個文件,它是第一個加載的,必然有它的做用,帶着對全局app2有什麼蹊蹺的疑問,咱們去看了這個文件,果真發現了玄機!
var app2;
app2 =
(() => {
"use strict";
var __webpack_modules__ = ({
"?8619": ((__unused_webpack_module, exports, __webpack_require__) => {
var moduleMap = {
"Button": () => {
return __webpack_require__.e("src_Button_js").then(() => () => __webpack_require__( /*! ./src/Button */ "./src/Button.js"));
}
};
var get = (module) => {
return (
__webpack_require__.o(moduleMap, module) ?
moduleMap[module]() :
Promise.resolve().then(() => {
throw new Error("Module " + module + " does not exist in container.");
})
);
};
var override = (override) => {
Object.assign(__webpack_require__.O, override);
}
__webpack_require__.d(exports, {
get: () => get,
override: () => override
});
})
});
return __webpack_require__("?8619");
})()
複製代碼
若是你細心看,就會發現,這個文件定義了全局的app2變量,而後提供了一個get函數,裏面實際上就是去加載具體的模塊
因此app2.get("Button")在這裏就變成了app2內部定義的get函數,隨後執行本身的webpack_require
是否是有種煥然大悟的感受!
原來它是這樣在兩個獨立打包的應用之間,經過全局變量去創建了一座彩虹橋!
固然,app2/remoteEntry.js是由app2根據配置打包出來的,裏面實際上就是根據配置文件的導出模塊,生成對應的內部modules
細心的讀者若是注意的話,會發現,在入口文件index.js和真正的文件app.js之間多了一個bootstrap.js,並且裏面內容就是異步加載app.js
那這個文件是否是多餘的,筆者試了一下,直接把入口換成app.js或者這裏換成同步加載,整個應用就跑不起來了
其實從原理上分析後也是能夠理解的:
由於依賴須要前置,而且等依賴加載完成後才能執行本身的入口文件,若是不把入口變成一個異步的chunk,那如何去實現這樣的依賴前置呢?畢竟實現依賴前置加載的核心是webpack_require.e
至此,Module federation如何實現shared和remotes兩個配置我相信你們都有了理解了,其實仍是逃不過在第二節末尾說的問題:
如何解決依賴問題,這裏的實現方式是重寫了加載chunk的webpack_require.e,從而前置加載依賴
如何解決modules
的共享問題,這裏是使用全局變量來hook
總體看起來實現仍是挺巧妙的,不是webpack核心開發者,估計不能想到這樣解決,實際上改動也是蠻大的。
這種實現方式的優缺點其實也明顯:
優勢:作到代碼的運行時加載,並且shared代碼無需本身手動打包
缺點:對於其餘應用的依賴,其實是強依賴的,也就是說app2有沒有按照接口實現,你是不知道的
至於網上一些其餘文章所說的app2的包必須在代碼裏面異步使用,這個你看前面的demo以及知道原理後也知道,根本沒有這樣的限制!
對於騰訊文檔來講,實際上更須要的是目前的shared能力,對一些常見的公共依賴庫配置shared後就能夠解決了,可是也只是理想上的,實際上仍是會遇到一些可見的問題,例如:
可是至少帶來了解決這個問題的但願,remotes配置也讓咱們看到了多應用共享代碼的可能,因此仍是會讓人眼前一亮,期待webpack5的正式發佈!
最後,若是有寫的不正確的地方,歡迎斧正~
AlloyTeam 歡迎優秀的小夥伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)