Webpack 的 Chunk,想怎么分就怎么分

news2024/11/18 19:34:31

想必大家都用过 webpack,也或多或少了解它的原理,但是不知道大家有没有写过 Webpack 的插件呢?

今天我们就一起来写一个划分 Chunk 的 webpack 插件吧,写完后你会发现想怎么分 Chunk 都可以!

首先我们简单了解下 webpack 的原理:

webpack 的原理

webpack 是一个打包工具(bundler),它打包的是什么呢?

模块。

那模块能再拆分么?

不能了,模块是 webpack 处理的基本单位了,只是对模块做一些打包。

那怎么对模块打包呢?

首先要找到所有的模块(Module),从入口模块开始,分析依赖,构成一个图,叫做模块依赖图(ModuleGraph)。

然后模块要分成几个包,要有一种中间结构来保存这种划分,叫做 Chunk。把不同的模块放到不同的 Chunk 里就完成了分包。

但是 Chunk 只是一种中间结构,还要再变成可用的目标代码。通过代码模版把它打印成代码就可以了。

这三步分别叫 make、seal、emit。

make 这一步就是构建模块依赖图 ModuleGraph 的,这个过程中会从入口模块(EntryPoint)开始递归解析依赖,对解析出的每个模块做处理,也就是调用注册的 loader。

然后 Seal 也就是封装的意思,把不同的 Module 分到不同的 Chunk 里。

这一步会先做基础的 Chunk 划分,比如入口模块 EntryPoint 肯定要单独放到 Chunk 里,动态引入的模块肯定也要单独放到 Chunk 里。

完成了基础的划分之后,可以再对这些 Chunk 做进一步的优化划分,比如根据 Chunk 大小等来划分。

分完之后,ModuleGrapqh 就变成了 ChunkGraph。

最后 emit 阶段就是通过模版打印代码了。

这三步合起来就是一次编译过程 Compilation。

编译过程由 webpack 的 Compiler 调用。

整个过程中还会暴露出很多扩展点,也就是留给插件的 hook,不同阶段的 hook 自然就可以拿到不同阶段的资源。

这些插件都是保存在对象上的:

比如 compiler 的 hook:

compilation 的 hook:

那插件里自然就是往不同对象的 hook 上添加回调函数:

而且 webpack 为了控制 hook 的执行顺序,封装了一个 tappable 的包。可以指定 hook 是同步、异步,并行、串行执行。

比如这几种 hook:

SynHook 就是同步顺序执行。

AsyncSeriesHook 就是异步串行执行。

SyncBailHook 也是同步顺序执行,但是如果中间的 hook 返回 false 就会停止后续 hook 的执行,也就是可以熔断。

理解了 webpack 的编译流程,hook 的运行机制,接下来我们就写个插件来操作下 Chunk 吧:

操作 Chunk 的 webpack 插件

前面讲过,webpack 会对 Module 根据是否是入口模块、是否是异步引入的模块做基础的 Chunk 划分。

之后会进一步做优化的 Chunk 划分。

这些 chunk 相关的逻辑都是在 seal 那一步做的。

我们在源码里看到的也确实是这样:

在 seal 里做了 ChunkGraph 的创建,然后调用 optimizeChunks 的 hook 对 Chunks 做处理。

这里为啥是个死循环呢?

记得上面说过一种 hook 类型叫 SyncBailHook 么?

也就是同步执行插件,但是可以插件可以返回 false 熔断后面插件的执行。

这里的 hook 就是同步熔断 hook:

那我们就开始在这个 hook 里写一些逻辑吧:

class ChunkTestPlugin {constructor(options) {this.options = options || {};}apply(compiler) {const options = this.options;

	compiler.hooks.thisCompilation.tap("ChunkTestPlugin", compilation => {compilation.hooks.optimizeChunks.tap("ChunkTestPlugin", chunks => {return true;});});}
}

module.exports = ChunkTestPlugin; 

把 options 挂到 this 上。

然后注册一个 optimizeChunks 这个 hook 的回调。

为啥外面还要加一层 compiler 的 hook 呢?

