关于webpack:编译的速度与激情从10mins到1s

4次阅读

共计 12250 个字符,预计需要花费 31 分钟才能阅读完成。

原文:编译的速度与激情:从 10mins 到 1s | AlloyTeam
作者:glendonli

导语:对于大型前端我的项目而言,构建的稳定性和易用性至关重要,腾讯文档在迭代过程中,简单的我的项目构造和编译带来的问题日益增多,极大的减少了新人上手与日常搬砖的开销。恰逢 Webpack5 上线,不如来一次彻底的魔改~

1. 前言

腾讯文档最近基于刚刚公布的 Webpack5 进行了一次编译的大重构,作为一个多个仓库独特形成的大型项目,任意品类的代码量都超过百万。对于腾讯文档这样一个疾速迭代,高度依赖自动化流水线,长年并行多个大型需要和有数小需要的我的项目来说,稳固且疾速的编译对于开发效率至关重要。

这篇文章,就是笔者最近进行重构,胜利将日常开发优化到 1s 的过程中,遇到的一些大型项目特有的问题和思考,心愿能给大家在前端我的项目构建的优化中带来一些参考和启发。

2. 大型项目编译之痛

随着我的项目体系的逐步扩充,往往会遇到旧的编译配置无奈反对新个性,因为各种 config 文件自带的浏览 debuff,以及累累的技术债,大家总会趋于不去批改旧配置,而是试图新增一些配置在外围对编译系统进行修改。也是这样相似的起因,腾讯文档过来的编译编译也并不优雅:

多级的子仓库构造,简单的编译系统造成很高的了解和改变老本,也带来了较高的编译耗时,对于整个团队的开发效率有着不小的影响。

3.All in One

为了解决编译简单和迟缓的问题,至关重要的,就是禁止套娃:多层级混合的零碎必须破除,对立的编译才是王道。在所有编译系统中,Webpack 在大我的项目的打包上具备很强劣势,插件零碎最为饱满,并且 Webpack5 的带来了 Module Federation 新个性,因而笔者抉择了用 Webpack 来统合多个子仓库的编译。

3.1. 整合基于 lerna 的仓库构造

腾讯文档应用了 lerna 来治理仓库中的子包,应用 lerna 的益处此处就不作开展了。不过 lerna 的通用用法也带来了肯定的问题,lerna 将一个仓库变成了构造上的多个仓库,如果依照默认的应用形式,每个仓库都会有本人的编译配置,单个我的项目的编译变成了多个我的项目的联编联调,批改配置和增量优化都会变得比拟艰难。

尽管应用 lerna 的目标是使各个子包绝对独立,然而在整个我的项目的编译调试中,往往须要的是所有包的汇合,那么,笔者就能够疏忽掉这个子包间的物理隔离,把子仓库作为子目录来对待。不依赖 lerna,笔者须要解决的,是子包间的援用问题:

/** package/layout/src/xxx.ts **/
import {Stream} from "@core/model";
// do something

事实上,笔者能够通过 webpack 配置中 resolve 的 alias 属性来达到相应成果:

{
 resolve: {
 alias: {'@core/model': 'word/package/model/src/',}
 }
}

3.2. 治理游离于打包零碎之外的文件

在大型项目中,有时会存在一些非凡的动态代码文件,它们往往并不参加到打包零碎中,而是由其余形式间接引入 html,或者合并到最终的后果中。

这样的文件,个别分为如下几类:

  1. 加载机会较早的内部 sdk 文件,自身以 minify 文件提供
  2. 内部文件依赖的其余框架文件,比方 jquery
  3. 一些 polyfill
  4. 一些非凡的必须晚期运行的独立逻辑,比方初始化 sdk 等

