webpack4 與 webpack5 公共代碼抽離共享方案

問題引入

先拋出一個尖銳問題: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項目)

smsclient


在 webpack5 發佈以前,這彷佛是一個不可能實現的事情!

騰訊文檔的前端們也研究過這個問題,可是從文中描述的研究過程來看,主要是針對打包後的 __webpack_require__ 方法中埋入勾子作文章,而且一直沒有提出有效的解決方案。

說實話,webpack 底層仍是很複雜的,在不熟悉的狀況下並且定製程度也不能肯定,因此咱們也是遲遲沒有去真正作這個事情。

—— 摘自《騰訊文檔的困境

可是,咱們通過一系列的探索,在2019年7月利用 webpack4 現有特性完美解決了這個問題!巧合的是,Wepback 團隊在最新的 V5 版本中也新增了 Module-Federation 這個 Feature,用於此場景。

下面開始正式上乾貨!

webpack4 解決方案

騰訊文檔的小夥伴之因此不敢對 __webpack_require__ 動手無非就是由於它太複雜了,怕改動以後引起其它問題。

其實一開始他們的方向就錯了,正所謂打蛇打七寸,若是沒打中七寸就會引起一系列問題,或者遲遲不敢打。

因此,咱們將目光移到」七寸「 外部擴展(externals) 屬性上來看一下(默認各位都已經知道它的做用了)。

正由於它是 webpack 內部(npm + 構建)與外部引用的橋樑,因此我認爲在這裏動刀子是最恰當不過的!


回顧 externalsumd

回憶一下,咱們使用 externals 配置 CDN 第三方庫,好比 React,配置以下:

externals: {
  'react-dom': 'ReactDOM',
  'react': 'React'
}
複製代碼

而後咱們再看下 React 的CDN引用連接,通常咱們使用的是 umd 構建版本,它會兼容 commonjscommonjs2amdwindow 等方案,在咱們的瀏覽器環境中,它會綁定一個 React 變量到 window 上:

JoLAfI.png

externals 的做用在於:當 webpack 進行構建時,碰到 import React from 'react'import ReactDOM from 'react-dom' 導入語句時會避開 node_modules 而去 externals 配置的映射上去找,而這個映射值( ReactDOMReact )正是在 window 變量上找到的。

下面兩張圖能夠證實這一點:

JoO4qU.png

JoXwWR.png

爲何我要花這麼多篇幅去鋪墊這個 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 上這麼死板,確定有隱藏入口才對。果不其然!細讀了下官方文檔,讓我找到了一絲端倪:它還支持函數

J5olT0.png

函數的功能在於:能夠自由控制任何 import 語句!

咱們能夠試着在這個函數裏打印一下入參 request 的值,結果以下圖所示:

J7Sefg.png

全部的 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 上,關於這一點咱們可使用 umdwindowglobal 形式進行構建。可是,$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 具有以下結構:

J7AwTA.png

那麼,該如何構建這種層級結構的 $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 就有兩種導入方式,因此結構裏面出現了兩次)。因此,你們若是要使用這套方案,建議定個規範控制一下比較好。

組合使用

公共部分獨立構建完成了,頁面應用也將它們抽離了,那麼如何配合使用呢?

J7nwTg.png

直接按順序引用便可!

如何調試

有細心的童鞋可能就會問了,這樣子頁面應用引用的是打包後的 public.js,實際開發的時候開發環境怎麼調試呢?

J7uJN4.png

頁面應用構建或運行時,我加了 isDevelopment 變量去控制,只有構建生產環境時才抽離。不然直接調用 callback() 原樣返回,不做任何操做。

這樣,在開發環境寫代碼的時候,實際引用的仍是 node_modules 下的本地項目。

對於 monorepo 構架的本地項目依賴, lerna 創建的是軟鏈接。

其實,能用 webpack4 現有特性作到這程度,仍是很不容易的,畢竟人家國內外一線技術團隊都爲這事頭疼了好幾年呢!

接下來,讓咱們來看看 webpack5 這個讓他們眼前一亮的解決方案吧!

webpack5 解決方案

Module Federation

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 共享的 ButtonTable 組件。

