轻松学会webpack核心原理-无代码,适合新手小白

298 阅读9分钟

image.png

前言

webpack发展至今,功能已经变得非常庞大,包括:模块打包、代码分割、按需加载、HMR、Tree-shaking、文件监听、sourcemap、Module Federation、devServer、DLL、多进程等等,看起来很复杂,其实可以将webpack整个庞大的体系抽象为三方面的知识:

1、核心构建流程

2、资源加载loader

3、打包优化plugin

我们可以用一张图来简单描述一下:核心的构建流程将各类资源转换为浏览器能识别的资源资源文件,loader负责加载各类资源,将资源格式转化为主流程能够识别的文件格式,plugin则负责在构建的整个过程中优化打包过程。

image.png

名词解释

如果你是新手,对本文中的一些名词含义不了解,可以仔细阅读下面的名词解释模块,熟悉webpack的可以略过本模块:

Entry:编译入口,webpack编译的起点,用户手动配置

Compiler:编译管理器,webpack启动后会创建compiler对象,该对象一直存活直到打包过程结束

Compilation:单次编辑过程的管理器,比如watch=true时,运行过程中只有一个compiler,但每次文件变更触发重新编译时,都会创建一个新的compilation对象

Dependence:依赖对象,webpack基于该对象记录模块之间的依赖关系

Module:webpack内部所有资源都会以module对象形式存在,所有关于资源的操作、转译、合并都是以module为基本单位进行的(各类资源文件=>ast=>module)

Chunk:编译完成准备输出时,webpack会将module按特定的规则组织成一个一个的chunk,这些chunk某种程度上跟最终输出一一对应

Loader:资源内容转换器,其实就是实现从内容A转换B的转换器

Plugin:webpack构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程 webpack 编译过程都是围绕着这些关键对象展开的,更详细完整的信息,可以参考Webpack知识图谱。

loader与plugin的作用与区别

这里单独将loader和plugin拿出来说,是因为这两个模块是webpack最重要的两个模块,可谓是左膀右臂,但新手经常在概念上搞混他们。

1、loader--静态资源加载器、加载各类资源文件,其功能单一,负责加载各类资源文件;

2、plugin--插件系统、负责优化打包过程、例如删除chunk文件中的公共代码,压缩图片等,你想实现的任何功能,都可以通过plugin插件系统实现。

loader和plugin这两个模块的作用了解就可以了,需要知道的是你可以通过配置文件,利用所有写好的loader来帮你加载转换各类资源文件,可以利用现成的plugin插件来优化你的打包过程,这些都需要经验的积累。另外,你还可以通过自定义插件,来优化自己的打包过程!

tips:webpack利用的好,可以大大提升前端项目的性能哦!

了解了webpack的左膀右臂之后,让我们来关注一下核心的打包过程:

构建过程

webpack的打包构建过程完成了内容转换+资源合并两种功能,实现上包含三个阶段:初始化构建配置、构建模块依赖、生成chunk文件,首先我们来总体了解一下:

1、初始化构建配置

初始化参数:从配置文件webpack.config.js、配置对象、Shell参数(命令行)中读取用户的配置参数,结合webpack默认配置,得出最终的配置参数

创建编译器对象:使用配置参数创建Compiler对象

初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化RuleSet集合、加载配置的插件等

开始编译:执行compiler对象的run方法

确定入口:根据entry入口文件,调用compilition.addEntry将入口文件转换为dependence对象,找出模块之间的依赖关系。

2、构建阶段

编译模块(make):根据entry对应的dependence创建module对象,调用loader将模块转译为标准JS内容,调用JS解释器将内容转换为AST对象,从中找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的依赖关系图

3、生成阶段

输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会

写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

接下来,我们来看一下三个阶段具体完成了哪些工作:

1、配置阶段

看图说话:

image.png 首先,webpack将process.args + webpack.config.js合并成用户配置

调用validateSchema校验配置

调用getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults合并出最终配置

创建compiler对象

遍历用户定义的plugins集合,执行插件的apply方法

调用new WebpackOptionsApply().process方法,加载各种内置插件

构建阶段