因为 polyfill 和内部 sdk 往往间接通过挂全局变量运行的模式,我的项目中往往会通过间接写入 html script 标签的形式援用它们。不过,随着此类文件的增多,间接利用标签援用,对于版本治理和编译流程都不敌对,它们对应的一些初始化逻辑,也无奈增加到打包流程中来。这种状况,笔者倡议手工的创立一个 js 入口文件,对以上文件进行援用,并作为 webpack 的一个入口。如此,就能通过代码的形式,将这些散装文件治理起来了:

import "jquery";
import "raven.min.js";
import "log.js";
// ...

然而,一些内部的 js 可能依赖于其余 sdk,比方 jQuery,然而打包零碎并不知道它们之间的依赖关系,导致 jQuery 没有及时裸露到全局中,该怎么办呢?事实上,webpack 提供了很灵便的计划来解决这些问题,比方,笔者能够通过 expose-loader,将 jQuery 的裸露到全局,供第三方援用。

在腾讯文档中,还蕴含了一些对近程 cdn 的 sdk 组件,这些 sdk 也须要援用一些库,比方 jQuery 的。因而,笔者还通过 splitChunks 的配置,将 jQuery 从新分离出来,放在了较早的加载机会,保障基于 cdn 加载的 sdk 亦能失常初始化。

通过代码援用,一方面,能够很好的进行依赖文件的版本治理;另一方面,因为对应文件的编译也退出了打包流程,所有对应文件的改变都能够被动静监督到,有利于后续进行增量编译。同时,因为 webpack 的封装特点,每个库都会被蕴含在一个 webpack_require 的非凡函数之中,全局变量的裸露数量也变得较为可控。

3.3. 定制化的 webpack 流程

Webpack 提供了一个非常灵活的 html-webpack-plugin 来进行 html 生成,它反对模板和一众的专属插件,然而,依然架不住我的项目有一些非凡的需要,通用的插件配置要么无奈满足这些需要,要么适配的后果就非常难懂。这也是腾讯文档在最后应用了 gulp 来生成 html 的起因,在 gulp 配置中,有很多自定义流程来满足腾讯文档的公布要求。

既然,gulp 能够自定义流程来实现 html 生成,那么,笔者也能够独自写一个 webpack 插件来实现定制的流程。

Webpack 自身是一个非常灵活的零碎,它是一个依照特定的流程执行的框架,在每个流程的不同的阶段提供了不同的钩子,通过各种插件去实现这些钩子的回调,来实现代码的打包,事实上,webpack 自身就是由有数原生插件组成的。在这整个流程中,笔者能够做各种不同的事件来定制它。

对于生成 html 的场景,通过减少一个插件,在 webpack 解决生成文件的阶段,将生成的 js、css 等资源文件,以及 ejs 模板和非凡配置整合到一起,再增加到 webpack 的 assets 汇合中,便能够实现一次自定义的 html 生成。

compilation.hooks.processAssets.tap(
 {
 name: "TemplateHtmlEmitPlugin",
 stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
 },
 () => {
 // custom generation
 compilation.emitAsset(`${parsed.name}.html`,
 new sources.RawSource(source, true)
 );
 compilation.fileDependencies.addAll(dependencies);
 }
);

在以上代码中,大家能够留意到最初一句:compilation.fileDependencies.addAll(dependencies),通过这一句,笔者能够将所有被自定义生成所依赖的文件退出的 webpack 的依赖零碎中,那么当这些文件产生变更的时候,webpack 可能主动再次触发对应的生成流程。

3.4. 一键化的开发体验

至此,各路编译都曾经统一化,笔者能够用一个 webpack 编译整个我的项目了,watch 和 devServer 也能够一起 high 起来。不过,既然编译能够对立,何不让所有操作都整合起来呢?

基于 node_modules 不应该手工操作的假如,笔者能够创立 package.json 中依赖的快照,每次依据 package 的变动来判断是否须要重新安装,防止开发同学同步代码后的手动判断,跳过不必要的步骤。

