问题引入
先抛出一个尖锐问题:MPA 多页应用或微前端架构,如何处理页面的公共部分。
之所以说它尖锐,是因为不止我们公司,包括腾讯在内很多国内外一线技术团队都碰到了。
比如这篇博客:腾讯文档的困境
我们拿MPA应用举例,比如菜单部分,传统后端模板渲染时一般会通过
// .NET
@Html.Partial("Header")
或
// Java
<%@ include file="header.jsp" %>
引入公共模板,这样在访问页面时会直接渲染公共部分。
但如果是现代化工程(比如 React),可前后端又未分离的MPA项目(页面仍由后端渲染,Dom 渲染交由 React 接管),我们就会将构建后的资源文件拷贝到后端工程里,通过在页面引入 script 与 style 进行渲染。
此时问题就暴露出来了,对于页头这种公共部分,是 React 渲染的组件。
常规做法是将公共部分作为组件直接构建进每个页面级项目,嗯,腾讯文档也是这么做的。
这样做会带来如下缺点:
- 构建冗余,每个页面级项目构建时都会将其打包进去,无端浪费加载带宽。
比如
Header
部分单独构建体积为400KB
,那么每个页面级构建结果都会在现有体积上增大400KB
(忽略公共库依赖,假设统一使用DllReferencePlugin
处理)。没有丝毫夸张的成份,我们Header
里有很多功能,加上chunks
之后确实有将近500KB
。
- 如果公共部分做了修改,此时所有引用它的项目全部要重新构建发版!
尤其是对于
Header
这种每个页面都会使用的公共部分而言,只要做一丁点修改,所有页面级应用都必须重新构建发版。
比如下图中腾讯文档的通知中心:
在 webpack5 发布之前,这似乎是一个不可能实现的事情!
腾讯文档的前端们也研究过这个问题,但是从文中描述的研究过程来看,主要是针对打包后的 __webpack_require__
方法中埋入勾子做文章,并且一直没有提出有效的解决方案。
说实话,webpack 底层还是很复杂的,在不熟悉的情况下而且定制程度也不能确定,所以我们也是迟迟没有去真正做这个事情。
—— 摘自《腾讯文档的困境》
但是,我们经过一系列的探索,在2019年7月利用 webpack4 现有特性完美解决了这个问题!巧合的是,Wepback 团队在最新的 V5 版本中也新增了 Module-Federation
这个 Feature,用于此场景。
下面开始正式上干货!
webpack4 解决方案
腾讯文档的小伙伴之所以不敢对 __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 解决方案
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 共享的 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
)
最后在 app1 中的 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
的作用:它主要是遍历上面这三个方法,挨个去查找某依赖是否存在!
overridables
shared 公共第三方依赖, react
与 react-dom
等公共依赖会有此处进行解析。app1
在构建时,会独立构建出这两个文件,app2
里的 exposes
模块在加载时会优先查找 app1
下的 shared
依赖,若有,则直接使用,若无,则使用自身的。
remotes
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.
截至2010/05/13,发现 webpack5 的这块插件代码还在不停合并新的提交到 master 分支。稍微看了一眼最近两次提交,发现打包层面改动还比较大(配置项暂时没有变化),所以以上打包结果的代码仅供参考,大致明白原理即可,我今天测试的构建结果代码已经不一样了。
总结
ModuleFederationPlugin
给我们带来了无限的想象空间,应用场景很多,例如微前端上微应用的依赖共享,模块共享等。
我能想到的两点缺陷:
-
其一在于针对要暴露出去的模块需要额外的
exposes
配置(对于本文前一节中我们自身的场景并不合适,entry
导出结构太复杂了),而且必须通知所有使用的模块正确配置; -
其二则是本地依赖调试时,在配置了 npm link 或 lerna 本地依赖之后,还需要针对
remotes
配置同名的webpack alias
跟tsconfig paths
,略有些繁琐。
但是,将 wepback4
与 webpack5
这两种解决方案结合起来之后按场景使用,就近乎完美了!
噢,忘了提一嘴,使用了这两种方案之后,编译性能提升非常大,因为公共部分直接跳过,不必再进行编译了;而针对分离的共享文件也可以做缓存,加载性能也随之提升了。数据就不贴了,各自的应用场景不同,心中明了即可。
参考资料
Webpack 5 Module Federation: A game-changer in JavaScript architecture