你有没有思考过这样的问题:

Webpack编译过程中,如何怎么处理资源依赖?资源文件都经历了哪些状态转变?

这些问题,在构建阶段都能得出答案。构建阶段从entry开始递归解析资源与资源的依赖,在compilation对象内逐步构建出module集合以及module之间的依赖关系,核心流程看图:

image.png

首先,调用handleModuleCreate方法,根据文件类型构建module子类

调用loader-runner仓库的runLoaders转译module内容,通常是从各类资源类型转译为JavaScript文本

调用acorn将JS文本解析为AST

遍历AST,触发各种钩子在HarmonyExportDependencyParserPlugi插件监听exportImportSpecifier钩子,解读JS文本对应的资源依赖

调用module对象的addDependency将依赖对象加入到module依赖列表中

AST遍历完毕后,调用module.handleParseResult处理模块依赖

对于module新增的依赖,调用handleModuleCreate,控制流回到第一步

所有依赖都解析完毕后,构建阶段结束。

这个过程中数据流经过这样的转换:module(loader能识别的文件) => ast => dependences => module(标准的js模块),先转AST再从AST找依赖。这就要求loaders处理完的最后结果,必须是可以被acorn处理的标准 JavaScript语法,比如说对于图片,需要从图像二进制转换成类似于export default "data:image/png;base64,xxx" 这类 base64 格式或者 export default "http://xxx" 这类url格式。

compilation按这个流程递归处理,逐步解析出每个模块的内容以及module依赖关系,后续就可以根据这些内容打包输出。

总结 回顾章节开始时提到的问题:

Webpack编译过程中,如何怎么处理资源依赖?资源文件都经历了哪些状态转变?

1、Webpack 遍历 AST 集合过程中,识别 require/ import 之类的导入语句,确定模块对其他资源的依赖关系。

2、数据流经过这样的转换:module(loader能识别的文件) => ast => dependences => module(标准的js模块)。

生成阶段

webpack的构建阶段围绕module展开,生成阶段则围绕chunks展开。经过构建阶段之后,webpack得到足够的模块内容与模块关系信息,接下来开始生成最终资源了。代码层面,就是开始执行compilation.seal函数:

seal原意为密封、上锁。我很喜欢一位大牛的总结,他形容整个的seal过程是“将模块装进蜜罐”。seal函数主要完成从module到chunks的转化,核心流程还是看图先:

image.png 简单梳理一下:

首先,构建本次编译的ChunkGraph对象

遍历compilation.modules集合,将module按entry/动态引入 的规则分配给不同的Chunk对compilation.modules集合遍历完毕后,得到完整的chunks集合对象,调用createXxxAssets方法 createXxxAssets遍历module/chunk,调用compilation.emitAssets方法将资assets信息记录到 compilation.assets对象中

触发seal回调,控制流回到compiler对象。这一步的关键逻辑是module按规则组织成chunks,webpack内置的 chunk封装规则比较简单:

1、entry及entry触达到的模块,组合成一个chunk

2、使用动态引入语句引入的模块,各自组合成一个chunk(tips: 黑体字很关键哦,仔细体会实践!)

chunk是输出的基本单位,默认情况下这些chunks与最终输出的资源一一对应,那按上面的规则大致上可以推导出一个entry会对应打包出一个资源,而通过动态引入语句引入的模块,也对应会打包出相应的资源。

总结

webpack的整体构建流程主要还是依赖于compilation和module这两个对象,但其思想远不止这么简单。对于用户来说,webpack本质就是个插件集合,可以通过tapable控制各插件在webpack事件流上运行。在业务开发中,无论是为了提升打包构建效率,或是减小打包文件大小,我们都可以通过编写webpack插件来进行流程上的控制。

学习webpack最好的办法是实践,了解webpack的原理之后,需要亲手去配置,看到底哪个loader加载哪个模块,哪个plugin优化了哪个打包过程,实践是验证真理的唯一标准!且行且珍惜!

希望你能早日成为一个webpack大神!

参考文献

1、zhuanlan.zhihu.com/p/363928061

2、juejin.cn/post/684490…