因为你得在 compiler 刚开始编译的时候去注册 compilation 的 hook 呀!不然就晚了。

可以看到 thisCompilation 是在 newCompilation 这个方法调用的。

而 newCompilation 是在 make、seal、emit 的流程开始之前调用的:

也就是说在 thisCompilation 的 Compiler hook 里注册的 Compilation hook 就可以在这次编译过程中生效。

有的同学说,那还有另一个 hook 是干啥的呢?

这俩 hook 唯一的区别是当有 child compiler 的时候,compilation 的 hook 会生效,而 thisCompilation 不会。

而我们是想在这个 hook 里注册 Compilation 的 hook 的,全局只需要执行一次就行,所以用 thisCompilation 的 Compiler hook。

我们在项目里用一下:

这个项目有三个入口模块:

pageA:

require(["./common"], function (common) {
	common(require("./a"));
}); 

pageB:

require(["./common"], function(common) {
	common(require("./b"));
}); 

pageC:

require(["./a"], function(a) {
	console.log(a + require("./b"));
}); 

这三个模块里都通过 requrie() 或者 import() 的 webpack api 来动态引入了一些模块。

动态引入的模块分别是:

a:

module.exports = "a"; 

b:

module.exports = "b"; 

common:

module.exports = function (msg) {console.log(msg);
};

function hello() {return "guangguangguangguangguangguanggua"
	 + "ngguangguangguangguangguangguangguangguangg"
	 + "uangguangguangguangguangguangguangguangguangg"
	 + "uangguangguangguangguangguangguangguangguang"
	 + "guangguangguangguangguangguangguangguangguangguang"
	 + "guangguangguangguangguangguangguangguangguangguangguang";
} 

webpack 会从入口模块开始构建 ModuleGraph,然后划分 Chunk,构成 ChunkGraph。

大家觉得这几个模块会分几个 Chunk 呢?

6 个。

因为入口模块要用单独的 Chunk,而且异步引入的模块也是单独的 Chunk。

打个断点看一下就知道了:

确实,到了 optimizeChunks 这一步,拿到的是 6 个 Chunk:

分别是 3 个入口,以及每个入口用到的异步模块。

在这个 optimizeHook 的插件里,我们就可以自己做一些 Chunk 拆分了。

chunkGroup 有一个 integrateChunks 的 api,把后面的 chunk 合并到前面的 chunk 里:

我们调用 integrateChunks 进行 chunk 合并,然后把被合并的那个 chunk 删掉即可。

那怎么找到 a 和 b 两个 chunk 呢?

两层循环,分别找到两个不想等的 chunk 进行合并即可:

我们只取第一组 chunk 进行合并,合并完如果还有就返回 true,继续进行下次合并。

合并完之后记得 return false,因为外面是一个 while 循环,不 return false,就一直死循环。

先试一下现在的效果:

不引入插件的时候是这样的:

3 个入口 chunk,3 组入口 chunk 的异步引入的模块。所以产生了 6 个文件。

入口 chunk 对应的文件里引入异步模块的方法变成了 webpack runtime 的 _webpack_require.e

而它引入的异步 chunk 里就如前面分析的,包含了这个模块的所有异步依赖:

分别是 a + common,b + common,a + b,也就是每个入口模块依赖的所有异步模块。

那优化之后呢?

都放到一个 chunk 里了:

这倒是符合我们写的逻辑,因为两两合并,最后剩下的肯定只有一个。

但这样显然不大好,因为每个页面是独立的,应该分开,但是异步的 chunk 倒是可以合并。

所以我们优化一下:

调用 chunk 的 isInitial 方法就可以判断是否是入口的 chunk,是的话就跳过。

这样就只合并了异步 chunk。

效果是这样的:

3 个入口 chunk 的依赖也变成这个 chunk 了:

那如果我要根据 chunk 大小来优化呢?

那就可以判断下 a、b 的 chunk 的大小和合并之后的 chunk 大小,如果合并之后比合并前小很多,就合并。

当然,不同的 chunk 合并效果是不一样的,我们要把所有的合并效果下来:

