webpack 笔记
Webpack 核心原理
主体框架
核心流程解析
- 初始化阶段
- 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数
- 创建编译器对象:用上一步得到的参数创建
Compiler
对象 - 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
- 开始编译:执行
Compiler
对象的run
方法 - 确定入口:根据配置中的 entry 找出所有的入口文件,调用
compilation.addEntry
将入口文件转换为dependence
对象
- 构建阶段
- 编译模块(make):根据 entry 对应的
dependence
创建module
对象,调用loader
将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理 - 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图
- 编译模块(make):根据 entry 对应的
- 生成阶段
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会 - 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
技术名词介绍
- 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 构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程
初始化阶段
构建阶段
生成阶段
seal
原意密封、上锁,我个人理解在 webpack 语境下接近于 “将模块装进蜜罐” 。seal 函数主要完成从 module
到 chunks
的转化,核心流程:
资源形态流转
compiler.make
phase:- entry 文件以
dependence
对象形式加入compilation
的依赖列表,dependence
对象记录有entry
的类型、路径等信息 - 根据
dependence
调用对应的工厂函数创建module
对象,之后读入module
对应的文件内容,调用loader-runner
对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为module
- entry 文件以
compilation.seal
phase:- 遍历
module
集合,根据entry
配置及引入资源的方式,将module
分配到不同的chunk
- 遍历
chunk
集合,调用compilation.emitAsset
方法标记chunk
的输出规则,即转化为assets
集合
- 遍历
compiler.emitAssets
phase:- 将
assets
写入文件系统
- 将
Webpack 插件架构深度讲解
Tapable 全解析
Tapable 是 Webpack 插件架构的核心支架,但它的源码量其实很少,本质上就是围绕着 订阅/发布 模式叠加各种特化逻辑,适配 webpack 体系下复杂的事件源-处理器之间交互需求,比有些场景需要支持将前一个处理器的结果传入下一个回调处理器;有些场景需要支持异步并行调用这些回调处理器。
基本用法
Tapable 使用时通常需要经历如下步骤:
- 创建钩子实例
- 调用订阅接口注册回调,包括:
tap
、tapAsync
、tapPromise
- 调用发布接口触发回调,包括:
call
、callAsync
、promise
example
1 | const { SyncHook } = require("tapable"); |
示例中使用 tap
注册回调,使用 call
触发回调,在某些钩子中还可以使用异步风格的 tapAsync/callAsync
、promise 风格 tapPromise/promise
,具体使用哪一类函数与钩子类型有关。
Tapable 钩子类型
名称 | 简介 | 统计 |
---|---|---|
SyncHook | 同步钩子 | Webpack 共出现 86 次,如 Compiler.hooks.compilation |
SyncBailHook | 同步熔断钩子 | Webpack 共出现 90 次,如 Compiler.hooks.shouldEmit |
SyncWaterfallHook | 同步瀑布流钩子 | Webpack 共出现 26 次,如 Compilation.hooks.assetPath |
SyncLoopHook | 同步循环钩子 | Webpack 中未使用 |
AsyncParallelHook | 异步并行钩子 | Webpack 仅出现 6 次:Compiler.hooks.make |
AsyncParallelBailHook | 异步并行熔断钩子 | Webpack 中未使用 |
AsyncSeriesHook | 异步串行钩子 | Webpack 共出现 32 次,如 Compiler.hooks.done |
AsyncSeriesBailHook | 异步串行熔断钩子 | Webpack 共出现 9 次,如 Compilation.hooks.optimizeChunkModules |
AsyncSeriesLoopHook | 异步串行循环钩子 | Webpack 中未使用 |
AsyncSeriesWaterfallHook | 异步串行瀑布流钩子 | Webpack 共出现 3 次,如 ContextModuleFactory.hooks.beforeResolve |
按回调逻辑,分为:
- 基本类型,名称不带
Waterfall
/Bail
/Loop
关键字,与通常 「订阅/回调」 模式相似,按钩子注册顺序,逐次调用回调waterfall
类型:前一个回调的返回值会被带入下一个回调bail
类型:逐次调用回调,若有任何一个回调返回非 undefined 值,则终止后续调用loop
类型:逐次、循环调用,直到所有回调函数都返回 undefined
- 第二个维度,按执行回调的并行方式,分为:
sync
:同步执行,启动后会按次序逐个执行回调,支持call
/tap
调用语句async
:异步执行,支持传入 callback 或 promise 风格的异步回调函数,支持callAsync/tapAsync
、promise/tapPromise
两种调用语句
Dependency Graph 深度解析
有点难的 Webpack 知识点:Dependency Graph 深度解析
webpack 处理应用代码时,会从开发者提供的 entry
开始递归地组建起包含所有模块的 Dependency Graph
,之后再将这些 module
打包为 bundles
。
Dependency Graph
本节将深入 webpack 源码,解读 Dependency Graph 的内在数据结构及依赖关系收集过程。在正式展开之前,有必要回顾几个 webpack 重要的概念:
Module
:资源在 webpack 内部的映射对象,包含了资源的路径、上下文、依赖、内容等信息Dependency
:在模块中引用其它模块,例如import "a.js"
语句,webpack 会先将引用关系表述为Dependency
子类并关联module
对象,等到当前module
内容都解析完毕之后,启动下次循环开始将Dependency
对象转换为适当的Module
子类。Chunk
:用于组织输出结构的对象,webpack 分析完所有模块资源的内容,构建出完整的Dependency Graph
之后,会根据用户配置及Dependency Graph
内容构建出一个或多个chunk
实例,每个chunk
与最终输出的文件大致上是一一对应的。
数据结构
Webpack 4.x 的 Dependency Graph
实现较简单,主要由 Dependence/Module 内置的系列属性记录引用、被引用关系。
而 Webpack 5.0 之后则实现了一套相对复杂的类结构记录模块间依赖关系,将模块依赖相关的逻辑从 Dependence/Module 解耦为一套独立的类型结构,主要类型有:
ModuleGraph
:记录Dependency Graph
信息的容器,一方面保存了构建过程中涉及到的所有module
、dependency
对象,以及这些对象互相之间的引用;另一方面提供了各种工具方法,方便使用者迅速读取出module
或dependency
附加的信息ModuleGraphConnection
:记录模块间引用关系的数据结构,内部通过originModule
属性记录引用关系中的父模块,通过module
属性记录子模块。此外还提供了一系列函数工具用于判断对应的引用关系的有效性ModuleGraphModule
:Module 对象在Dependency Graph
体系下的补充信息,包含模块对象的incomingConnections
—— 指向模块本身的ModuleGraphConnection
集合,即谁引用了模块自己;outgoingConnections
—— 该模块对外的依赖,即该模块引用了其他那些模块。
ModuleGraph
对象通过_dependencyMap
属性记录Dependency
对象与ModuleGraphConnection
连接对象之间的映射关系,后续的处理中可以基于这层映射迅速找到Dependency
实例对应的引用与被引用者ModuleGraph
对象通过_moduleMap
在module
基础上附加ModuleGraphModule
信息,而ModuleGraphModule
最大的作用就是记录了模块的引用与被引用关系,后续的处理可以基于该属性找到module
实例的所有依赖与被依赖关系
ModuleGraph
、ModuleGraphConnection
、ModuleGraphModule
三者协作,在 webpack 构建过程(make
阶段)中逐步收集模块间的依赖关系
addDependency
:webpack 从模块内容中解析出引用关系后,创建适当的Dependency
子类并调用该方法记录到module
实例handleModuleCreation
:模块解析完毕后,webpack 遍历父模块的依赖集合,调用该方法创建Dependency
对应的子模块对象,之后调用compilation.moduleGraph.setResolvedModule
方法将父子引用信息记录到ModuleGraph
对象上
实例解析
Webpack 启动后,在构建阶段递归调用 compilation.handleModuleCreation
函数,逐步补齐 Dependency Graph
结构,最终可能生成如下数据结果:
1 | ModuleGraph: { |
module.issuer
十分钟精进 Webpack:module.issuer 属性详解
在 webpack 实现上,文件资源使用 Module
类管理,所有关于资源的操作、转译、合并、关系都在 module
实现。而 module.issuer
属性用于记录资源的引用者,例如对于下面的资源依赖:
index
引用了 a/b
两个文件,webpack 构建时会用三个 module
对象分别对应三个文件,同时在 a/b
模块中通过 issuer
属性指向 index
模块:
module['a.js'].issuer = module['index.js']
module['b.js'].issuer = module['index.js']
通过issuer
属性,模块可以反向查找到引用者。
实例: Stats 类
Stats
是 webpack 内置的对象,用于收集构建过程信息,比如耗时、模块依赖关系、错误信息、报警信息等,我们运行 webpack 命令输出的命令行信息就是由 Stats
类提供的
如果编译过程发生错误,Stats
会通过 module.issuer
属性逐级往上查找出完整调用堆栈
1 | class Stats { |
何时修改 issuer
在 compilation
解析出 index.js
内容的 AST
后,遍历 require/import
语句解读当前模块引用了那些资源,解析到任意依赖后就会调用 addModuleDependencies
记录依赖关系,从 addModuleDependencies
源码看在依赖被创建为 module
时,会同步修改新模块的 issuer
,记录引用者的信息。
Webpack Chunk 分包规则详解
webpack 实现中,原始的资源模块以 Module
对象形式存在、流转、解析处理。
而 Chunk
则是输出产物的基本组织单位,在生成阶段 webpack 按规则将 entry
及其它 Module
插入 Chunk
中,之后再由 SplitChunksPlugin
插件根据优化规则与 ChunkGraph
对 Chunk
做一系列的变化、拆解、合并操作,重新组织成一批性能(可能)更高的 Chunks
。运行完毕之后 webpack 继续将 chunk
一一写入物理文件中,完成编译工作。
综上,Module
主要作用在 webpack 编译过程的前半段,解决原始资源“「如何读」”的问题;而 Chunk
对象则主要作用在编译的后半段,解决编译产物“「如何写」”的问题,两者合作搭建起 webpack 搭建主流程。
默认分包规则
Webpack 4 之后编译过程大致上可以拆解为四个阶段
在构建(make
) 阶段,webpack 从 entry
出发根据模块间的引用关系(require/import
) 逐步构建出模块依赖关系图(ModuleDependencyGraph),依赖关系图表达了模块与模块之间互相引用的先后次序,基于这种次序 webpack 就可以推断出模块运行之前需要先执行那些依赖模块,也就可以进一步推断出那些模块应该打包在一起,那些模块可以延后加载(异步执行),关于模块依赖图的更多信息,可以参考我另一篇文章 《有点难的 webpack 知识点:Dependency Graph 深度解析》。
到了生成(seal
) 阶段,webpack 会根据模块依赖图的内容组织分包 —— Chunk
对象,默认的分包规则有:
- 同一个
entry
下触达到的模块组织成一个chunk
- 异步模块单独组织为一个
chunk
entry.runtime
单独组织成一个chunk
默认规则集中在 compilation.seal
函数实现,seal
核心逻辑运行结束后会生成一系列的 Chunk
、ChunkGroup
、ChunkGraph
对象,后续如 SplitChunksPlugin
插件会在 Chunk
系列对象上做进一步的拆解、优化,最终反映到输出上才会表现出复杂的分包结果。
Entry 分包处理
重点:seal 阶段遍历 entry 对象,为每一个 entry 单独生成 chunk,之后再根据模块依赖图将 entry 触达到的所有模块打包进 chunk 中。
在生成阶段,Webpack 首先根据遍历用户提供的 entry 属性值,为每一个 entry 创建 Chunk 对象,比如对于如下配置:
1 | module.exports = { |
Webpack 遍历 entry 对象属性并创建出 chunk[main]
、chunk[home]
两个对象,此时两个 chunk 分别包含 main
、home
模块:
初始化完毕后,Webpack 会读取 ModuleDependencyGraph
的内容,将 entry
所对应的内容塞入对应的 chunk
(发生在 webpack/lib/buildChunkGrap.js
文件)。比如对于如下文件依赖:
main.js
以同步方式直接或间接引用了 a/b/c/d
四个文件,分析 ModuleDependencyGraph
过程会逐步将 a/b/c/d
模块逐步添加到 chunk[main]
中,最终形成:
异步模块分包处理
重点:分析 ModuleDependencyGraph 时,每次遇到异步模块都会为之创建单独的 Chunk 对象,单独打包异步模块。
Webpack 4 之后,只需要用异步语句 require.ensure("./xx.js")
或 import("./xx.js")
方式引入模块,就可以实现模块的动态加载,这种能力本质也是基于 Chunk
实现的。
Webpack 生成阶段中,遇到异步引入语句时会为该模块单独生成一个 chunk
对象,并将其子模块都加入这个 chunk
中。例如对于下面的例子:
1 | // index.js, entry 文件 |
在 index.js
中,以同步方式引入 sync-a
、sync-b
;以异步方式引入 async-a
模块;同时,在 async-c
中以同步方式引入 sync-c
模块。对应的模块依赖如:
此时,webpack 会为入口 index.js
、异步模块 async-a.js
分别创建分包,形成如下数据:
里需要引入一个新的概念 —— Chunk
间的父子关系。由 entry
生成的 Chunk
之间相互孤立,没有必然的前后依赖关系,但异步生成的 Chunk
则不同,引用者(上例 index.js
块)需要在特定场景下使用被引用者(上例 async-a
块),两者间存在单向依赖关系,在 webpack 中称引用者为 parent
、被引用者为 child
,分别存放在 ChunkGroup._parents
、ChunkGroup._children
属性中。
上述分包方案默认情况下会生成两个文件:
- 入口
index
对应的index.js
- 异步模块
async-a
对应的src_async-a_js.js
运行时,webpack 在 index.js 中使用 promise 及 __webpack_require__.e
方法异步载入并运行文件 src_async-a_js.js
,从而实现动态加载。
PS: 基于异步模块的 chunk
在 webpack 官方文档中,通常称之为 「Async chunk」
Runtime 分包
重点: Webpack 5 之后还能根据 `entry.runtime` 配置单独打包运行时代码。
除了 entry、异步模块外,webpack 5 之后还支持基于 runtime
的分包规则。除业务代码外,Webpack 编译产物中还需要包含一些用于支持 webpack 模块化、异步加载等特性的支撑性代码,这类代码在 webpack 中被统称为 runtime
。举个例子,产物中通常会包含如下代码:
1 | /******/ (() => { |
编译时,Webpack 会根据业务代码决定输出那些支撑特性的运行时代码(基于 Dependency
子类),例如:
- 需要
__webpack_require__.f
、__webpack_require__.r
等功能实现最起码的模块化支持 - 如果用到动态加载特性,则需要写入
__webpack_require__.e
函数 - 如果用到
Module Federation
特性,则需要写入__webpack_require__.o
函数 - 等等
虽然每段运行时代码可能都很小,但随着特性的增加,最终结果会越来越大,特别对于多 entry
应用,在每个入口都重复打包一份相似的运行时代码显得有点浪费,为此 webpack 5 专门提供了 entry.runtime
配置项用于声明如何打包运行时代码。用法上只需在 entry
项中增加字符串形式的 runtime
值,例如:
1 | module.exports = { |
Webpack 执行完 entry
、异步模块分包后,开始遍历 entry
配置判断是否带有 runtime
属性,如果有则创建以 runtime
值为名的 Chunk
,因此,上例配置将生成两个chunk
:chunk[index.js]
、chunk[solid-runtime]
,并据此最终产出两个文件:
- 入口 index 对应的
index.js
文件 - 运行时配置对应的
solid-runtime.js
文件
在多 entry
场景中,只要为每个 entry
都设定相同的 runtime
值,webpack 运行时代码最终就会集中写入到同一个 chunk
,例如对于如下配置:
入口 index
、home
共享相同的 runtime
,最终生成三个 chunk
,分别为:
同时生成三个文件:
- 入口 index 对应的
index.js
- 入口 index 对应的
home.js
- 运行时代码对应的
solid-runtime.js
分包规则的问题
至此,webpack 分包规则的基本逻辑就介绍完毕了,实现上,大部分功能代码都集中在:
- webpack/lib/compilation.js 文件的 seal 函数
- webpack/lib/buildChunkGraph.js 的 buildChunkGraph 函数
默认分包规则最大的问题是无法解决模块重复,如果多个 chunk
同时包含同一个 module
,那么这个 module
会被不受限制地重复打包进这些 chunk
。比如假设我们有两个入口 main/index
同时依赖了同一个模块:
默认情况下,webpack 不会对此做额外处理,只是单纯地将 c 模块同时打包进 main/index
两个 chunk
,最终形成:
可以看到 chunk
间互相孤立,模块 c 被重复打包,对最终产物可能造成不必要的性能损耗!
为了解决这个问题,webpack 3 引入 CommonChunkPlugin
插件试图将 entry
之间的公共依赖提取成单独的 chunk
,但 CommonChunkPlugin
本质上是基于 Chunk
之间简单的父子关系链实现的,很难推断出提取出的第三个包应该作为 entry
的父 chunk
还是子 chunk
,CommonChunkPlugin
统一处理为父 chunk
,某些情况下反而对性能造成了不小的负面影响。
在 webpack 4 之后则引入了更负责的设计 —— ChunkGroup
专门实现关系链管理,配合 SplitChunksPlugin
能够更高效、智能地实现 「启发式分包」
彻底理解 Webpack 运行时
编译产物分析
为了正常、正确运行业务项目,Webpack 需要将开发者编写的业务代码以及支撑、调配这些业务代码的「运行时」一并打包到产物(bundle
)中,以建筑作类比的话,业务代码相当于砖瓦水泥,是看得见摸得着能直接感知的逻辑;运行时相当于掩埋在砖瓦之下的钢筋地基,通常不会关注但决定了整座建筑的功能、质量。
大多数 Webpack
特性都需要特定钢筋地基才能跑起来,比如说:
异步按需加载
HMR
WASM
Module Federation
基本结构
1 | // a.js |
使用如下配置:
1 | module.exports = { |
直接看编译生成的结果
虽然看起来很非主流,但细心分析还是能拆解出代码脉络的,bundle 整体由一个 IIFE 包裹,里面的内容从上到下依次为:
__webpack_modules__
对象,包含了除入口外的所有模块,示例中即 a.js 模块__webpack_module_cache__
对象,用于存储被引用过的模块__webpack_require__
函数,实现模块引用(require) 逻辑__webpack_require__.d
,工具函数,实现将模块导出的内容附加的模块对象上__webpack_require__.o
,工具函数,判断对象属性用__webpack_require__.r
,工具函数,在 ESM 模式下声明 ESM 模块标识- 最后的
IIFE
,对应entry
模块即上述示例的 index.js ,用于启动整个应用
这几个 __webpack_
开头奇奇怪怪的函数可以统称为 Webpack
运行时代码,作用如前面所说的是搭起整个业务项目的骨架,就上述简单示例所罗列出来的几个函数、对象而言,它们协作构建起一个简单的模块化体系从而实现 ES Module
规范所声明的模块化特性。
上述示例中最终的函数是 __webpack_require__
,它实现了模块间引用功能,核心代码:
1 | function __webpack_require__(moduleId) { |
从代码可以推测出,它的功能:
根据 moduleId
参数找到对应的模块代码,执行并返回结果
如果 moduleId
对应的模块被引用过,则直接返回存储在 __webpack_module_cache__
缓存对象中的导出内容,避免重复执行
其中,业务模块代码被存储在 bundle 最开始的 __webpack_modules__
变量中,内容如:
1 | var __webpack_modules__ = { |
结合 __webpack_require__
函数与 __webpack_modules__
变量就可以正确地引用到代码模块,例如上例生成代码最后面的IIFE
:
1 | (() => { |
这几个函数、对象构成了 Webpack 运行时最基本的能力 —— 模块化,它们的生成规则与原理我们放到文章第二节《实现原理》再讲,下面我们继续看看异步模块加载、模块热更新场景下对应的运行时内容。
异步模块加载
我们来看个简单的异步模块加载示例:
1 | // ./src/a.js |
使用异步模块加载特性时,会额外增加如下运行时:
__webpack_require__.e
:逻辑上包裹了一层中间件模式与 promise.all
,用于异步加载多个模块__webpack_require__.f
:供 __webpack_require__.e
使用的中间件对象,例如使用 Module Federation
特性时就需要在这里注册中间件以修改 e
函数的执行逻辑__webpack_require__.u
:用于拼接异步模块名称的函数__webpack_require__.l
:基于 JSONP
实现的异步模块加载函数__webpack_require__.p
:当前文件的完整 URL
,可用于计算异步模块的实际 URL
建议读者运行示例对比实际生成代码,感受它们的具体功能。这几个运行时模块构建起 Webpack 异步加载能力,其中最核心的是 __webpack_require__.e
函数,它的代码很简单:
1 | __webpack_require__.f = {}; |
从代码看,只是实现了一套基于 __webpack_require__.f
的中间件模式,以及用 Promise.all
实现并行处理,实际加载工作由 __webpack_require__.f.j
与 __webpack_require__.l
实现,分开来看两个函数:
1 | /******/ __webpack_require__.f.j = (chunkId, promises) => { |
__webpack_require__.f.j
实现了异步 chunk
路径的拼接、缓存、异常处理三个方面的逻辑,而 __webpack_require__.l
函数:
1 | /******/ var inProgress = {}; |
1 | /******/ var inProgress = {}; |
__webpack_require__.l
中通过 script
实现异步 chunk
内容的加载与执行。
e + l + f.j
三个运行时函数支撑起 Webpack 异步模块运行的能力,落到实际用法上只需要调用 e
函数即可完成异步模块加载、运行,例如上例对应生成的 entry
内容:
1 | /*!**********************!*\ |
模块热更新
1 | module.exports = { |
按照上述配置,使用命令 webpack serve --hot-only
启动 Webpack,就可以在 dist 文件夹找到产物:
- 支持 HMR 所需要用到的 webpack-dev-server 、webpack/hot/xxx 、querystring 等框架,这一部分占了大部分代码
__webpack_require__.l
:与异步模块加载一样,基于 JSONP 实现的异步模块加载函数__webpack_require__.e
:与异步模块加载一样__webpack_require__.f
:与异步模块加载一样__webpack_require__.hmrF
: 用于拼接热更新模块 url 的函数webpack/runtime/hot
:这不是单个对象或函数,而是包含了一堆实现模块替换的方法
可以看到, HMR 运行时是上面异步模块加载运行时的超集,而异步模块加载的运行时又是第一个基本示例运行时的超集,层层叠加。在 HMR 中包含了:
- 模块化能力
- 异步模块加载能力 —— 实现变更模块的异步加载
- 热替换能力 —— 用拉取到的新模块替换掉旧的模块,并触发热更新事件
实现原理
- 除了业务代码外,
bundle
中还必须包含「运行时」代码才能正常运行 - 「运行时的具体内容由业务代码,确切地说由业务代码所使用到的特性决定」,例如使用到异步加载时需要打包
__webpack_require__.e
函数,那么这里面必然有一个运行时依赖收集的过程 - 开发者编写的业务代码会被包裹进恰当的运行时函数中,实现整体协调
落到 Webpack 源码实现上,运行时的生成逻辑可以划分为两个步骤:
- 「依赖收集」:遍历业务代码模块收集模块的特性依赖,从而确定整个项目对
Webpack runtime
的依赖列表 - 「生成」:合并
runtime
的依赖列表,打包到最终输出的bundle
两个步骤都发生在打包阶段,即 Webpack(v5) 源码的 compilation.seal
函数中:
进入 runtime
处理环节时 Webpack 已经解析得出 ModuleDependencyGraph
及 ChunkGraph
关系,也就意味着此时已经可以计算出:
- 需要输出那些
chunk
- 每个
chunk
包含那些module
,以及每个module
的内容 chunk
与chunk
之间的父子依赖关系
基于这些信息,接下来首先需要收集运行时依赖。
依赖收集
Webpack runtime 的依赖概念上很像 Vue 的依赖,都是用来表达模块对其它模块存在依附关系,只是实现方法上 Vue 基于动态、在运行过程中收集,而 Webpack 则基于静态代码分析的方式收集依赖。实现逻辑大致为:
运行时依赖的计算逻辑集中在 compilation.processRuntimeRequirements
函数,代码上包含三次循环:
第一次循环遍历所有 module
,收集所有 module
的 runtime
依赖
第二次循环遍历所有 chunk
,将 chunk
下所有 module
的 runtime
统一收录到 chunk
中
第三次循环遍历所有 runtime chunk
,收集其对应的子 chunk
下所有 runtime
依赖,之后遍历所有依赖并发布 runtimeRequirementInTree
钩子,(主要是) RuntimePlugin
插件订阅该钩子并根据依赖类型创建对应的 RuntimeModule
子类实例
第一次循环:收集模块依赖
在打包(seal)阶段,完成 ChunkGraph
的构建之后,Webpack 会紧接着调用 codeGeneration
函数遍历 module
数组,调用它们的 module.codeGeneration
函数执行模块转译,模块转译结果如:
其中,sources
属性为模块经过转译后的结果;而 runtimeRequirements
则是基于 AST 计算出来的,为运行该模块时所需要用到的运行时,计算过程与本文主题无关,挖个坑下一回我们再继续讲。
所有模块转译完毕后,开始调用 compilation.processRuntimeRequirements
进入第一重循环,将上述转译结果的 runtimeRequirements
记录到 ChunkGraph
对象中。
第二次循环:整合 chunk 依赖
第一次循环针对 module
收集依赖,第二次循环则遍历 chunk
数组,收集将其对应所有 module
的 runtime
依赖,例如:
示例图中,module a
包含两个运行时依赖;module b
包含一个运行时依赖,则经过第二次循环整合后,对应的 chunk
会包含两个模块对应的三个运行时依赖。
第三次循环:依赖标识转 RuntimeModule 对象
源码中,第三次循环的代码最少但逻辑最复杂,大致上执行三个操作:
- 遍历所有
runtime chunk
,收集其所有子chunk
的runtime
依赖 - 为该
runtime chunk
下的所有依赖发布runtimeRequirementInTree
钩子 RuntimePlugin
监听钩子,并根据runtime
依赖的标识信息创建对应的RuntimeModule
子类对象,并将对象加入到ModuleDependencyGraph
和ChunkGraph
体系中管理
至此,runtime
依赖完成了从 module
内容解析,到收集,到创建依赖对应的 Module
子类,再将 Module
加入到 ModuleDependencyGraph /ChunkGraph
体系的全流程,业务代码及运行时代码对应的模块依赖关系图完全 ready,可以准备进入下一阶段 —— 生成最终产物。
但在继续讲解产物逻辑之前,我们有必要先解决两个问题:
- 何谓
runtime chunk
?与普通chunk
是什么关系 - 何谓
RuntimeModule
?与普通Module
有什么区别
回顾一下在三种特定的情况下,Webpack 会创建新的 chunk:
- 每个
entry
项都会对应生成一个chunk
对象,称之为initial chunk
- 每个异步模块都会对应生成一个
chunk
对象,称之为async chunk
- Webpack 5 之后,如果
entry
配置中包含 runtime 值,则在entry
之外再增加一个专门容纳runtime
的chunk
对象,此时可以称之为runtime chunk
默认情况下 initial chunk
通常包含运行该 entry
所需要的所有 runtime
代码,但 webpack 5 之后出现的第三条规则打破了这一限制,允许开发者将 runtime
从 initial chunk
中剥离出来独立为一个多 entry 间可共享的 runtime chunk
。
类似的,异步模块对应 runtime
代码大部分都被包含在对应的引用者身上,比如说:
1 | // a.js |
在这个示例中,index 异步引入 a 模块,那么按默认分配规则会产生两个 chunk
:入口文件 index 对应的 initial chunk
、异步模块 a 对应的 async chunk
。此时从 ChunkGraph
的角度看 chunk[index]
为 chunk[a]
的父级,运行时代码会被打入 chunk[index]
,站在浏览器的角度,运行 chunk[a]
之前必须先运行 chunk[index]
,两者形成明显的父子关系。
在最开始阅读 Webpack 源码的时候,我就觉得很奇怪,Module
是 Webpack 资源管理的基本单位,但 Module
底下总共衍生出了 54 个子类,且大部分为 Module => RuntimeModule => xxxRuntimeModule
的继承关系:
在 有点难的 webpack 知识点:Dependency Graph 深度解析 一文中我们聊到模块依赖关系图的生成过程及作用,但篇文章的内容是围绕业务代码展开的,用到的大多是 NormalModule
。到 seal
函数收集运行时的过程中,RuntimePlugin
还会为运行时依赖一一创建对应的 RuntimeModule
子类,例如:
- 模块化实现中依赖
__webpack_require__.r
,则对应创建MakeNamespaceObjectRuntimeModule
对象 - ESM 依赖
__webpack_require__.o
,则对应创建HasOwnPropertyRuntimeModule
对象 - 异步模块加载依赖
__webpack_require__.e
,则对应创建EnsureChunkRuntimeModule
对象 - 等等
所以可以推导出所有 RuntimeModule
结尾的类型与特定的运行时功能一一对应,收集依赖的结果就是在业务代码之外创建出一堆支撑性质的 RuntimeModule
子类,这些子类对象随后被加入 ModuleDependencyGraph
,并入整个模块依赖体系中。
资源合并生成
经过上面的运行时依赖收集过程后,bundles
所需要的所有内容都就绪了,接着就可以准备写出到文件中,即下图核心流程中的生成(emit
)阶段:
Webpack-核心原理对这一块有比较细致的讲解,这里从运行时的视角再简单聊一下代码流程:
调用 compilation.createChunkAssets
,遍历 chunks
将 chunk
对应的所有 module
,包括业务模块、运行时模块全部合并成一个资源(Source
子类)对象
调用 compilation.emitAsset
将资源对象挂载到 compilation.assets
属性中
调用 compiler.emitAssets
将 assets
全部写到 FileSystem
发布 compiler.hooks.done
钩子
运行结束
如何编写 loader
Loader 基础
代码层面,Loader 通常是一个函数,结构如下:
1 | module.exports = function (source, sourceMap?, data?) { |
Loader 函数接收三个参数,分别为:
source
:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果sourceMap
: 可选参数,代码的 sourcemap 结构data
: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递参数的 AST 对象
其中 source 是最重要的参数,大多数 Loader 要做的事情就是将 source 转译为另一种形式的 output
1 | export default function rawLoader(source) { |
这段代码的作用是将文本内容包裹成 JavaScript 模块
返回多个结果
上例通过 return 语句返回处理结果,除此之外 Loader 还可以以 callback 方式返回更多信息,供下游 Loader 或者 Webpack 本身使用
1 | export default function loader(content, map) { |
通过 this.callback(null, content, map)
语句同时返回转译后的内容与 sourcemap 内容。callback 的完整签名如下:
1 | this.callback( |
异步处理
涉及到异步或 CPU 密集操作时,Loader 中还可以以异步形式返回处理结果
1 | import less from "less"; |
在 less-loader 中,逻辑分三步:
- 调用
this.async
获取异步回调函数,此时 Webpack 会将该 Loader 标记为异步加载器,会挂起当前执行队列直到callback
被触发 - 调用 less 库将 less 资源转译为标准 css
- 调用异步回调
callback
返回处理结果
this.async
返回的异步回调函数签名与上一节介绍的 this.callback
相同,此处不再赘述。
缓存
Loader 为开发者提供了一种便捷的扩展方法,但在 Loader 中执行的各种资源内容转译操作通常都是 CPU 密集型 —— 这放在单线程的 Node 场景下可能导致性能问题;又或者异步 Loader 会挂起后续的加载器队列直到异步 Loader 触发回调,稍微不注意就可能导致整个加载器链条的执行时间过长。
为此,默认情况下 Webpack 会缓存 Loader 的执行结果直到资源或资源依赖发生变化,开发者需要对此有个基本的理解,必要时可以通过 this.cachable 显式声明不作缓存,例如:
1 | module.exports = function (source) { |
上下文与 Side Effect
除了作为内容转换器外,Loader 运行过程还可以通过一些上下文接口,有限制地影响 Webpack 编译过程,从而产生内容转换之外的副作用。
上下文信息可通过 this 获取,this
对象由 NormolModule.createLoaderContext
函数在调用 Loader 前创建,常用的接口包括:
1 | const loaderContext = { |
其中,addDependency
、emitFile
、emitError
、emitWarning
都会对后续编译流程产生副作用,例如 less-loader
中包含这样一段代码:
1 | try { |
解释一下,代码中首先调用 less
编译文件内容,之后遍历所有 import
语句,也就是上例 result.imports
数组,一一调用 this.addDependency
函数将 import
到的其它资源都注册为依赖,之后这些其它资源文件发生变化时都会触发重新编译。
Loader 链式调用
使用上,可以为某种资源文件配置多个 Loader,Loader 之间按照配置的顺序从前到后(pitch),再从后到前依次执行,从而形成一套内容转译工作流,例如对于下面的配置:
1 | module.exports = { |
这是一个典型的 less 处理场景,针对 .less
后缀的文件设定了:less、css、style 三个 loader 协作处理资源文件,按照定义的顺序,Webpack 解析 less 文件内容后先传入 less-loader;less-loader 返回的结果再传入 css-loader 处理;css-loader 的结果再传入 style-loader;最终以 style-loader 的处理结果为准,流程简化后如:
上述示例中,三个 Loader 分别起如下作用:
less-loader
:实现less => css
的转换,输出 css 内容,无法被直接应用在 Webpack 体系下css-loader
:将 css 内容包装成类似module.exports = "${css}"
的内容,包装后的内容符合 JavaScript 语法style-loade
r: 做的事情非常简单,就是将 css 模块包进 require 语句,并在运行时调用injectStyle
等函数将内容注入到页面的style
标签
三个 Loader 分别完成内容转化工作的一部分,形成从右到左的调用链条。链式调用这种设计有两个好处,一是保持单个 Loader 的单一职责,一定程度上降低代码的复杂度;二是细粒度的功能能够被组装成复杂而灵活的处理链条,提升单个 Loader 的可复用性。
不过,这只是链式调用的一部分,这里面有两个问题:
- oader 链条一旦启动之后,需要所有 Loader 都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常
- 某些场景下并不需要关心资源的具体内容,但 Loader 需要在 source 内容被读取出来之后才会执行
为了解决这两个问题,Webpack 在 loader 基础上叠加了 pitch
的概念。
Loader Pitch
Webpack 允许在这个函数上挂载名为 pitch
的函数,运行时 pitch
会比 Loader
本身更早执行,例如:
1 | const loader = function (source) { |
Pitch 函数的完整签名:
1 | function pitch( |
remainingRequest
: 当前 loader 之后的资源请求字符串previousRequest
: 在执行当前 loader 之前经历过的 loader 列表data
: 与 Loader 函数的 data 相同,用于传递需要在 Loader 传播的信息
这些参数不复杂,但与 requestString 紧密相关,我们看个例子加深了解:
1 | { |
css-loader.pitch
中拿到的参数依次为:
1 | // css-loader 之后的 loader 列表及资源路径 |
调度逻辑
Pitch
翻译成中文是抛、球场、力度、事物最高点等,我觉得 pitch
特性之所以被忽略完全是这个名字的锅,它背后折射的是一整套 Loader
被执行的生命周期概念。
实现上,Loader
链条执行过程分三个阶段:pitch
、解析资源、执行,设计上与 DOM
的事件模型非常相似,pitch
对应到捕获阶段;执行对应到冒泡阶段;而两个阶段之间 Webpack 会执行资源内容的读取、解析操作,对应 DOM
事件模型的 AT_TARGET
阶段:
pitch
阶段按配置顺序从左到右逐个执行 loader.pitch
函数(如果有的话),开发者可以在 pitch
返回任意值中断后续的链路的执行:
那么为什么要设计 pitch 这一特性呢?「阻断」!
示例:style-loader
实际上, style-loader
只是负责让 css
能够在浏览器环境下跑起来,本质上并不需要关心具体内容,很适合用 pitch
来处理,核心代码:
1 | // ... |
loaderApi
为空函数,不做任何处理loaderApi.pitch
中拼接结果,导出的代码包含:- 引入运行时模块
runtime/injectStylesIntoLinkTag.js
- 引入运行时模块
- 复用
remainingRequest
参数,重新引入 css 文件
1 | // 运行结果大致如 |
注意了,到这里 style-loader
的 pitch
函数返回这一段内容,后续的 Loader
就不会继续执行,当前调用链条中断了:
之后,Webpack 继续解析、构建 style-loader
返回的结果,遇到 inline loader
语句:
var content = require('!!css-loader!less-loader!./xxx.less');
所以从 Webpack 的角度看,实际上对同一个文件调用了两次 loader
链,第一次在 style-loader
的 pitch
中断,第二次根据 inline loader
的内容跳过了 style-loader
。
产物转译打包逻辑
Dependency-Graph-深度解析
经过 「构建(make
)阶段」 后,Webpack 解析出:
module
内容module
与module
之间的依赖关系图
而进入 「生成(「「seal
」」)阶段」 后,Webpack 首先根据模块的依赖关系、模块特性、entry
配置等计算出 Chunk Graph
,确定最终产物的数量和内容
本文继续聊聊 Chunk Graph
后面之后,模块开始转译到模块合并打包的过程,大体流程如下:
为了方便理解,我将打包过程横向切分为三个阶段:
- 「入口」:指代从 Webpack 启动到调用
compilation.codeGeneration
之前的所有前置操作 - 「模块转译」:遍历
modules
数组,完成所有模块的转译操作,并将结果存储到compilation.codeGenerationResults
对象 - 「模块合并打包」:在特定上下文框架下,组合业务模块、
runtime
模块,合并打包成bundle
,并调用compilation.emitAsset
输出产物
这里说的 「业务模块」 是指开发者所编写的项目代码;「runtime
模块」 是指 Webpack 分析业务模块后,动态注入的用于支撑各项特性的运行时代码. 彻底理解-Webpack-运行时
模块转译原理
上述示例由 index.js
/ name.js
两个业务文件组成,对应的 Webpack 配置如上图左下角所示;Webpack 构建产物如右边 main.js
文件所示,包含三块内容,从上到下分别为:
name.js
模块对应的转译产物,函数形态- Webpack 按需注入的运行时代码
index.js
模块对应的转译产物,IIFE(立即执行函数) 形态
以看到产物与源码语义、功能均相同,但表现形式发生了较大变化,例如 index.js 编译前后的内容:
上图右边是 Webpack 编译产物中对应的代码,相对于左边的源码有如下变化:
- 整个模块被包裹进 IIFE (立即执行函数)中
- 添加
__webpack_require__.r(__webpack_exports__);
语句,用于适配 ESM 规范 - 源码中的
import
语句被转译为__webpack_require__
函数调用 - 源码
console
语句所使用的name
变量被转译为_name__WEBPACK_IMPORTED_MODULE_0__.default
- 添加注释
核心流程
「模块转译」 操作从 module.codeGeneration
调用开始,对应到上述流程图的:
- 调用
JavascriptGenerator
的对象的generate
方法,方法内部:- 遍历模块的
dependencies
与presentationalDependencies
数组 - 执行每个数组项
dependency
对象的对应的Template.apply
方法,在apply
内修改模块代码,或更新initFragments
数组
- 遍历模块的
- 遍历完毕后,调用
InitFragment.addToSource
静态方法,将上一步操作产生的source
对象与initFragments
数组合并为模块产物
简单说就是遍历依赖,在依赖对象中修改 module
代码,最后再将所有变更合并为最终产物。这里面关键点:
- 在
Template.apply
函数中,如何更新模块代码 - 在
InitFragment.addToSource
静态方法中,如何将Template.apply
所产生的side effect
合并为最终产物
Template.apply 函数
上述流程中,JavascriptGenerator
类是毋庸置疑的 C 位角色,但它并不直接修改 module
的内容,而是绕了几层后委托交由 Template
类型实现。
Webpack 5 源码中,JavascriptGenerator.generate
函数会遍历模块的 dependencies
数组,调用依赖对象对应的 Template
子类 apply
方法更新模块内容,说起来有点绕,原始代码更饶,所以我将重要步骤抽取为如下伪代码:
1 | class JavascriptGenerator { |
从上述伪代码可以看出,JavascriptGenerator.generate
函数的逻辑相对比较固化:
- 初始化一系列变量
- 遍历
module
对象的依赖数组,找到每个dependency
对应的template
对象,调用template.apply
函数修改模块内容 - 调用
InitFragment.addToSource
方法,合并source
与initFragments
数组,生成最终结果
这里的重点是 JavascriptGenerator.generate
函数并不操作 module
源码,它仅仅提供一个执行框架,真正处理模块内容转译的逻辑都在 xxxDependencyTemplate
对象的 apply
函数实现,如上例伪代码中 24-28 行。
每个 Dependency
子类都会映射到一个唯一的 Template
子类,且通常这两个类都会写在同一个文件中,例如 ConstDependency
与 ConstDependencyTemplate``;NullDependency
与 NullDependencyTemplate
。Webpack 构建(make
)阶段,会通过 Dependency
子类记录不同情况下模块之间的依赖关系;到生成(seal
)阶段再通过 Template
子类修改 module
代码。
综上 Module
、JavascriptGenerator
、Dependency
、Template
四个类形成如下交互关系:
Template
对象可以通过两种方法更新 module
的代码:
直接操作 source
对象,直接修改模块代码,该对象最初的内容等于模块的源码,经过多个 Template.apply
函数流转后逐渐被替换成新的代码形式
操作 initFragments
数组,在模块源码之外插入补充代码片段
这两种操作所产生的 side effect
,最终都会被传入 InitFragment.addToSource
函数,合成最终结果,下面简单补充一些细节。
使用 Source 更改代码
Source
是 Webpack 中编辑字符串的一套工具体系,提供了一系列字符串操作方法,包括:
- 字符串合并、替换、插入等
- 模块代码缓存、sourcemap 映射、hash 计算等
Webpack 内部以及社区的很多插件、loader 都会使用 Source
库编辑代码内容,包括上文介绍的 Template.apply
体系中,逻辑上,在启动模块代码生成流程时,Webpack 会先用模块原本的内容初始化 Source
对象,即:
const source = new ReplaceSource(module.originalSource());
之后,不同 Dependency
子类按序、按需更改 source
内容,例如 ConstDependencyTemplate
中的核心代码:
1 | ConstDependency.Template = class ConstDependencyTemplate extends ( |
上述 ConstDependencyTemplate
中,apply
函数根据参数条件调用 source.insert
插入一段代码,或者调用 source.replace
替换一段代码。
使用 InitFragment 更新代码
除直接操作 source
外,Template.apply
中还可以通过操作 initFragments
数组达成修改模块产物的效果。initFragments
数组项通常为 InitFragment
子类实例,它们通常带有两个函数: getContent
、getEndContent
,分别用于获取代码片段的头尾部分。
例如 HarmonyImportDependencyTemplate
的 apply
函数中:
1 | HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends ( |
代码合并
上述 Template.apply
处理完毕后,产生转译后的 source
对象与代码片段 initFragments
数组,接着就需要调用 InitFragment.addToSource
函数将两者合并为模块产物。
addToSource
的核心代码如下:
1 | class InitFragment { |
可以看到,addToSource
函数的逻辑:
- 遍历
initFragments
数组,按顺序合并fragment.getContent()
的产物 - 合并
source
对象 - 遍历
initFragments
数组,按顺序合并fragment.getEndContent()
的产物
所以,模块代码合并操作主要就是用 initFragments
数组一层一层包裹住模块代码 source
,而两者都在 Template.apply
层面维护。
自定义 banner 插件
经过 Template.apply
转译与 InitFragment.addToSource
合并之后,模块就完成了从用户代码形态到产物形态的转变,为加深对上述 「模块转译」 流程的理解,接下来我们尝试开发一个 Banner
插件,实现在每个模块前自动插入一段字符串。
实现上,插件主要涉及 Dependency
、Template
、hooks
对象,代码:
1 | const { Dependency, Template } = require("webpack"); |
关键步骤:
- 编写
DemoDependency
与DemoDependencyTemplate
类,其中DemoDependency
仅做示例用,没有实际功能;DemoDependencyTemplate
则在其apply
中调用source.insert
插入字符串,如示例代码第 10-14 行 - 使用
compilation.dependencyTemplates
注册DemoDependency
与DemoDependencyTemplate
的映射关系 - 使用
thisCompilation
钩子取得compilation
对象 - 使用
succeedModule
钩子订阅module
构建完毕事件,并调用module.addDependency
方法添加DemoDependency
依赖
module
对象的产物在生成过程就会调用到 DemoDependencyTemplate.apply
函数,插入我们定义好的字符串,效果如:
lib/dependencies/ConstDependency.js
,一个简单示例,可学习source
的更多操作方法lib/dependencies/HarmonyExportSpecifierDependencyTemplate.js
,一个简单示例,可学习initFragments
数组的更多用法
l-ib/dependencies/HarmonyImportDependencyTemplate.js
,一个较复杂但使用率极高的示例,可综合学习source``、initFragments
数组的用法
模块合并打包原理
讲完单个模块的转译过程后,我们先回到这个流程图:
流程图中,compilation.codeGeneration
函数执行完毕 —— 也就是模块转译阶段完成后,模块的转译结果会一一保存到 compilation.codeGenerationResults
对象中,之后会启动一个新的执行流程 —— 「模块合并打包」。
「模块合并打包」 过程会将 chunk
对应的 module
及 runtimeModule
按规则塞进 「模板框架」 中,最终合并输出成完整的 bundle
文件,例如上例中:
示例右边 bundle
文件中,红框框出来的部分为用户代码文件及运行时模块生成的产物,其余部分撑起了一个 IIFE 形式的运行框架即为 「模板框架」,
1 | (() => { |
- 最外层由一个 IIFE 包裹
- 一个记录了除
entry
外的其它模块代码的__webpack_modules__
对象,对象的 key 为模块标志符;值为模块转译后的代码 - 一个极度简化的 CMD 实现:
__webpack_require__
函数 - 最后,一个包裹了
entry
代码的 IIFE 函数
「模块转译」 是将 module
转译为可以在宿主环境如浏览器上运行的代码形式;而 「模块合并」 操作则串联这些 modules
,使之整体符合开发预期,能够正常运行整个应用逻辑。接下来,我们揭晓这部分代码的生成原理。
核心流程
在 compilation.codeGeneration
执行完毕,即所有用户代码模块与运行时模块都执行完转译操作后,seal
函数调用 compilation.createChunkAssets
函数,触发 renderManifest
钩子,JavascriptModulesPlugin
插件监听到这个钩子消息后开始组装 bundle
,伪代码
1 | // Webpack 5 |
这里的核心逻辑是,compilation
以 renderManifest
钩子方式对外发布 bundle
打包需求; JavascriptModulesPlugin
监听这个钩子,按照 chunk
的内容特性,调用不同的打包函数。
上述仅针对 Webpack 5。在 Webpack 4 中,打包逻辑集中在 MainTemplate 完成。
两个打包函数实现的逻辑接近,都是按顺序拼接各个模块,下面简单介绍下 renderMain
的实现。
renderMain 函数
renderMain
函数涉及比较多场景判断,原始代码很长很绕,我摘了几个重点步骤
1 | class JavascriptModulesPlugin { |
- 先计算出
bundle CMD
代码,即__webpack_require__
函数 - 计算出当前
chunk
下,除entry
外其它模块代码chunkModules
- 计算出运行时模块代码
- 开始执行合并操作,子步骤有:
- 合并
CMD
代码 - 合并
runtime
模块代码 - 遍历
chunkModules
变量,合并除entry
外其它模块代码 - 合并
entry
模块代码
- 合并
- 返回结果
总结:先计算出不同组成部分的产物形态,之后按顺序拼接打包,输出合并后的版本。
至此,Webpack 完成 bundle
的转译、打包流程,后续调用 compilation.emitAsset
,按上下文环境将产物输出到 fs
即可,Webpack 单次编译打包过程就结束了。
详细讨论了打包流程后半截 —— 从 chunk graph
生成一直到最终输出产物的实现逻辑,重点:
- 首先遍历
chunk
中的所有模块,为每个模块执行转译操作,产出模块级别的产物 - 根据
chunk
的类型,选择不同结构框架,按序逐次组装模块产物,打包成最终bundle