public async install(force = false) {const startTime = performance.now();
 const lastSnapshot = this.readSnapshot();
 const snapshot = this.createSnapshot();
 const runs = this.repoInfos.map((repo) => {
 if (this.isRepoInstallMissing(repo.root)
 || (!repo.installed
 && (force || !isEqual(snapshot[repo.root], lastSnapshot[repo.root])))
 ) {// create and return install cmd}
 return undefined;
 }).filter(script => !!script);
 const {info} = console;
 if (runs.length > 0) {
 try {
 // 执行装置并保留快照
 await Promise.all(runs.map(run => this.exec(run!.cmd, run!.cwd, run!.name)));
 this.saveSnapshot(snapshot);
 } catch (e) {this.removeSnapshot();
 throw e;
 }
 } else {info(chalk.green('Skip install'));
 }
 info(chalk.bgGreen.black(`Install cost: ${TimeUtil.formatTime(performance.now() - startTime)}`));
}

同样的,腾讯文档的本地调试是基于非凡的测试环境,通过 whislte 进行代理,这样的步骤也能够自动化,那么,对于开发来说,所有就很轻松了,一条命令,轻松搬砖~

不过,作为是一个简单的零碎,第一次应用,总须要初始化的吧,如果编译系统的依赖尚未装置,没有鸡,怎么生蛋呢?

其实不然,笔者无妨在整套编译系统的外层套个娃,做前端开发,node 总会先装置的吧?那么,在执行正在的编译命令之前,笔者执行一个只依赖于 node 的脚本,这个脚本会尝试执行次要命令,如果主命令间接 crash,阐明装置环境尚未筹备结束,那么这个时候,对编译系统进行初始化就 ok 了。如此,就真的能够做到一键开发了。

const cmd = "启动编译的命令";
const main = (extraArg) =>
 childProcess.execSync(`${cmd} ${extraArg}`, {
 stdio: "inherit",
 cwd: __dirname,
 });
try {main("");
} catch (e) {
 // 初始化
 main("after-initialize");
}

3.5. 编译系统代码化

在这一次的重构过程中,笔者将本来的编译配置改为了由 ts 调用 webpack 的 nodeApi 来执行编译。代码化的编译系统有诸多益处:

  1. 应用 api 调用,能够享受 IDE 带来的代码提醒,再也不会因为不小心在配置文件外面打了一个 typo 而调试一整天。
  2. 应用代码 api,可能更好的实现编译的构造,特地是有多重输入的时候,比起简略的 config 文件组合,更好治理。
  3. 应用代码化的编译系统,还有一个特地的作用,编译系统也能够写测试了!啥?编译系统要写测试?事实上,在腾讯文档历次的公布中,经验过数次莫名的 bug,在上线前的测试中,整个程序的体现忽然就不失常了。相干代码,并没有任何改变,大家地毯式的排查了很久,才发现编译的后果和以前有渺小的不同。

    事实上,在零碎测试环境生成的前五个小时,一个编译所依赖的插件默默的更新了一个小版本,而笔者在 package.json 中对该插件应用的是默认的 ^xx.xx,流水线 install 到了最新的版本,导致了 crash。过后笔者得出了一个论断,编译相干的库须要锁定版本。然而,锁定版本并不能真正的解决问题,编译所应用的组件,总有降级的一天,如果保障这个降级不会引起问题呢?这就是自动化测试的领域了。

    如果大家看看 Webpack 的代码,会发现他们也做了很多测试用例来编译的一致性,然而,webpack 的插件形形色色,并不是每一个作者在品质保障上都有足够的投入,因而,用自动化测试保障编译系统的稳定性,也是一个能够深入研究的课题。

4. 编译提速

在波及 typescript 编译的我的项目中,根本的提速操作,就是异步的类型查看,ts-loader 的 tranpsileOnly 参数和 fork-ts-checker 的组合拳百试不厌。不过,对于简单的大型项目来说,这一套组合拳的启用过程未必是一帆风顺,无妨随着笔者一起看看,在腾讯文档中,启用疾速编译的崎岖之路。

4.1. 隐没的 enum

在启用 transpileOnly 参数后,编译速度立刻有了质的晋升,然而,后果并不乐观。编译后,页面还没关上,就 crash 了。依据报错查上来,发现一个从依赖库导入的对象变成了 undefined,从而引起程序解体。这个变为 undefined 的对象,是一个 enum,定义如下:

export const enumScope{
 VAL1= 0,
 VAL2= 1,
}

为什么当笔者启用了 transpileOnly 后它就为空了呢?这和它的非凡属性无关,它不是一个一般的 enum,它是一个 const enum。家喻户晓,枚举是 ts 的语法糖,每一个枚举,对应了 js 中的一个对象,所以,一个一般的枚举,转化为 js 之后,会变成这样:

// ts
export enum Scope {
 VAL1 = 0,
 VAL2 = 1,
}
const a = Scope.VAL1;
// js
constScope = {
 VAL1: 0,
 VAL2: 1,
 0: "VAL1",
 1: "VAL2",
};
const a = Scope.EDITOR;

如果笔者给 Scope 加上一个 const 关键字呢?它会变成这样:

// ts
export const enumScope{
 VAL1= 0,
 VAL2= 1,
}
const a = Scope.VAL1;
// js
const a = 0;

也就是说,const enum 就和宏是等效的,在翻译成 js 之后,它就不存在了。可是,为何在敞开 transpileOnly 时,编译后果能够失常运行呢?其实,认真翻看内部库的申明文件.d.ts,就会发现,在这个.d.ts 文件中,Scope 被一成不变的保留了下来。

// .d.ts
export const enumScope{
 VAL1= 0,
 VAL2= 1,
}

在失常的编译流程下,tsc 会查看.d.ts 文件,并且曾经预知了这一个定义,因而,它可能正确的执行宏转换,而对于 transpileOnly 开启的状况下,所有的类型被疏忽,因为本来的库模块中曾经不存在 Scope 了,所以编译后果无奈失常执行(PS:tsc 官网曾经表态 transpile 模式下的编译不解析.d.ts 是规范 feature,失落了 const enum 不属于 bug,所以期待官网反对是无果的)。既然得悉了原因,就能够修复了。四种计划:

  • 计划一,遵循官网领导,对于不导出 const enum,只对外部应用的枚举 const 化,也就是说,须要批改依赖库。当然,腾讯文档本次 crash 所有依赖库的确属于自有的 sdk,然而如果是内部的库引起了该问题呢?所以该计划并不保险。
  • 计划二,完美版,手动解析.d.ts 文件,寻找所有 const enum 并提取定义。然而,transpileOnly 获取的编译减速真是得益于疏忽.d.ts 文件,如果笔者再去为了一个 enum 手工解析.d.ts,而.d.ts 文件可能存在简单的援用链路,是极其耗时的。

  • 计划三 ,字符串替换,既然 const enum 是宏,那么笔者能够手工通过 string-replace-loader 达到相似成果。不过,字符串替换形式仍旧过于暴力,如果应用了相似于 Scope[‘VAL1’] 的用法,可能就猝不及防的生效了。

  • 计划四,也是笔者最终所采取的计划,既然定义隐没了,从新定义就好,通过 Webpack 的 DefinePlugin,笔者能够从新定义失落的对象,保障编译的失常解析。
new DefinePlugin({Scope: { VAL1: 0, VAL2: 1},
});

4.2. 爱恨交加的 decorator 及依赖注入

很可怜,仅仅是解决了编译对象失落的问题,代码仍旧无奈运行。程序在初始化的时候,仍旧迷之失败了,通过一番调试,发现,初始化流程有一些奥妙的不同。很显著,transpileOnly 开启的状况下,编译的后果产生了变动。

要解决这个问题,就须要对 transpileOnly 模式的实现一探到底了。transpileOnly 底层是基于 tsc 的 transpileModule 性能来实现的,transpileModule 的作用,是将每一个文件当做独立的个体进行解析,每一个 import 都会被当做一个整体模块来对待,编译器不会再解析模块导出与文件的具体关系,举个例子:

// src/base/a.ts
export class A {}
// src/base/b.ts
export class B {}
// src/base/index.ts
export * from "./a";
export * from "./b";
// src/app.ts
import {A} from "./base";
const a = new A();

如上是常见的代码写法,咱们往往会通过一个 index.ts 导出 base 中的模块,这样,在其余模块中,笔者就不须要援用到文件了。在失常模式下,编辑器解析这段代码,会附带信息,告知 webpack,A 是由 a.ts 导出的,因而,webpack 在打包时,能够依据具体场景将 A、B 打包到不同的文件中。

然而,在 transpileModule 模式下,webpack 所晓得的,只有 base 模块导出了 A,然而它并不知道 A 具体是由哪个文件导出的,因而,此时的 webpack 肯定会将 A、B 打包到一个文件中,作为一整个模块,提供给 App。对于腾讯文档,这个状况产生了如下变动(模块依照 1、2、3、4、5 的程序进行加载,模块的视觉大小示意体积大小):

能够看到,在 transpileOnly 开启的状况下,大量的文件被打包到了模块 1 中,被提前加载了。不过,个别状况下,模块被打包到什么地位,并不应该影响代码的体现,不是么?毕竟,敞开 code splitting,代码是能够不拆包的。对于个别的状况而言,这样了解并没有错。然而,对于应用了 decorator 的我的项目而言,就不实用了。

在代码广泛会转为 es5 的时代,decorator 会被转换为一个__decorator 函数,这个函数,是代码加载时的一个自执行函数。如果代码打包的程序产生了变动,那么自执行函数的执行程序也就可能产生了变动。那么,这又如何导致了腾讯文档无奈失常启动呢?这,就要从腾讯文档全面引入依赖注入技术开始说起。

在腾讯文档中,每一个性能都是一个 feature,这个 feature 并不会手动初始化,而是通过一个非凡装璜器,注入到腾讯文档的 DI 框架中,而后,由注入框架进行对立的实例创立。

举个例子,在失常的变一下,由三个 Feature A、B、C,A、B 被编译在模块 1 中,C 被编译到模块 2 中。在模块 1 加载时,workbench 会进行一轮实例创立和初始化,此时,FeatureA 的初始化带来了某个副作用。而后,模块 2 加载了,workbench 再次进行一轮实力创立和初始化,此时 FeatureC 的初始化依赖了 FeatureA 的副作用,然而第一轮初始化曾经完结,因而 C 顺利实例化了。

当 transpileOnly 被开启式,所有变了样,因为无奈辨别导出,Feature A、B、C 被打包到同一个模块了。可想而知,在 Feature C 初始化时,因为副作用尚未产生,C 的初始化就失败了。

既然 transpileOnly 与依赖注入先天不兼容,那笔者就须要想方法修复它。如果,笔者将 app 中的援用进行替换:

// src/app.ts
import {A} from "./base/a";
const a = new A();

模块导出的解析问题,是否就迎刃而解了?不过,这么多的代码,改成这样的援用,岂但难看,反人类,工作量也很大。因而,让笔者设计一个 plugin/loader 组合在编译时来解决问题吧。在编译的初始阶段,笔者通过一个 plugin,对我的项目文件进行解析,将其中的 export 提取进去,找到每一个 export 和文件的对应关系,并储存起来(此处,可能大家会放心 IO 读写对性能的影响,思考到当初开发人均都是高速 SSD,这点 IO 吞吐真的不算什么,实测这个 export 解析 <1s),而后在编译过程中,笔者再通过一个自定义的 loader 将对应的 import 语句进行替换,这样,就能够实现在不影响失常写代码的状况下,放弃 transpileOnly 解析的有效性了。

通过一番折腾,终于,胜利的将腾讯文档在高速编译模式下运行了起来,达到了预约的编译速度。

5.Webpack5 降级之路

5.1. 一些兼容问题解决

Webpack5 毕竟属于一次非兼容的大降级,在腾讯文档编译系统重构的过程中,也遇到诸多问题。

5.1.1. SplitChunks 自定义 ChunkGroups 报错

如果你也是 splitChunks 的重度用户,在降级 webpack5 的过程中,你可能会遇到如下正告:

这个正告的阐明并不是十分明确,用大白话来说,呈现了这个提醒,阐明你的 chunkGroups 配置中,呈现了 module 同时属于 A、B 两组(此处 A、B 是两个 Entrypoint 或者两个异步模块),然而你明确指定了将模块属于 A 的状况。为何此时 Webpack5 会报出正告呢?因为从通常状况来说,module 分属于两个 Entrypoint 或者异步模块,module 应该被提取为公共模块的,如果 module 被归属于 A,那么 B 模块如果独自加载,就无奈胜利了。

不过,一般来说,呈现这样的指定,如果不是配置谬误,那就是 A、B 之间曾经有明确的加载程序。然而这个加载程序,Webpack 并不知道。对于 entrypoint,webpack5 中,容许通过 dependOn 属性,指定 entry 之间的依赖关系。然而对于异步模块,则没有这么遍历的设置。当然,笔者也能够通过自定义插件,在 optimize 之前,对已有的模块依赖关系以及批改,保障 webpack 可能通晓额定的信息:

compiler.hooks.thisCompilation.tap("DependOnPlugin", (compilation) => {compilation.hooks.optimize.tap("DependOnPlugin", () => {forEach(this.dependencies, (parentNames, childName) => {const child = compilation.namedChunkGroups.get(childName);
 if (child) {parentNames.forEach((parentName) => {const parent = compilation.namedChunkGroups.get(parentName);
 if (parent && !child.hasParent(parent)) {parent.addChild(child);
 child.addParent(parent);
 }
 });
 }
 });
 });
});

5.1.2.plugin 依赖的 api 已删除

Webpack5 公布后,各大支流 plugin 都曾经相继适配,大家只有将插件更新到最新版本即可。不过,也有一些插件因为诸多原因,一些插件并没有及时更新。(PS:目前,没有匹配的插件大多曾经比拟小众了。)总之,这个问题是比拟无解的,不过能够适当期待,应该在近期,大部分插件都会适配 webpack5,事实上 webpack5 也是用了不少改名大法,局部接口进行转移,调用形式产生了扭转,倒也没有全副天翻地覆的变动,所以,切实等不及的小插件无妨试试本人 fork 批改一下。

5.2.Module Federation 初体验

通常,对于一个大型项目来说,笔者会抽取很多公共的组件来进步我的项目间的模块共享,然而,这些模块之间,难免会有一些独特依赖,比方 React、ReactDOM,JQuery 之类的根底库。这样,就容易造成一个问题,公共组件抽取后,我的项目体积收缩了。随着公共组件的增多,我的项目体积的收缩变得非常可怕。在传统打包模型上,笔者摸索出了一套简略无效的办法,对于公共组件,笔者应用 external,将这些公共局部抠出来,变成一个残疾的组件。

然而,随着组件的增多,共享组件的 Host 增多,这样的形式带来了一些问题:

  1. Component 须要为 Host 专门打包,它不是一个能够独立运行的组件,每一个运行该 Component 的 Host 必须携带残缺的运行时,否则 Component 就须要为不同的 Host 打出不同的残疾包。
  2. Component 与 Component 之间如果存在较大的共享模块,无奈通过 external 解决。

这个时候,Module Federation 呈现了,它是 Webpack 从动态打包到残缺运行时的一个转变,Module Federation 中,提出了 Host 和 Remote 的概念。Remote 中的内容能够被 Host 生产,而在这个生产过程中,能够通过 webpack 的动静加载运行时,只加载其中须要的局部,对于曾经存在的局部,则不作二次加载。(下图中,因为 host 中曾经蕴含了 jQuery、react 和 dui,Webpack 的运行时将只加载 Remote1 中的 Component1 和 Remote2 中的 Component2。)

也就是说,公共组件作为一个 Remote,它蕴含了残缺的运行时,Host 无需晓得须要筹备什么样的运行时才能够运行 Remote,然而 Webpack 的加载器保障了共享的代码不作加载。如此一来,就防止了传统 external 打包模式下的诸多问题。事实上,一个组件能够同时是 Host 和 Remote,也就是说,一个程序既能够作为主程运行,也能够作为一个在线的 sdk 仓库。对于 Module Federation 的实现原理,此处不再赘述,大家感兴趣能够参考《摸索 webpack5 新个性 Module federation 在腾讯文档的利用》中的解析,也能够多多参考 module-federation-examples 这个仓库中的实例。

Webpack5 的 Module Federation 是依赖于其动静加载机制的,因而,在它的演示实例中,你都能够看到这样的构造:

// bootstrap.js
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
// index.js
import("./bootstrap.js");

而 Webpack 的入口配置,都设置在了 index.js 上,这里,是因为所有的依赖都须要动静断定加载,如果不把入口变成一个异步的 chunk,那如何去保障依赖可能按程序加载内?毕竟实现 Moudle Federation 的外围是 基于 webpack_require 的动静加载零碎。

因为 Module Federation 须要多个仓库的联动,它的推动必然是绝对漫长的过程。那么笔者是否有必要将现有的我的项目间接革新为 index-bootstrap 构造呢?事实上,笔者仍然能够利用 Webpack 的插件机制,动静实现这一个异步化过程:

private bootstrapEntry(entrypoint: string) {const parsed = path.parse(entrypoint);
const bootstrapPath = path.join(parsed.dir, `${parsed.name}_bootstrap${parsed.ext}`);
this.virtualModules[bootstrapPath] = `import('./${parsed.name}${parsed.ext}')`;
return bootstrapPath;
}

在上述的 bootstrapEntry 办法中,笔者基于本来的 entrypoint 文件,创立一个虚构文件,这个文件的内容就是:

import("./entrypoint.ts");

再通过 webpack-virtual-modules 这个插件,在雷同目录生成一个虚构文件,将本来的入口进行替换,就实现了 module-federation 的构造转换。这样,配合一些其余的相应配置,笔者就能够通过一个简略参数开启和敞开 module-federation,把我的项目变成一个 Webpack5 ready 的构造,当相干我的项目陆续适配胜利,便能够一起欢畅的上线了。

6. 后记

对编译的大重构是笔者蓄谋已久的事件,犹记得退出团队之时,第一次接触到编译链路如此简单的我的项目,深感本人的我的项目经验太浅,接触的编译和打包都如此简略,现在亲自操刀才晓得,这两头除了许多技术难题,大我的项目必有的祖传配置也是妨碍我的项目提高的一大阻力。

Webpack 5 的 Beta 周期很长,所以在 Webpack5 公布之后,兼容问题还真不如料想的那么多,不过 Webpack5 的文档有些坑,如果不是应用 NodeApi 的时候,有类型申明,笔者相对无奈发现官网不少文档的给的参数还是 webpack4 的旧数据,对不上号。于是不得不埋着头调试源代码,寻找正确的配置形式,不过也因而播种良多。

同时 Webpack5 还在继续迭代中,还存在一些 bug,比方 Module Federation 中应用了不失当的配置,可能会导致奇怪的编译后果,并且不会报错。所以,遇到问题大家要大胆的提 issue。本次重构的教训就临时说到这儿,如有不当之处,欢送斧正。

最初,腾讯文档大量招人,如果你也想来钻研这么乏味的技术,欢送退出腾讯文档的小家庭,欢送分割笔者 glendonzli@qq.com。


AlloyTeam 欢送优良的小伙伴退出。
简历投递: alloyteam@qq.com
详情可点击 腾讯 AlloyTeam 招募 Web 前端工程师

正文完
 0