通过 chunkGraph.getChunkSize 的 api 拿到 chunk 大小,通过 chunkGroup.getIntegratedChunkSize 的 api 拿到合并后的 chunk 大小。

记录下合并的两个 chunk 合并的收益。

做个排序,把合并收益最大的两个 chunk 合并。

返回 true 来继续循环进行合并,直到收益小于 1.5,那就 return false 停止合并。

当然,这个 1.5 也可以通过 options 传进来。

效果是这样的:

两个异步 chunk 分别为:

a + b + common:

a + b:

也就是说只把之前的 a + common 和 b + common 合并了,因为 common 模块比较大,所以合并之后的收益是挺大的。

这样就完成了 chunk 拆分的优化。

有的同学说,我平时也不用自己写插件来拆分 chunk 呀,webpack 不是提供了 SplitChunksPlugin 的插件么,还变成内置的了,配置下 optimization.splitChunks 就行。

没错,webpack 默认提供了拆分 chunk 的插件。

那这个插件是怎么实现的呢?

没错,SplitChunkPlugin 的实现原理就是我们刚才说的这些,注册了 optimizeChunks 的 hook,在里面做了 chunk 拆分:

它可以根据配置来拆分 chunk,但是终究是有局限性的。

如果某种 chunk 拆分方式它不支持呢?

我们就可以写插件自己拆分了,会自己拆分 chunk 之后,还不是想怎么分就怎么分么!

我们写的这个 webpack 插件的全部代码如下:

class ChunkTestPlugin {
	constructor(options) {
		this.options = options || {};
	}

	apply(compiler) {
		const options = this.options;
		const minSizeReduce = options.minSizeReduce || 1.5;

		compiler.hooks.compilation.tap("ChunkTestPlugin", compilation => {
			compilation.hooks.optimizeChunks.tap("ChunkTestPlugin", chunks => {
				const chunkGraph = compilation.chunkGraph;

				let combinations = [];
				for (const a of chunks) {
					if (a.canBeInitial()) continue;
					for (const b of chunks) {
						if (b.canBeInitial()) continue;
						if (b === a) break;

						const aSize = chunkGraph.getChunkSize(b, {
							chunkOverhead: 0
						});
						const bSize = chunkGraph.getChunkSize(a, {
							chunkOverhead: 0
						});
						const abSize = chunkGraph.getIntegratedChunksSize(b, a, {
							chunkOverhead: 0
						});
						const improvement = (aSize + bSize) / abSize;

						combinations.push({
							a,
							b,
							improvement
						});
					}
				}

				combinations.sort((a, b) => {
					return b.improvement - a.improvement;
				});

				const pair = combinations[0];

				if (!pair) return;
				if (pair.improvement < minSizeReduce) return;

				chunkGraph.integrateChunks(pair.b, pair.a);
				compilation.chunks.delete(pair.a);
				return true;
			});
		});
	}
}

module.exports = ChunkTestPlugin; 

总结

webpack 的处理单位是模块,它的编译流程分为 make、seal、emit:

  • make:对入口模块分析依赖,构建 ModuleGraph,对每个模块调用 loader 处理。
  • seal:合并 Module 为 Chunk,合并之后 ModuleGraph 会变为 ChunkGraph。
  • emit:对每个 Chunk 通过模版打印成代码后输出

这个编译流程中有很多 hook,通过 tappable 的 api 组织,可以控制回调的同步、异步、串行、并行执行。

我们今天写的 Chunk 拆分插件,就是一个 SyncBailHook,同步熔断的串行 hook 类型,也就是前面回调返回 false 会终止后面的回调执行。

首先在 compiler 的 thisCompilation 的 hook 里来注册 compilation 的 optimizeChunks 的 hook。

在 optimizeChunks 的 hook 里可以拿到所有的 chunk,调用 chunkGraph 的 api 可以进行合并。

我们排除掉了入口 chunk,然后把剩下的 chunk 根据大小进行合并,达到了优化 chunk 的目的。

webpack 内置了 SplitChunksPlugin,但是毕竟有局限性,当不满足需求的时候就可以自己写插件来划分 chunk 了。

