大家好,我是Webpack,AKA打包老炮,我的slogan是:“打天下的包,让Rollup无包可打”。
今天我要带来的才艺是:剖析打包的艺术
故事还要从一次npm包工头拉我进项目开始…
那是一个阳光明媚的凌晨,我的眼前是一个正在脱头发的精神程序小哥。他写了一个很简单的webpack+vue项目,企图通过这个项目来了解我的全部。
他的要求很过分,但…我答应了,因为从我出生那天开始,我就发四要让每一个前端靓仔深深的爱上我,或许这就是该死的爱情。
在办事之前我想先给家人们介绍一下我们穷士康的哥哥姐姐们。
他们是我最爱的员工,个个都是人才!!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6XycNav5-1659080796322)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef18b7a70d01460a8d3428cb01ca8574~tplv-k3u1fbpfcp-watermark.image?)]
那开始吧,用力敲下npm run build。我便奋不顾身裸露在你面前。
随着 npm run build回车执行,正题开始:
通常下面的代码可能手脚架搭建出来的项目都已经写好了,执行npm run build命令就执行了;但是呢,也有个别的靓仔喜欢特立独行,年轻人嘛!
var webpack = require('webpack')
var config = require('./webpack.config') // 自定义配置
webpack(config, (err, stats) => {}) // 调用我我就干活
我会从下面三个地方结合生成最终的配置清单。【优先级也从高到低】
接下来就进入了工厂的准备阶段【webpack()函数执行】
我是老板,我只看不干!
于是我把这次的整合好的任务清单【options】给到厂长【Compiler】
compiler = new Compiler(options.context);
叫上工厂内部机动组人员【内置插件】以及外部机动组人员 【自定义配置引入的外部插件】来登记一下【注册插件】,以便干活的时候联系。
例如:
机动组人1号【SingleEntryPlugin插件】留了个电话make【钩子:make】。到时候我们打给make的时候,机动组人1号【SingleEntryPlugin插件】就能够接到电话然后干专属自己的活。当然可以多个人留同一个电话,到时候也是都能接听到。
毕竟科技改变生活嘛,联想该学学了!
// 注册配置文件的插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 注册内置插件
// 内部插件之多,令人发指。这也进一步说明了我的设计真的很强,功能解耦,拓展性...令人敬佩。不愧是我
// 此外还处理了devtool相关逻辑
new WebpackOptionsApply().process(options, compiler);
厂长这个时候出来喊话了:
“comeon bady,everybody move!”
【compiler.run()/compiler.watch() ,且都会再调用compiler.compile()】
此话一出,各位打工人便开始忙碌起来!
接着上图的代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3WCSCLVd-1659080796328)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0651e05ddccd4038b3c195e89086eb5d~tplv-k3u1fbpfcp-zoom-1.image)]
贴一下compiler.js中的compile()函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-90Ffpfwc-1659080796355)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b17a8d52175d4771aec4fd6784e8599c~tplv-k3u1fbpfcp-zoom-1.image)]
该函数做了两个重要的事:
至此,我们称上面的工作为工厂的准备阶段【初始化阶段】,
下面我们来总结下工厂准备阶段做了些什么。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kPvLvuby-1659080796357)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4627c25d1fa34416a1bdabecd075d5d4~tplv-k3u1fbpfcp-zoom-1.image)]
用厂里的话总结:
tips:过程中有钩子触发environment,afterEnvironment等,只说对我们重要的钩子
tips:webpack中的钩子,有点类似于发布订阅模式,但更加的强耦合,因为这些订阅者能够影响打包的流程,发布者在通知订阅者时还把compiler、Compilation这些重要的构建上下文作为参数传递过去。这个事件机制是基于Tapable实现,非常值得学习的一种设计。
到这里厂里的准备工作已经告一段落,厂长【Compiler】以及打工人【Compilation】已经就位【实例化】
还记得我们的宗旨吗?
“帮助客户把一堆文件整合成另一堆好一点的文件”
在拨打“make”号码后,机动组SingleEntryPlugin员工接到电话,此时他不慌不忙的通知打工人【Compilation】从客户给的一堆文件中,根据任务清单【options】中找到入口,从这里开始整理客户提供的一大堆文件【Comilation.addEntry()】。
所以说机动组员工登记要趁早【注册插件】,不然打电话找不到人就麻烦了。
所以说员工按部就班,规规矩矩也是老板的福报。
贴一下SingleEntryPlugin插件的代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fFdZLMzU-1659080796358)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4d6428bd117b42799d80b598b9dfbb51~tplv-k3u1fbpfcp-zoom-1.image)]
总结一下:初始化阶段注册了SingleEntryPlugin插件,监听make钩子,监听到则触发compilation.addEntry()从入口文件开始打包。
这个过程不简单,函数与函数之间基本都是通过callback回调,并且还有很多监听钩子的插件触发回调,所以这里需要很耐心很耐心才能够理解。
厂言厂语总结:厂长【Compiler】打电话【hooks:make】,机动组SingleEntryPlugin接到电话,通知打工人【Compilation】从入口开始整理文件【Compilation.addEntry()】
新介绍一下厂里的材料:
打工人【Compilation】在拿到入口的盒子标签后【Compilation.addEntry(dependency)】,会创建一个对应的盒子【Compilation.handleModuleCreation(dependency)】,紧接着Compilation会对这个盒子装着的文件进行解析,分析盒子有没有依赖其他的盒子,有的话就在这个盒子上面贴上这些盒子标签。
想了很久,觉得还是有必要给你看看当时的情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rBMJuFWU-1659080796359)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f505f83d1d3d4a5293f5bb69736d5856~tplv-k3u1fbpfcp-watermark.image?)]
这个盒子分析完之后呢,打工人会看看盒子上面有没有贴着盒子标签,有的话就又创建对应新的盒子了。
接下来盒子是如何分析以及打上盒子标签的…
到这里我需要再给大家介绍下厂里的其他成员:
趁这次机会想给各位员工再培训一下,知己知彼,百战不殆!
“各位员工好,大家都知道我们工厂服务对象是前端靓仔,而且我们的工厂是把前端靓仔提供的文件变成另一堆更合理的文件。”
“那各位知道为什么我们是怎么做到的吗?"
”你们说得对,首先客户文件先转成JavaScript资源,我们配置了很多加载机器【Loader】,能够处理各种类型文件,变成Javascript“
”变成JavaScript之后呢?我们需要分析文件内容,这样我们才知道客户提供的文件哪些是要拆开的,哪些是要合并的,哪些是没用要去掉的,文件之前是怎么关联的。那怎么分析文件内容呢?“
这个时候解析员走了出来说到:
”是AST,我用了AST! 我将JavaScript变成AST之后,去分析里面的(import/require)关键字,如果发现了就会打电话号码”exportImportSpecifier“【触发hooks:exportImportSpecifier】去通知机动人员HarmonyExportDependencyParserPlugin“,
”据我所知,这个机动组人员就是在盒子上打上盒子标签【Dependency】的那个人“。
话音刚落,海归解析员嘴角止不住微微扬起,内心止不住想:还有谁?
md,厂长【Compiler】忍不了别人装b,手里拿着两张工作流程图走了出来,里面有各个角色的分工以及流程。看完图纸我笑了,不愧是厂长,稳如老狗。还是我最信任的那个男人!
这两个图纸要细看,厂里最值钱的东西了。
简易版:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AwoZhrI6-1659080796359)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f18252639a424d498b06c5aa92254d92~tplv-k3u1fbpfcp-zoom-1.image)]
详细版:【这张图凝聚了我一个寂寞夜深的心血】
一定要看这个图,细品
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Wtyaqnb-1659080796360)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a37acd62b0744ee691f1ff20a933da73~tplv-k3u1fbpfcp-zoom-1.image)]
所以提个问题?
”入口文件index.js内导入了a.js,那我们工厂是怎么知道他们是有这样的引用关系的呢?打工人【Compilatin】,你站起来说一下“
打工人【Compilatin】也站了起来,忍不了高材生装b的看来不止厂长一个,说道:
“
说完,全场掌声雷动,精彩的演讲!堪称前端特冷普。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nF05XN2A-1659080796361)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f67c5a44041d4fd180e09b6a6e420f55~tplv-k3u1fbpfcp-watermark.image?)]
那么你如何理解module顺藤摸瓜的过程?
compilation
module
Parser
例如以下文件结构,index.js作为入口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-09q746Wq-1659080796361)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33d6b090f4334abe9ebf6fb9dea92e5d~tplv-k3u1fbpfcp-zoom-1.image)]
此时通过Compilation.addEntry()找到index.js,导入成module对象,同时分析得出module的依赖列表有left.js以及right.js
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8C9Prpjd-1659080796362)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a1eab9183294449fb483b0718205cb6d~tplv-k3u1fbpfcp-zoom-1.image)]
此时遍历依赖列表[dependency-left.js, dependency-right.js],创建对应的module-left.js、module-right.js
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eVUqxGrD-1659080796363)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1375f461edc4ef3b7bf32798bb00f0c~tplv-k3u1fbpfcp-zoom-1.image)]
再遍历下一批依赖
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSk2GdVb-1659080796363)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47e06c222d424e8f96d81afdc899d88f~tplv-k3u1fbpfcp-zoom-1.image)]
再看下完整的文件构建流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9vNsR9Yv-1659080796364)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd17d48f27ef480c9f86ccaa03e3a34e~tplv-k3u1fbpfcp-zoom-1.image)]
至此构建阶段告一段落。在文件盒都准备好之后,则进入下一步
先思考一个问题:我们为什么要装箱?
如果将我们的开发文件1比1的打包到生产环境上的话,数量上是非常大的,这会导致浏览器发起的http请求资源次数则需要非常频繁,可能一个module就一两行代码,我们依然要发起一个http去请求它回来。而浏览器对同时能发起的http数量是有限制的,例如chrome就是最多同时6个请求,所以最后放到的生产的文件不能太多,不能太小,也不能太大。我们有时候看到的各种打包优化策略,也是朝着这个目标尽量实现。
所以我们需要将他们合并组成一个一个块,以达到目的。
这里我们再介绍一个我们的厂的成员:
在上面阶段整理出来那么多的文件盒【module】,我们会先把他们装箱【chunk】再交付客户【输出文件】。
这些箱子里面装的都是module
在递归生成所有的文件盒【module】之后,打工人【Compilation】就开始装箱操作了【compilation.seal()】【module分配到chunk】。
其实我也一直疑惑,打工人【Compilation】是怎么装箱的,例如要使用到多少个箱子?哪些文件盒放到哪些箱子?这其中必然有功夫。
于是我以老板的身份要求打工人说出自己的秘籍!
打工人【Compilation】开始巴拉巴拉:“除非升职加薪,否则不说!”
我:“最近公司准备优…”,话还没说完,
打工人准备全盘托出:
“首先我自己有一套默认装箱的规则”
动态依赖的分包规则补充一个点:
例如:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X8qjawBW-1659080796365)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f7ce5e1bdafd44ab8743d5970de600a2~tplv-k3u1fbpfcp-zoom-1.image)]
那么一个箱子装【index.js+right.js】,另一个箱子装【left.js + left-1.js + left-2.js】.而不是另一个箱子只装【left.js】喔!
装箱逻辑的实现在不同的webpack版本中差异还是比较大的:
〇 webpack4的时候还是相对简单一点点,装箱逻辑直接利用module以及dependency之间相互引用的信息,
〇 webpack5则做了升级【将module与dependency之间的依赖关系拆分的更加细致】:
增加了下面这些对象
ChunkGraph:所有关于模块如何与块连接的信息现在都存储在 ChunkGraph 类中
ModuleGraph:所有关于模块如何在模块图中连接的信息现在都存储在 ModuleGraph 类中。
ModuleGraphConnection:记录模块间的引用以及被引用的关系;originModule字段记录被引用的module,module字段表示自己
ModuleGraphModule:incomingConnections字段记录自己的ModuleGraphConnection,outgoingConnections字段记录依赖的module的ModuleGraphConnection
webpack5正是将module与depenency的关系拆分的如此细致,加上在seal()阶段做了更多的优化,webpack5在打包性能上面又进步了不少。
例如:
这里再介绍两位帮助优化装箱的工厂成员:
打工人【Compilation】把老底都说出来时候,眼泪忍不住从眼角流出,向生活低下了高贵的头颅。
打工人【Compilatin】继续说道:
“除了内置的装箱规则,客户也可以点名叫机动组的人帮他们分包【插件-上面提到的两位大师】”
语气里多少都带点小脾气。
“还有就是机动组员【SplitChunksPlugin】会在我装箱之后,再偷偷摸摸再搞一遍装箱,客户自定义的分包规则就是在这里起作用的,当然客户不自定义,罗老师也有自己的一套默认规则”
罗老师的默认配置
module.exports = {
optimization: {
//罗老师的默认规则,webpack4跟webpack5默认好像不一样
splitChunks: {
chunks: 'async', // 2. 处理的 chunk 类型
minSize: 20000, // 4. 允许新拆出 chunk 的最小体积
minRemainingSize: 0,
minChunks: 1, // 5. 拆分前被 chunk 公用的最小次数
maxAsyncRequests: 30, // 7. 每个异步加载模块最多能被拆分的数量
maxInitialRequests: 30, // 6. 每个入口和它的同步依赖最多能被拆分的数量
enforceSizeThreshold: 50000, // 8. 强制执行拆分的体积阈值并忽略其他限制
cacheGroups: { // 1. 缓存组
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 1.1 模块路径/文件名匹配正则
priority: -10, // 1.2 缓存组权重
reuseExistingChunk: true, // 1.3 复用已被拆出的依赖模块,而不是继续包含在该组一起生成
},
default: {
minChunks: 2, // 5. default 组的模块必须至少被 2 个 chunk 共用 (本次分割前)
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
这里没啥好说的,打工人【Compilation】装箱之后【compilation.seal()完成】,厂长【Compiler】就把箱子给到客户了【compiler.emiAssets()将chunk生成一个个文件】
就这样日复一日,年复一年,我们厂为所有一线奋斗的客户解决模块化打包的问题。渐渐的有些客户想了解我们工厂内部的运作原理,为此我作为工厂老板,势必要为我们穷士康写下这么一篇文章,让更多的人能够了解我们。但苦于才疏学浅,文笔功力有限;且厂里部门繁多,各种人情世故暂时也只能写下这么一篇水平有限的文章。
不到之处望海涵~
看到文章的人今年必定升值加薪!
点个赞吧👍【肮脏手段:不点赞不是中国人】