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

相关文章
相关标签/搜索