原文: 探索webpack5新特性Module federation在騰訊文檔的應用 | AlloyTeam
做者:TAT.jay
前言:javascript
webpack5的使人激動的新特性Module federation可能並不會讓不少開發者激動,可是對於深受多應用傷害的騰訊文檔來講,倒是着實讓人眼前一亮,這篇文章就帶你瞭解騰訊文檔的困境以及Module federation能夠如何幫助咱們走出這個困境。
騰訊文檔從功能層面上來講,用戶最熟悉的可能就是word、excel、ppt、表單這四個大品類,四個品類彼此獨立,可能由不一樣的團隊主要負責開發維護,那從開發者角度來講,四個品類四個倉庫各自獨立維護,好像事情就很簡單,可是現實狀況實際上卻複雜不少。咱們來看一個場景:html
對於複雜的權限場景,爲了讓使用者能快速能得到最新狀態,咱們實際上有一個通知中心的需求,在pc的樣式大體就是上圖裏面的樣子。這裏是在騰訊文檔的列表頁看到的入口,實際上在上面提到的四大品類裏面,都須要嵌入這樣的一個頁面。前端
那麼問題來了,爲了最小化這裏的開發和維護成本,確定是各個品類公用一套代碼是最好的,那最容易想到的就是使用獨立npm包的方式來引入。確實,騰訊文檔的內部不少功能如今是使用npm包的方式來引入的,可是實際上這裏會遇到一些問題:java
騰訊文檔的歷史很複雜,簡而言之,在剛開始的時候,代碼裏面是不支持寫ES6的,因此沒辦法引入npm包。一時半會想改造完成是不現實的,產品需求也不會等你去完成這樣的改造node
這裏的問題實際上也是如今咱們使用npm包的問題,其實仍是咱們懶,想投機取巧。以npm包的方式引入的話,一旦有改動,你須要改5個倉庫(四個品類+列表頁)去升級這裏的版本,實際上發佈成本是蠻大的,對於開發者來講其實也很痛苦react
爲了能在不支持ES6代碼的環境下快速引入React來加速需求開發,咱們想出了一個所謂的Script-Loader(下面會簡稱SL)的模式。jquery
總體架構如圖:webpack
簡單來講就是,參考jquery的引入方式,咱們用另一個項目去實現這些功能,而後把代碼打包成ES5代碼,對外提供不少接口,而後在各個品類頁,引入咱們提供的加載腳本,內部會自動去加載文件,獲取每一個模塊的js文件的CDN地址而且加載。這樣作到各個模塊各自獨立,而且全部模塊和各個品類造成獨立。git
在這種模式下,每次發佈,咱們只須要去發佈各個改動的模塊以及最新的配置文件,其餘品類就能得到自動更新。github
這個模式並不必定適合全部項目,也不必定是最好的解決方案,從如今的角度來看,有點像微前端的概念,可是實際上卻也是有區別的,這裏就不展開了。這種模式目前確實能解決騰訊文檔這種多應用複用代碼的需求。
這種模式本質上目前沒有很嚴重的問題,可是有一個很痛點一直困擾咱們,那就是品類代碼和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文件其實均可以當作一個modulechunk:每個打包落地的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
`moduleB.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兩個配置我相信你們都有了理解了,其實仍是逃不過在第二節末尾說的問題:
modules
的共享問題,這裏是使用全局變量來hook總體看起來實現仍是挺巧妙的,不是webpack核心開發者,估計不能想到這樣解決,實際上改動也是蠻大的。
這種實現方式的優缺點其實也明顯:
優勢:作到代碼的運行時加載,並且shared代碼無需本身手動打包
缺點:對於其餘應用的依賴,其實是強依賴的,也就是說app2有沒有按照接口實現,你是不知道的
至於網上一些其餘文章所說的app2的包必須在代碼裏面異步使用,這個你看前面的demo以及知道原理後也知道,根本沒有這樣的限制!
對於騰訊文檔來講,實際上更須要的是目前的shared能力,對一些常見的公共依賴庫配置shared後就能夠解決了,可是也只是理想上的,實際上仍是會遇到一些可見的問題,例如:
可是至少帶來了解決這個問題的但願,remotes配置也讓咱們看到了多應用共享代碼的可能,因此仍是會讓人眼前一亮,期待webpack5的正式發佈!
最後,若是有寫的不正確的地方,歡迎斧正~
AlloyTeam 歡迎優秀的小夥伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)