自己来控制 Chunk 划分,想怎么分就怎么分!

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/183878.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图像配准:基于 OpenCV 的高效实现

在这篇文章中&#xff0c;我将对图像配准进行一个简单概述&#xff0c;展示一个最小的 OpenCV 实现&#xff0c;并展示一个可以使配准过程更加高效的简单技巧。什么是图像配准图像配准被定义为将不同成像设备或传感器在不同时间和角度拍摄的两幅或多幅图像&#xff0c;或来自同…

什么牌子的护眼灯最好推荐?盘点口碑好的护眼灯品牌

护眼灯是目前大部分家庭都在使用的灯具之一&#xff0c;利用光源起到保护视力的效果&#xff0c;预防近视&#xff0c;可谓是现代生活中伟大的发明&#xff0c;今天由小编来列出优秀的护眼灯品牌&#xff0c;并详细的介绍&#xff0c;告诉大家哪个护眼灯品牌好。① 南卡护眼台…

【微信小程序-原生开发】实用教程07 - Grid 宫格导航,详情页,侧边导航(含自定义页面顶部导航文字)

开始前&#xff0c;请先完成成员页的开发&#xff0c;详见 【微信小程序-原生开发】实用教程 06-轮播图、分类页签 tab 、成员列表&#xff08;含Tdesign升级&#xff0c;切换调试基础库&#xff0c;设置全局样式&#xff0c;配置组件按需注入&#xff0c;添加图片素材&#x…

Canal快速入门

Canal 一、Canal 入门 1.1、什么是 Canal ​ 阿里巴巴 B2B 公司&#xff0c;因为业务的特性&#xff0c;卖家主要集中在国内&#xff0c;买家主要集中在国外&#xff0c;所以衍生出了同步杭州和美国异地机房的需求&#xff0c;从 2010 年开始&#xff0c;阿里系公司开始逐步…

PaddleSeg图像分割预测pyinstaller打包报错:No module named ‘framework_pb2‘,问题解决

报错 在使用PaddlePaddle的FastDeploy部署图像分割时&#xff0c;需要验证目标电脑环境&#xff0c;所以先将预测代码predict.py用pyinstaller打包来试试&#xff0c;指令&#xff1a; pyinstaller -D predict.py 打包完成&#xff0c;成功生成exe 运行时报错如下&#xff1a;…

项目五linux 内网完整渗透测试实例笔记

1.DDD4 靶场介绍本靶场存在三个 flag 把下载到的虚拟机环境导入到虚拟机&#xff0c;本靶场需要把网络环境配置好。1.1.网络示意图2. 信息收集2.1.主机发现sudo netdiscover -i eth0 -r 192.168.1.0/242.2.nmap 主机发现nmap -sn 192.168.1.0/242.3.masscan 端口探测sudo massc…

Python批量改文件名

对以下路径中的文件名批量修改。 一、读取指定路径中的文件名 #导入标准库 import os#读取文件名 filesDir "路径……" fileNameList os.listdir(filesDir)#输出路径中的所有文件 for filename in fileNameList:print(filename)二、正则表达式提取需要保留的部分 …

[Vulnhub] DC-6

下载链接&#xff1a;https://download.vulnhub.com/dc/DC-6.zip 知识点&#xff1a; wordpress-getshellnc反弹shell横向越权nmap提权 目录 <1> 信息搜集 <2> wordpress_Activity monitor插件rce漏洞getshell <3> Privilege Escalation&#xff08;nm…

windows上安装hadoop

下载与安装 hadoop下载官网 清华大学镜像下载 进入官网点击Binary download是运行在windows上的。在Apache里面下东西经常会有binary和source的版本&#xff0c;binary是编译好的可以直接使用&#xff0c;source是还没编译过的源代码&#xff0c;需要自行编译。 镜像下载 选…

OSERDESE3与ODELAYE3原语

SelectIO 接口 UltraScale 架构的器件 包括三种I/O&#xff1a;高性能(HP), 高密度 (HD),和高量程(HR) I/O banks 。 HP I/O banks满足高速存储和片到片接口性能要求&#xff0c;电压最高1.8V。HR I/O banks支持更大范围电压&#xff0c;电压最高3.3V。HD I/O banks支持低速接…

