webpack的基本原理
前言
本文是对知乎博主范文杰《[万字总结] 一文吃透 Webpack 核心原理》的沉淀化总结,原文写的可谓是鞭辟入里 精彩绝伦,推荐大家去看原文
在学习webpack之前,我们得先知道
- 为什么要学习webpack,他到底解决了哪些问题
- 其次我们得知道webpack大体包含了哪些内容,这样学起来才能做到心中有数
我们都知道,js本身就是一个不完善的脚本语言,并没有实现模块化,早年也只是在浏览中写写脚本操作DOM,但是随着时间的发展web应用变得越来越复杂,模块化开发变得刻不容缓,webpack其实就是充当了这个角色,只不过他所指代的模块更加宽泛,不仅仅指的js模块,甚至css图片字体都能当做模块来处理,最终打包出能在浏览器运行的js文件。 总的来说就是内容转换+资源合并,具体包含以下三个阶段
-
初始化阶段
- 初始化参数: 从配置文件,配置对象,shell参数中读取,与默认配置结合得出最终的参数
- 创建编译器对象: 用上一步得到的参数创建compiler对象
- 初始化编译环境: 包括注入内置插件,注册各种模块工厂,加载配置插件等
- 开始编译: 执行compiler对象的run方法
- 确定入口: 根据配置中的entry找到所有入口文件,调用compilation.addEntry将入口文件转换为dependence对象
-
构建阶段
- 编译模块(make) : 根据entry对应的dependence创建module对象,调用loader将模块转译为标准JS内容,调用JS解释器将内容转换为AST对象,从中找到该模块依赖的模块,通过递归本步骤直到所有入口依赖的文件都经过本步骤的处理
- 完成模块编译: 上一步递归处理所有能触达到的模块后,得到每个模块被翻译后的内容以及他们之间的依赖关系图
-
生成阶段
- 输出资源(seal) : 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 写入文件系统(emitAssets) : 在确定好输出内容后,根据配置确定输出的路径和文件名,将文件内容写入到文件系统
一些技术名称的介绍
- entry: 编译入口,webpack编译的起点
- compiler: 编译管理器,webpack启动后会创建compiler对象,该对象一直存活直到结束退出
- compilation: 单次编译过程中管理器,比如watch=true时,运行过程中只有一个compiler,但每次文件变更触发重新编译时,都会创建一个新的compilation对象
- dependence: 依赖对象,webpack基于该类型记录模块间依赖关系
- module: webpack内部所有资源都会以module对象形式存在,所有关于资源的操作,转译,合并都是以module为基本单位进行的
- chunk: 编译完成准备输出时,webpack会将module按照特定的规则组织成一个个的chunk,这些chunk某种程度上和最终输出一一对应
- loader: 资源内容转换器,其实就是实现将内容A转换成内容B
- plugin: webpack构建过程中,会在特定的实际广播对应的时间,插件监听这些时间,在特定的时间点接入编译过程
核心流程
初始化阶段
大概流程如下
-
将process.args + webpack.config.js合并成用户配置
-
调用validateSchema校验配置
-
调用getNormalizeWebpackOptions + applyWebpackOptionsBaseDefaults合并出最终配置
-
创建compiler对象
-
遍历用户定义的plugins集合,执行插件的apply方法
-
调用new webpackOptionsApply.process方法,加载各种内置插件,这些插件不需要我们手动配置而是会根据配置内容动态注入,比如
- 注入EntryOptionPlugin插件,处理entry配置
- 根据devtool值判断后续用哪个插件处理sourcemap
- 注入RuntimePlugin,用于根据代码内容动态注入webpack运行时
- ...
构建阶段
构建阶段从入口文件开始
-
调用handleModuleCreate,根据文件类型创建module子类
-
调用loader转译module内容,通常是从各种资源类型转译为js文本
-
调用acorn将js文本解析成AST
-
遍历AST,触发各种钩子
- 在HarmonyExportDependencyParsePlugin插件监听exportImportSpecifier钩子,解读js文本对应的资源依赖
- 调用module对象的addDenpendency将依赖对象加入到module依赖列表中
-
AST遍历完成后,调用module.handleParseResult处理模块依赖,对于新增的依赖,则调用handleModuleCreate递归处理
-
所有依赖解析完毕 则构建结束
在整个数据流中module => ast => dependences => modules,先转AST再从AST中寻找依赖,举个例子,入口文件为index.js 最终生成结果为
生成阶段
经历过构建阶段,webpack已经掌握了所有module信息以及他们之间的依赖关系,此时通过调用compilation.seal方法将module转换成chunk 大体流程如下
- 构建本次编译的ChunkGraph对象
- 遍历compilation.modules集合,将module按照entry/动态引入的规则分配给不同的chunk对象
- 得到完成chunk集合后,调用createXxxAssets方法将信息记录到compilation.assets对象中
- 触发seal回调,控制流回到compiler对象,通过调用compiler.emitAssets将assets集合写入文件系统
这里有个关键点就是我们都知道module可以有很多个,但是最终打包出来的chunk文件是很少的,其中的封装规则是咋样的?其实默认规则很简单
- entry和entry触达的模块,组合成一个chunk
- 动态引入的模块,各自组合成一个chunk
这里可以查看一个例子,index-a.js和index-b.js都是入口文件 最终生成的chunk结构如下
而且细心的观众可以可以发现c.js被打包进了两个chunk中,造成了重复,其实这种情况webpack也考虑到了,可以通过CommonChunkPlugin,SplitChunkPlugin插件来进行优化 那SplitChunkPlugin是如何优化chunk的呢,其实我们在创建完chunk对象后会触发各种优化钩子,SplitChunkPlugin利用钩子获取到chunk对象进行分析并增加一些通用的chunk,例如分析出多个chunk中都存在c模块从而将c模块独立成一个chunk。这其中也体现了webpack架构其实非常具有扩展性。
资源流转形态
为了更加完整了解入口文件最终是如何变成打包文件的,可以查看这张图
-
compilation.make阶段
- entry文件以dependence对象形式加入compilation的依赖列表,dependence对象记录有entry的类型,路径等信息
- 根据dependence调用对应的工厂函数创建module对象,之后读入module对应的文件内容,调用loader进行内容转化,转换结果如果有其他依赖则继续读入依赖资源,重复此过程直到所有依赖都转换成module
-
compilation.seal阶段
- 遍历module集合,根据entry配置和引入资源的方式,将module分配到不同的chunk
- 遍历chunk集合,调用compilation.emitAssets方法标记chunk的输出规则,也就是转换成assets集合
-
compiler.emitAssets阶段
- 将assets写入文件系统
loader
loader其实就是实现将资源处理成标准的js语法,比如图片处理成 export defult "http://xxx.png"
,也比如将ts处理成js,后续webpack才能基于转义出来的js解析成AST并进行解析依赖等操作。 由于内容比较独立,这里也不做展开。
插件
webpack的插件架构有点类似订阅发布模型,但是又有不同之处,区别在于一般来说订阅发布仅仅是订阅了某种消息后得到通知,但是webpack触发的回调钩子中会携带上下文,用户可以通过修改上下文的数据结构或者调用其接口来产生side effect,从而达到影响编译状态和后续流程。 举个例子,正如前面所提到了的SplitChunkPlugin通过监听compilation.hooks.optimizeChunks钩子实现chunk的拆分 由于内容比较独立,这里也不做展开。
转载自:https://juejin.cn/post/7350320544527646759