先拋出一個尖銳問題:MPA 多頁應用或微前端架構,如何處理頁面的公共部分。javascript
之因此說它尖銳,是由於不止咱們公司,包括騰訊在內不少國內外一線技術團隊都碰到了。css
好比這篇博客:騰訊文檔的困境html
咱們拿MPA應用舉例,好比菜單部分,傳統後端模板渲染時通常會經過前端
// .NET
@Html.Partial("Header")
複製代碼
或java
// Java
<%@ include file="header.jsp" %>
複製代碼
引入公共模板,這樣在訪問頁面時會直接渲染公共部分。
node
但若是是現代化工程(好比 React),可先後端又未分離的MPA項目(頁面仍由後端渲染,Dom 渲染交由 React 接管),咱們就會將構建後的資源文件拷貝到後端工程裏,經過在頁面引入 script 與 style 進行渲染。react
此時問題就暴露出來了,對於頁頭這種公共部分,是 React 渲染的組件。
webpack
常規作法是將公共部分做爲組件直接構建進每一個頁面級項目,嗯,騰訊文檔也是這麼作的。
ios
這樣作會帶來以下缺點:web
好比
Header
部分單獨構建體積爲400KB
,那麼每一個頁面級構建結果都會在現有體積上增大400KB
(忽略公共庫依賴,假設統一使用DllReferencePlugin
處理)。沒有絲毫誇張的成份,咱們Header
裏有不少功能,加上chunks
以後確實有將近500KB
。
尤爲是對於
Header
這種每一個頁面都會使用的公共部分而言,只要作一丁點修改,全部頁面級應用都必須從新構建發版。
好比下圖中騰訊文檔的通知中心:
下圖中 SMS Client 端的菜單與Feedback等一系列公共組件(MPA項目)
在 webpack5 發佈以前,這彷佛是一個不可能實現的事情!
騰訊文檔的前端們也研究過這個問題,可是從文中描述的研究過程來看,主要是針對打包後的 __webpack_require__
方法中埋入勾子作文章,而且一直沒有提出有效的解決方案。
說實話,webpack 底層仍是很複雜的,在不熟悉的狀況下並且定製程度也不能肯定,因此咱們也是遲遲沒有去真正作這個事情。
—— 摘自《騰訊文檔的困境》
可是,咱們通過一系列的探索,在2019年7月利用 webpack4 現有特性完美解決了這個問題!巧合的是,Wepback 團隊在最新的 V5 版本中也新增了 Module-Federation
這個 Feature,用於此場景。
下面開始正式上乾貨!
騰訊文檔的小夥伴之因此不敢對 __webpack_require__
動手無非就是由於它太複雜了,怕改動以後引起其它問題。
其實一開始他們的方向就錯了,正所謂打蛇打七寸,若是沒打中七寸就會引起一系列問題,或者遲遲不敢打。
因此,咱們將目光移到」七寸「 外部擴展(externals) 屬性上來看一下(默認各位都已經知道它的做用了)。
正由於它是 webpack 內部(npm + 構建)與外部引用的橋樑,因此我認爲在這裏動刀子是最恰當不過的!
externals
與 umd
回憶一下,咱們使用 externals
配置 CDN 第三方庫,好比 React
,配置以下:
externals: {
'react-dom': 'ReactDOM',
'react': 'React'
}
複製代碼
而後咱們再看下 React
的CDN引用連接,通常咱們使用的是 umd
構建版本,它會兼容 commonjs
、commonjs2
、amd
、window
等方案,在咱們的瀏覽器環境中,它會綁定一個 React
變量到 window
上:
externals
的做用在於:當 webpack 進行構建時,碰到 import React from 'react'
與 import ReactDOM from 'react-dom'
導入語句時會避開 node_modules
而去 externals
配置的映射上去找,而這個映射值( ReactDOM
與 React
)正是在 window
變量上找到的。
下面兩張圖能夠證實這一點:
爲何我要花這麼多篇幅去鋪墊這個 externals
呢?由於這就是橋樑,鏈接外部模塊的橋樑!
讓咱們大膽的作一個設想:最理想的狀況,個人公共部分就一個 Header 組件!假如將它獨立構建成一個 umd
包,以 externals
的形式配置,經過 import Header from 'header';
導入,而後做爲組件使用,怎麼樣?
我作過試驗了,沒有任何問題!!!
可是,最理想的狀況並不存在,機率低到跟中福利彩票差很少。
咱們多數狀況是這樣的:
import { PredefinedRole, PredefinedClient } from '@core/common/public/enums/base';
import { isReadOnlyUser } from '@core/common/public/moon/role';
import { setWebICON } from '@core/common/public/moon/base';
import ErrorBoundary from '@core/common/public/wrapper/errorBoundary';
import OutClick from '@core/common/public/utils/outClick';
import { combine } from '@core/common/entry/fetoolkit';
import { getExtremePoint } from '@core/common/public/utils/map';
import { cookie } from '@core/common/public/utils/storage';
import Header from '@common/containers/header/header';
import { ICommonStoreContainer } from '@common/interface/store';
import { cutTextForSelect } from '@common/public/moon/format';
import { withAuthority } from '@common/hoc';
......
複製代碼
諸如此類的引用方式遍及幾十個項目之中,尤爲是別名(alias
)的使用,更是讓引用狀況多達幾十種!
PS:咱們是
monorepo
架構,@core/common
是公共依賴項目,工具方法、枚舉、axios實例、公共組件、菜單等都在這裏面維護,因此咱們才千方百計將這個項目獨立構建。
而 externals
上面的配置方式只支持轉換下面這種狀況,它只是徹底匹配了模塊名:
import React from 'react'; => 'react': 'React' => e.exports = React;
import ReactDom from 'react-dom'; => 'react-dom': 'ReactDOM' => e.exports = ReactDOM;
複製代碼
第三方庫名稱後面是不能跟 /
路徑的!好比下面這種就不支持:
import xxx from 'react/xxx';
複製代碼
我當時認爲 webpack 開發人員不太可能在 api 上這麼死板,確定有隱藏入口才對。果不其然!細讀了下官方文檔,讓我找到了一絲端倪:它還支持函數!
函數的功能在於:能夠自由控制任何 import
語句!
咱們能夠試着在這個函數裏打印一下入參 request
的值,結果以下圖所示:
全部的 import
引用都打印出來了!因此,咱們能夠隨意操縱 @common
與 @core/common
相關的引用!好比:
function(context, request, callback) {
if (/^@common\/?.*$/.test(request) && !isDevelopment) {
return callback(
null,
request.replace(/@common/, '$common').replace(/\//g, '.')
);
}
if (/^@moon$/.test(request) && !isDevelopment) {
return callback(null, '$common.Moon');
}
if (/^@http$/.test(request) && !isDevelopment) {
return callback(null, '$common.utils.http');
}
callback();
}
複製代碼
這裏解釋一下,callback
是一個回調函數(這也意味着它支持異步判斷),它的第一個參數目的不明,文檔沒有明說;第二個參數是個字符串,將會去 window
上執行此表達式,好比 $common.Moon
,它就會去找 window.$common.Moon
。
因此,以上代碼目的就很明瞭了:將 @common
替換成 $common
, 將引用路徑中的 /
替換爲 .
改成去 window
上查找。
變量名不容許以
@
符號開頭,因此我將library
的值換成了$common
那麼,如今構建頁面級項目已經能夠將公共部分剝離,讓它自動去 window
上尋找了,可此時 window
上尚未 $common
對象呢!
首先,上一節末尾,咱們的需求很明確,須要構建一個 $common
對象在 window
上,關於這一點咱們可使用 umd
、 window
或 global
形式進行構建。可是,$common
上要有一系列的子屬性,要能根據 import
的路徑進行層級設計,好比:
import $http, { Api } from '@http';
import Header from '@common/containers/header/header';
import { CommonStore } from '@common/store';
import { timeout } from '@packages/@core/common/public/moon/base';
import * as Enums2 from '@common/public/enums/enum';
import { Localstorage } from '@common/utils/storage';
複製代碼
咱們就須要 $common
具有以下結構:
那麼,該如何構建這種層級結構的 $common
對象呢?答案很簡單,針對編譯入口導出一個相應結構的對象便可!
直接貼代碼吧:
// webpack.config.js
output: {
filename: "public.js",
chunkFilename: 'app/public/chunks/[name].[chunkhash:8].js',
libraryTarget: 'window',
library: '$common',
libraryExport: "default",
},
entry: "../packages/@core/common/entry/index.tsx",
複製代碼
// @core/common/entry/index.tsx
import * as baseEnum from '../public/enums/base';
import * as Enum from '../public/enums/enum';
import * as exportExcel from '../public/enums/exportExcel';
import * as message from '../public/enums/message';
import commonStore from '../store';
import * as client from '../public/moon/client';
import * as moonBase from '../public/moon/base';
import AuthorityWrapper from '../public/wrapper/authority';
import ErrorBoundary from '../public/wrapper/errorBoundary';
import * as map from '../containers/map';
import pubsub from '../public/utils/pubsub';
import * as format from '../public/moon/format';
import termCheck from '../containers/termCheck/termCheck';
import filterManage from '../containers/filterManage/filterManage';
import * as post from '../public/utils/post';
import * as role from '../public/moon/role';
import resourceCode from '../public/moon/resourceCode';
import outClick from '../public/utils/outClick';
import newFeature from '../containers/newFeature';
import * as exportExcelBusiness from '../business/exportExcel';
import * as storage from '../public/utils/storage';
import * as _export from '../public/utils/export';
import * as _map from '../public/utils/map';
import * as date from '../public/moon/date';
import * as abFeature from '../public/moon/abFeature';
import * as behavior from '../public/moon/behavior';
import * as _message from '../public/moon/message';
import * as http from '../public/utils/http';
import Moon from '../public/moon';
import initFeToolkit from '../initFeToolkit';
import '../containers/header/style.less';
import withMonthPicker from '../public/hoc/searchBar/withMonthPicker';
import withDateRangePickerWeek from '../public/hoc/searchBar/withDateRangePickerWeek';
import withDateRangePickerClear from '../public/hoc/searchBar/withDateRangePickerClear';
import MessageCenterPush from '../public/moon/messageCenter/messageCenterPush';
import { AuthorityBusiness, ExportExcelBusiness, FeedbackBusinessBusiness,
FilterManageBusiness, HeaderBusiness, IAuthorityBusinessProps,
IExportExcelBusiness, IFeedbackBusiness, IFilterManageBusinessProps,
IHeaderBusinessProps, IMustDoBusinessProps, INewFeatureBusinessProps,
MustDoBusiness, NewFeatureBusiness } from '../business';
import {
Header, FeedBack, MustDoV1, MustDoV2, Weather,
withSearchBarCol, withAuthority,
withIconFilter, withExportToEmail, withSelectExport, withPageTable, withVisualEventLog
} from '../async';
const enums = {
base: baseEnum,
enum: Enum,
exportExcel,
message
};
const business = {
exportExcel: exportExcelBusiness,
feedback: FeedbackBusinessBusiness,
filterManage: { FilterManageBusiness },
header: { HeaderBusiness },
mustDo: { MustDoBusiness },
newFeature: { NewFeatureBusiness },
authority: { AuthorityBusiness },
};
const containers = {
map,
feedback: FeedBack,
newFeature,
weather: Weather,
header: { header: Header },
filterManage: { filterManage },
termCheck: { termCheck },
mustdo: {
mustdoV1: { mustDo: MustDoV1 },
mustdoV2: { mustDo: MustDoV2 },
}
};
const utils = {
pubsub,
post,
outClick,
storage,
http,
export: _export,
map: _map
};
const hoc = {
exportExcel: {
withExportToEmail: withExportToEmail,
withSelectExport: withSelectExport
},
searchBar: {
withDateRangePickerClear: withDateRangePickerClear,
withDateRangePickerWeek: withDateRangePickerWeek,
withMonthPicker: withMonthPicker,
withSearchBarCol: withSearchBarCol,
},
wo: {
withVisualEventLog: withVisualEventLog
},
withAuthority: withAuthority,
withIconFilter: withIconFilter,
withPageTable: withPageTable,
withVisualEventLog,
withSearchBarCol,
withMonthPicker,
withDateRangePickerWeek,
withDateRangePickerClear,
withSelectExport,
withExportToEmail,
};
export default {
enums,
utils,
business,
containers,
hoc,
initFeToolkit,
store: commonStore,
Moon: Moon,
wrapper: {
authority: AuthorityWrapper,
errorBoundary: ErrorBoundary,
},
public: {
enums,
hoc,
moon: {
date,
client,
role,
MessageCenterPush,
resourceCode,
format,
abFeature,
behavior,
message: _message,
base: moonBase,
}
}
};
複製代碼
代碼雖然有些長,可是沒有任何閱讀難度。咱們的目的就是構建這麼一個導出對象,它的層級結構窮舉了全部的 import
路徑可能性!
並且咱們一旦新增了公共文件給其它項目使用,就必須維護進這個文件,由於它纔是真正的入口!
這個文件這麼長,一方面是由於公共功能確實很是多,另外一方面也是由於咱們使用了 webpack 的
alias
功能,致使引用方式五花八門,窮舉出來的可能性稍微有點多(好比withSearchBarCol
就有兩種導入方式,因此結構裏面出現了兩次)。因此,你們若是要使用這套方案,建議定個規範控制一下比較好。
公共部分獨立構建完成了,頁面應用也將它們抽離了,那麼如何配合使用呢?
直接按順序引用便可!
有細心的童鞋可能就會問了,這樣子頁面應用引用的是打包後的 public.js
,實際開發的時候開發環境怎麼調試呢?
頁面應用構建或運行時,我加了 isDevelopment
變量去控制,只有構建生產環境時才抽離。不然直接調用 callback()
原樣返回,不做任何操做。
這樣,在開發環境寫代碼的時候,實際引用的仍是 node_modules
下的本地項目。
對於
monorepo
構架的本地項目依賴,lerna
創建的是軟鏈接。
其實,能用 webpack4 現有特性作到這程度,仍是很不容易的,畢竟人家國內外一線技術團隊都爲這事頭疼了好幾年呢!
接下來,讓咱們來看看 webpack5 這個讓他們眼前一亮的解決方案吧!
webpack5 給咱們帶來了一個內置 plugin: ModuleFederationPlugin
做者對它的定義以下:
Module federation allows a JavaScript application to dynamically load code from another application — in the process, sharing dependencies, if an application consuming a federated module does not have a dependency needed by the federated code — Webpack will download the missing dependency from that federated build origin.
Module Federation 使 JavaScript 應用得以從另外一個 JavaScript 應用中動態地加載代碼 —— 同時共享依賴。若是某應用所消費的 federated module 沒有 federated code 中所需的依賴,Webpack 將會從 federated 構建源中下載缺乏的依賴項。
幾個術語
Module federation
: 與 Apollo GraphQL federation
的想法相同 —— 但適用於在瀏覽器或者 Node.js 中運行的 JavaScript 模塊。
host
:在頁面加載過程當中(當 onLoad 事件被觸發)最早被初始化的 Webpack 構建;
remote
:部分被 「host」 消費的另外一個 Webpack 構建;
Bidirectional(雙向的) hosts
:當一個 bundle 或者 webpack build 做爲一個 host 或 remote 運行時,它要麼消費其餘應用,要麼被其餘應用消費——均發生在運行時(runtime)。
編排層(orchestration layer
):這是一個專門設計的 Webpack runtime 和 entry point,但它不是一個普通的應用 entry point,而且只有幾 KB。
先列出使用方式給你們看一下吧,待會兒咱們再深挖細節:
// app1 webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
...
plugins: [
new ModuleFederationPlugin({
name: "app1",
library: { type: "var", name: "app1" },
remotes: {
app2: "app2"
},
shared: ["react", "react-dom"]
}),
]
// app1 App.tsx
import * as React from "react";
import Button from 'app2/Button';
const RemoteButton = React.lazy(() => import("app2/Button"));
const RemoteTable = React.lazy(() => import("app2/Table"));
const App = () => (
<div> <h1>Typescript</h1> <h2>App 1</h2> <Button /> <React.Suspense fallback="Loading Button"> <RemoteButton /> <RemoteTable /> </React.Suspense> </div> ); export default App; // app2 webpack.config.js ... plugins: [ new ModuleFederationPlugin({ name: "app2", library: { type: "var", name: "app2" }, filename: "remoteEntry.js", exposes: { Button: "./src/Button", Table: "./src/Table" }, shared: ["react", "react-dom"] }) ] 複製代碼
這裏演示瞭如何在 app1 中使用 app2 共享的 Button
與 Table
組件。
稍微解釋下這幾個配置項的意義:
ModuleFederationPlugin
來自於 webpack/lib/container/ModuleFederationPlugin
,是一個 plugin
。
不管是 host
或是 remote
都須要初始化 ModuleFederationPlugin
插件。
任何模塊都能擔當 host
或 remote
或二者同時兼具。
name
必填項,未配置 filename
屬性時會做爲當前項目的編排層( orchestration layer
)文件名
filename
可選項,編排層文件名,若是未配置則使用 name
屬性值。
library
必填項,定義編排層模塊結構與變量名稱,與 output
的 libraryTarget
功能相似,只不過是只針對編排層。
exposes
可選項(共享模塊必填)對外暴露項,鍵值對,key
值爲 app1
(被共享模塊)中引用 import Button from 'app2/Button';
中後半截路徑,value
值爲 app2
項目中的實際路徑。
remote
鍵值對,含義相似於 external
,key
值爲 import Button from 'app2/Button';
中的前半截,value
值爲 app2
中配置的 library -> name
,也就是全局變量名。
shared
共享模塊,用於共享第三方庫。比方說 app1
先加載,共享 app2
中某個組件,而 app2
中這個組件依賴 react
。當加載 app2
中這個組件時,它會去 app1
的 shared
中查找有沒有 react
依賴,若是有就優先使用,沒有再加載本身的( fallback
)
最後在 index.html
中引入
<script src="http://app2/remoteEntry.js"></script>
複製代碼
便可。
有了以上這些配置, app1
中即可以自由的引入並使用 app2/Button
與 app2/Table
了。
那麼,ModuleFederationPlugin
是怎麼實現這個神奇的黑魔法的呢?
答案就在如下這段構建後的代碼中:
__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__.e(/* import() */ "src_bootstrap_tsx").then(__webpack_require__.bind(__webpack_require__, 601));
複製代碼
這是 app1
的啓動代碼,__webpack_require__.e
爲入口,查找 src_bootstrap_tsx
入口模塊依賴,去哪查找?
Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, [])
複製代碼
這裏遍歷了 f
對象上全部的方法。
下面貼出了 f
對象上綁定的全部三個方法 overridables
remotes
j
:
/******/ /* webpack/runtime/overridables */
/******/ (() => {
/******/ __webpack_require__.O = {};
/******/ var chunkMapping = {
/******/ "src_bootstrap_tsx": [
/******/ 471,
/******/ 14
/******/ ]
/******/ };
/******/ var idToNameMapping = {
/******/ "14": "react",
/******/ "471": "react-dom"
/******/ };
/******/ var fallbackMapping = {
/******/ 471: () => {
/******/ return __webpack_require__.e("vendors-node_modules_react-dom_index_js").then(() => () => __webpack_require__(316))
/******/ },
/******/ 14: () => {
/******/ return __webpack_require__.e("node_modules_react_index_js").then(() => () => __webpack_require__(784))
/******/ }
/******/ };
/******/ __webpack_require__.f.overridables = (chunkId, promises) => {
/******/ if(__webpack_require__.o(chunkMapping, chunkId)) {
/******/ chunkMapping[chunkId].forEach((id) => {
/******/ if(__webpack_modules__[id]) return;
/******/ promises.push(Promise.resolve((__webpack_require__.O[idToNameMapping[id]] || fallbackMapping[id])()).then((factory) => {
/******/ __webpack_modules__[id] = (module) => {
/******/ module.exports = factory();
/******/ }
/******/ }))
/******/ });
/******/ }
/******/ }
/******/ })();
/******/ /* webpack/runtime/remotes loading */
/******/ (() => {
/******/ var chunkMapping = {
/******/ "src_bootstrap_tsx": [
/******/ 341,
/******/ 980
/******/ ]
/******/ };
/******/ var idToExternalAndNameMapping = {
/******/ "341": [
/******/ 731,
/******/ "Button"
/******/ ],
/******/ "980": [
/******/ 731,
/******/ "Table"
/******/ ]
/******/ };
/******/ __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();
/******/ }
/******/ }))
/******/ });
/******/ }
/******/ }
/******/ })();
/******/ /* webpack/runtime/jsonp chunk loading */
__webpack_require__.f.j = (chunkId, promises) => {
...
/******/ })();
複製代碼
最後一個 f.j
方法就不貼細節了,是 wepback4 時代就有的 jsonp
加載。
咱們主要關注 f.remotes
與 f.overridables
兩個 webpack5 新增的方法。Zack Jackson (做者)選擇在這兒動刀子,確實很精妙。與 external
不一樣(external
是構建時與外界的聯繫入口) ,這兒是構建後與外界聯繫的入口。
咱們待會兒就能看到,實際上真正跟外界打交道的方式與我上一節在 webpack4 中探討的方式如出一轍,都是經過全局變量去打通引用。
先說下上段代碼中 reduce
的做用:它主要是遍歷上面這三個方法,挨個去查找某依賴是否存在!
shared 公共第三方依賴, react
與 react-dom
等公共依賴會有此處進行解析。app1
在構建時,會獨立構建出這兩個文件,app2
裏的 exposes
模塊在加載時會優先查找 app1
下的 shared
依賴,如有,則直接使用,若無,則使用自身的。
remotes 依賴,會將配置中的 remotes
鍵值對生成在 idToExternalAndNameMapping
變量中,而後最關鍵的一點在於:
貼兩張圖,咱們來一一分析:
首先,前面說會 __webpack_require__.e
會挨個查找 overridables
remotes
j
三個方法,當查找到 remotes
時會如上圖所示,進入 remotes
方法。
此時的 chunkId
變量值是 src_bootstrap_tsx
,那麼,首層會遍歷 341
與 980
,而後經過這兩個值,查找 idToExternalAndNameMapping
,從而找到 341
的值爲 [731, "Button"]
,980
的值爲 [731, "Table"]
。
圖中高亮的這行代碼 __webpack_require__(data[0]).get(data[1])
目的就是取 731
這個模塊,再調用它的 get
方法,參數爲 Button
| Table
,去取 Button 或 Table 組件。
那麼問題來了,這個 731
是什麼模塊? 它上面爲何會有 get
方法呢?
繼續看上面第二張圖,我高亮了 731
這個模塊,它的內部引用了 907
模塊,並 override
了 react
react-dom
兩個模塊,指向 14
與 471
(這兩個值正好來自於 overridables
方法裏定義的 idToNameMapping
映射)。
而 907
模塊正是引用了全局變量 app2
!
爲何 app2
這個變量上會存在 get
方法呢?咱們構建 app2
時可並無定義這個方法,讓咱們移步來看下 app2
的構建結果:
點開 remoteEntry.js
,答案揭曉:
ModuleFederationPlugin
會在編排層上定義兩個方法 get
與 override
,其中:
get
用於查找自身的 moduleMap
映射(來自於 exposes
配置),正是這個全局變量 app2
+ 它的 get
方法鏈接了兩個絕不相關的模塊!
override
則用於查找 shared
第三方依賴,這裏也極其精妙,爲何這麼說呢?在前文貼的代碼中,咱們將目光放在 app1
的編排層中,找到 __webpack_require__.O
對象,它定義在 overridables
方法運行時,其初始值爲 {}
,但又在 __webpack_require__.f.overridables
正式執行時是空的。這就使得 app1
在執行時是直接使用的 fallbackMapping
(也就是本地自身第三方依賴)。
而前面提到的 731
模塊中正好使用 app2
提供的 override
方法將 react
與 react-dom
的 app1
中的引用複寫到了 app2
內部,咱們將目光移到 app2
的編排層(全部的編排層代碼都是一致的),app2
中的 overridables
就使用了 __webpack_require__.O
中的 react
與 react-dom
依賴!
能夠看到,app2
中的 override
方法將外部調用傳入的 app1
中的第三方依賴複寫到了 __webpack_require__.O
變量上!
這也正是做者爲什麼強調幾乎沒有任何依賴冗餘的緣由所在:
There is little to no dependency duplication. Through the shared option — remotes will depend on host dependencies, if the host does not have a dependency, the remote will download its own. No code duplication, but built-in redundancy.
ModuleFederationPlugin
給咱們帶來了無限的想象空間,應用場景不少,例如微前端上微應用的依賴共享,模塊共享等。
我能想到的兩點缺陷:
其一在於針對要暴露出去的模塊須要額外的 exposes
配置(對於本文前一節中咱們自身的場景並不合適,entry
導出結構太複雜了),並且必須通知全部使用的模塊正確配置;
其二則是本地依賴調試時,在配置了 npm link 或 lerna 本地依賴以後,還須要針對 remotes
配置同名的 webpack alias
跟 tsconfig paths
,略有些繁瑣。
可是,將 wepback4
與 webpack5
這兩種解決方案結合起來以後按場景使用,就近乎完美了!
噢,忘了提一嘴,使用了這兩種方案以後,編譯性能提高很是大,由於公共部分直接跳過,沒必要再進行編譯了;而針對分離的共享文件也能夠作緩存,加載性能也隨之提高了。數據就不貼了,各自的應用場景不一樣,心中明瞭便可。
Webpack 5 Module Federation: A game-changer in JavaScript architecture