Day872.事务间是否需要隔离 -MySQL实战

事务间是否需要隔离 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于事务间是否需要隔离的内容。 创建的视图是静态视图&#xff0c;当前视图对应的数据由字段的当前值加上回滚段计算得到。 事务隔离级别 的时候提到过&#xff0c;如果是可重复读隔离级别&#xff…

1457. 二叉树中的伪回文路径

1457. 二叉树中的伪回文路径题目算法设计&#xff1a;深度优先搜索题目 传送门&#xff1a;https://leetcode.cn/problems/pseudo-palindromic-paths-in-a-binary-tree/ 算法设计&#xff1a;深度优先搜索 如何判断一组数字是否存在一个回文串组合&#xff1f; 如 [1, 2, 1]…

css元素显示模式(行内、块级、行内块)

1.块级元素 显示特点&#xff1a; 1、独占一行&#xff08;一行只能显示一个&#xff09; 2、宽度默认是父元素的宽度&#xff0c;高度默认由内容撑开 3、可以设置宽高 代表标签&#xff1a; div、p、h系列、ul、li、dl、dt、dd、form、header、anv、footer <style>div…

电脑数据怎么迁移?6种旧电脑数据传输到新电脑方法分享

如今&#xff0c;我们拥有如此多的设备&#xff0c;从一个设备跳到另一个设备似乎是一项艰巨的任务。平均而言&#xff0c;一个人可能拥有一台电脑、一部智能手机和一台平板电脑。但是&#xff0c;如果您有多台计算机或要换一台新计算机怎么办&#xff1f;您可能在互联网上问过…

电脑录像软件推荐?分享3款,简单好用且方便

​在日常生活中&#xff0c;我们经常会遇到临时有事情需要外出处理的时候&#xff0c;但在如果正好在上网课或者开会议、听讲座的时候&#xff0c;这时候外出很容易会错过一些重要的内容。这个时候&#xff0c;就需要借助电脑录像软件了。电脑录像软件推荐什么&#xff1f;今天…

Docker consul

目录 一、Docker consul简介 二、Consul优势 三、Consul中的概念 四、部署 1、consul服务器配置 2、查看集群信息 3、在浏览器上进到consul的界面进行管理 4、容器服务自动注册到consul集群 5、consul节点配置nginx 6、配置模板信息 7、配置并启动consul-template …

Java创建pdf的代码

一、概述 以下代码可以在指定文件夹内创建一个简历pdf。 以下代码生成pdf&#xff0c;主要是设置cell所占的行、列、内容。 二、代码 1.需要的jar包 itext-asian-5.2.0.jar itextpdf-5.5.5.jar2.个人信息类MsgUtil.java 这个类里面放了个人信息&#xff1b;也可以放多个人…

Python量化投资——股票择时到底能否赚钱?TA-Lib中33种技术指标有效性回测研究

TA-Lib中33种技术指标回测研究Python量化投资——TA-Lib中33种股票择时技术指标的有效性研究为什么要做这个评测技术指标清单评测方法评测工具期待你的意见Python量化投资——TA-Lib中33种股票择时技术指标的有效性研究 为什么要做这个评测 技术指标是股票交易中最常用的技术…

CSS 常见布局

文章目录CSS 常见布局单列布局单列布局&#xff08;不通栏&#xff09;单列布局&#xff08;通栏&#xff09;双列布局floatoverflow:hiddenflexgridCSS 常见布局 单列布局 单列布局&#xff08;不通栏&#xff09; <!DOCTYPE html> <html><head><meta …

推荐系统之推荐中心逻辑

5.5 推荐中心逻辑 学习目标 目标 无应用 无 5.5.1 推荐中心作用 推荐中一般作为整体召回结果读取与排序模型进行排序过程的作用&#xff0c;主要是产生推荐结果的部分。 5.5.2 推荐目录 server目录为整个推荐中心建立的目录 recall_service.:召回数据读取目录reco_centor:推…