探索webpack4与webpack5多项目公共代码复用架构
问题引入
先抛出一个尖锐问题: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
转载自:https://juejin.cn/post/6844904149746745357