稍微解釋下這幾個配置項的意義:

  • ModuleFederationPlugin 來自於 webpack/lib/container/ModuleFederationPlugin,是一個 plugin

  • 不管是 host 或是 remote 都須要初始化 ModuleFederationPlugin 插件。

  • 任何模塊都能擔當 hostremote 或二者同時兼具。

  • name 必填項,未配置 filename 屬性時會做爲當前項目的編排層( orchestration layer )文件名

  • filename 可選項,編排層文件名,若是未配置則使用 name 屬性值。

  • library 必填項,定義編排層模塊結構與變量名稱,與 outputlibraryTarget 功能相似,只不過是只針對編排層。

  • exposes 可選項(共享模塊必填)對外暴露項,鍵值對,key 值爲 app1 (被共享模塊)中引用 import Button from 'app2/Button'; 中後半截路徑,value 值爲 app2 項目中的實際路徑。

  • remote 鍵值對,含義相似於 externalkey 值爲 import Button from 'app2/Button'; 中的前半截,value 值爲 app2 中配置的 library -> name,也就是全局變量名。

  • shared 共享模塊,用於共享第三方庫。比方說 app1 先加載,共享 app2 中某個組件,而 app2 中這個組件依賴 react。當加載 app2 中這個組件時,它會去 app1shared 中查找有沒有 react 依賴,若是有就優先使用,沒有再加載本身的( fallback

最後在 index.html 中引入

<script src="http://app2/remoteEntry.js"></script>
複製代碼

便可。

有了以上這些配置, app1 中即可以自由的引入並使用 app2/Buttonapp2/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.remotesf.overridables 兩個 webpack5 新增的方法。Zack Jackson (做者)選擇在這兒動刀子,確實很精妙。與 external 不一樣(external 是構建時與外界的聯繫入口) ,這兒是構建後與外界聯繫的入口。

咱們待會兒就能看到,實際上真正跟外界打交道的方式與我上一節在 webpack4 中探討的方式如出一轍,都是經過全局變量去打通引用。

先說下上段代碼中 reduce 的做用:它主要是遍歷上面這三個方法,挨個去查找某依賴是否存在

overridables

shared 公共第三方依賴, reactreact-dom 等公共依賴會有此處進行解析。app1 在構建時,會獨立構建出這兩個文件,app2 裏的 exposes 模塊在加載時會優先查找 app1 下的 shared 依賴,如有,則直接使用,若無,則使用自身的。

remotes

remotes 依賴,會將配置中的 remotes 鍵值對生成在 idToExternalAndNameMapping 變量中,而後最關鍵的一點在於:

YZNCTK.png

YZpdmT.png

貼兩張圖,咱們來一一分析:

首先,前面說會 __webpack_require__.e 會挨個查找 overridables remotes j 三個方法,當查找到 remotes 時會如上圖所示,進入 remotes 方法。

此時的 chunkId 變量值是 src_bootstrap_tsx,那麼,首層會遍歷 341980 ,而後經過這兩個值,查找 idToExternalAndNameMapping ,從而找到 341 的值爲 [731, "Button"]980 的值爲 [731, "Table"]

圖中高亮的這行代碼 __webpack_require__(data[0]).get(data[1]) 目的就是取 731 這個模塊,再調用它的 get 方法,參數爲 Button | Table,去取 Button 或 Table 組件。

那麼問題來了,這個 731 是什麼模塊? 它上面爲何會有 get 方法呢?

繼續看上面第二張圖,我高亮了 731 這個模塊,它的內部引用了 907 模塊,並 overridereact react-dom 兩個模塊,指向 14471 (這兩個值正好來自於 overridables 方法裏定義的 idToNameMapping 映射)。

907 模塊正是引用了全局變量 app2

爲何 app2 這個變量上會存在 get 方法呢?咱們構建 app2 時可並無定義這個方法,讓咱們移步來看下 app2 的構建結果:

YZu8SK.png

點開 remoteEntry.js ,答案揭曉:

YZuaTA.png

ModuleFederationPlugin 會在編排層上定義兩個方法 getoverride,其中:

get 用於查找自身的 moduleMap 映射(來自於 exposes 配置),正是這個全局變量 app2 + 它的 get 方法鏈接了兩個絕不相關的模塊!

override 則用於查找 shared 第三方依賴,這裏也極其精妙,爲何這麼說呢?在前文貼的代碼中,咱們將目光放在 app1 的編排層中,找到 __webpack_require__.O 對象,它定義在 overridables 方法運行時,其初始值爲 {},但又在 __webpack_require__.f.overridables 正式執行時是空的。這就使得 app1 在執行時是直接使用的 fallbackMapping (也就是本地自身第三方依賴)。

YZ80HO.png

而前面提到的 731 模塊中正好使用 app2 提供的 override 方法將 reactreact-domapp1 中的引用複寫到了 app2內部,咱們將目光移到 app2 的編排層(全部的編排層代碼都是一致的),app2 中的 overridables 就使用了 __webpack_require__.O 中的 reactreact-dom 依賴!

YZJy6I.png

能夠看到,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 aliastsconfig paths,略有些繁瑣。

可是,將 wepback4webpack5 這兩種解決方案結合起來以後按場景使用,就近乎完美了!

噢,忘了提一嘴,使用了這兩種方案以後,編譯性能提高很是大,由於公共部分直接跳過,沒必要再進行編譯了;而針對分離的共享文件也能夠作緩存,加載性能也隨之提高了。數據就不貼了,各自的應用場景不一樣,心中明瞭便可。

參考資料

Webpack 5 Module Federation: A game-changer in JavaScript architecture

相關文章
相關標籤/搜索