webpack compile深入一小步
说是webpack compile, 准确的说是compiler执行run方法的过程, 这里包含make, seal, emit三个阶段.
这里会比上篇多深入一步, 介绍下最简单的情况下, 一个module经过各个阶段时的状态.
这些状态中夹杂着非常多的二开点(hooks), 了解module的状态, 就能知道在什么阶段可以对他大概进行什么处理了.
但对细节和具体数据结构点到为止, webpack细节实在是太多了, 慢慢再展开.
关于如何调试, 在上一篇有介绍, 在最近的调试中, 想强调的是, 遇到hooks, 回调, queue, 要注意记得使用run to line
, 如果错误按了下一步
, 那就得从头来过. 调试的技巧下文都不说了, 直接说结论.
tldr
运行一次webpack, 项目文件走过的流程是:
- 从配置中找到入口文件.
- 从配置中找到与入口文件匹配的loader.
- 读取入口文件, 并以此运行所有loader, 把结果保存下来.
- 分析上一步得到的结果, 依赖了哪些别的文件.
- 对这些入口文件依赖的文件, 重复步骤2~步骤4, 并记录所有文件之间的依赖关系.
- 根据配置和插件的操作, 确定需要输出的文件有几个, 分别包含了哪些项目文件.
- 根据上一步确定好的关系, 和步骤3每个文件运行loader后的结果, 再综合配置和插件, 计算出最终输出的文件内容.
- 把需要输出的文件写到磁盘中.
知道了流程后, 我们在写插件的时候就知道, 在那里修改项目文件会被分析依赖或走babel. 在哪里进行切分输出的文件, 或是在哪里控制代码压缩, 代码优化.
下面是详细一些的流程.
make
在这个阶段开始时, compilation
实例刚被建立, 还什么都没有, 模块的信息只存在于webpack配置的entry里.
addEntry
由EntryPlugin
注册的hooks, 调用了compilation.addEntry()
, 启动compile流程.
把入口信息放到compilation.entries
里.
factory.create()
通过EntryPlugin
注册的dependency和moduleFactory是EntryDependency
和normalModuleFactory
, 执行了handleModuleCreation
.
最后执行了factory.create()
, 这里的factory是normalModuleFactory
, 主要信息参数就是EntryDependency
, 其中包含了配置中entry的信息.
在create方法中, 调用的factorize
hooks和resolve
hooks都在自己的构造器里定义的.
resolve.tapAsync
resolve阶段比较复杂, 处理了模块引入的前缀和pre/post的loader, 最后整理出了createData
.
createData
里放入了: module的url, 整理后的所有loader, parser和generator.
之后读取url的内容再调用整理完的loader就行了. parser和generator是通过hooks注册的, 我的简单例子中被注册的都是javascriptplugin提供的.
factorize.tapAsync
resolve阶段结束后, 这里有2个hookscreateModule
和module
, 可以用来二开factory.create的结果.
我的例子中都没有做任何处理, 那么factory.create的结果是new NormalModule(createData)
.
这里的createData
是经过resolve阶段赋值的, 所以这个被实例化的NormalModule里已经包含了url, loader, parser和generator了.
至此, factory.create结束, 返回结果是NormalModule实例.
addModule
获取到module实例后, 添加/更新到compilation
的_modulesCache
, _modules
, 和modules
属性里.
然后调用moduleGraph.setResolvedModule()
, 把entryDependency和实例化的module进行关联.
module.build()
通过调用一系列方法, 走到了module.build()
, 在执行前有个buildModule
的hooks, 以及把当前module添加到了compilation
的builtModules
中. 然后开始运行build()
方法, 我例子里的module是NormalModule
.
NormalModule
的build()
分为2个阶段.
runLoaders
调用runLoaders()
, 当前module需要调用哪些loader已经在factory.create的resolve阶段整理好了.
所以这里做的理解成读取文件, 然后按个调用loader, 拿到最后的结果. (其实很复杂有很多概念, 以后展开)
拿到的结果, 除了文件内容经过loaders以后的字符串, 还有一系列依赖信息.
把依赖信息和loader信息存到compilation
的buildInfo
里, 并把字符串结果存在_source
里.
parse
还得说个前提, 我的例子走到的是javascript parser.
parse除了通过hooks把ast暴露给二开用户, 还做了个重要的事: 通过import
和exportImport
hooks和别的内置plugin关联, 别的plugin通过这个hooks调用了module的addDependency()
.
(顺便说一下用户在hooks里修改ast是无意义的, 这里ast可以认为是只读的, 只能用来分析, 甚至这个ast都不会被保存到webpack流程里.)
然后对_source
和hash方法配置进行hash, 保存到module中. (其实这部在运行完loader就可以做了)
到这里, module的build就完成了, 最后更新一下_modulesCache
.
processModuleDependencies
在parse阶段module通过别的内置plugin调用addDependency()
而新增了自己的dependencies
.
遍历dependencies
, 调用moduleGraph.setParents()
来建立module间的关系.
再调用processDependencyForResolving()
来处理dependencies的关系. (这里todo, 没深入看)
处理完以后, 对处理过的dependencies进行遍历, 调用handleModuleCreation()
进行处理, 重复从factory.create()
开始的步骤. (直到所有被build的模块都没有dependencies了.)
seal
make阶段结束后, 所有module已经都完成build, 拥有自己的_source
, 存放在多个属性和moduleGraph中了.
chunkGraph
seal开始前, 我们只有moduleGraph来维护module间的关系.
现在出现了多个变量: chunks, chunkGroup, chunkGraph.
再加上之前的module和moduleGraph. 在seal开始的阶段被互相关联起来了. (通过connectChunkGroupAndChunk()
, chunkGraph.connectChunkAndEntryModule()
, entryModules.add()
等, 在我的例子中, entrypoint是特殊的chunkGroup.)
我们稍微来看一下变量间的联系:
- chunkGraph的
_chunks
和_modules
和moduleGraph
. - chunk里有
entryModule
和_groups
. - chunkGroups里自然有chunk的信息.
总的来说, module是基本单位, chunk中包含了module并且是最后输出一个文件的单位.
而moduleGraph记录着module间的关系, 这个是不能改变的, 因为是项目代码决定的.
chunkGraph记录着chunk和module的包含关系, 初始有算法, 但是可以通过调用api来改变的.
调用了很多hook, 主要是修改modulegraph, dependency.
modulegraph会影响下一步chunkgraph的关系建立, dependency会影响codegen的结果. (也影响一些别的hook)
然后调用buildChunkGraph()
来建立chunkgraph和modulegraph之间的关系.
然后调用了很多hook, 这里是关于修改chunkgraph的hook.
走完这段流程, chunkGraph被建立起来, chunk, module之间都有了确定的联系. (而这里有一大坨hooks可以操作chunk, 但不在主流程讨论范围)
codeGeneration
遍历modules和各个情况, 让所有的模块都调用module.codeGenerate()
, 并把所有结果存到compilation.codeGenerationResults
里.
javascript的codeGenerate的输入是运行过loaders的结果_source
.
然后遍历module的dependency, 最后执行sourceDependency()
.
sourceDependency()
做的事也很简单, 根据dependency去compilation里取一个template. 然后调用template.apply.
compilation里dep和template的关系都是plugin给的. 一般plugin都会在compilation阶段设置关系(通过compilation.dependencyTemplates.set()
, 并且在别的生命周期给module增加dependency. (通过addDependency()
)来影响codegen结果.
createChunkAssets
遍历chunk, 通过renderManifest
这个hooks和其他内置plugin联动, 获取产生最后assets的render()
方法.
renderManifest
是一个waterfall hook, 会轮流调用, 把上一个的结果传给下一个.
直接在webpack代码里搜索, 很多plugin都注册了, 然后判断当前module归不归自己管, 如果归自己管就处理.
以javascriptModulePlugin为例, __webpack_require__xx之类的方法都是这里被加上的.
renderManifest
的运行是为了生成一个render()
方法.
render()
方法的生成, 依赖之前buildChunkGraph
整理出的chunkGraph.
具体行为是: 通过chunkGraph.getOrderedChunkModulesIterableBySourceType
来获取chunkgraph的chunkgraphchunk(cgc)中的modules, 再读取每个module的codegen结果, 并用Template.renderChunkModules
拼接起来.
这个render()
函数执行后就能获得可以最终输出的source
了.(可以理解为字符串, 只是为处理方便弄的数据结构)
最后调用emitAsset()
来向assets
里添加键值. 这个api也是webpack文档的plugin demo介绍的api.
至此, compilation
里已经有assets
了, 也就是最终要写到磁盘数据的信息.
emit
seal阶段结束后, 回到compiler
, 调用compiler.emitAssets()
.
根据配置的输出路径, 创建目录, 读取compilation
的assets
.
这里的assets已经包含了每个文件的输出路径和内容, 调用api输出就完事了.
至此webpack的一次执行结束.
todo
这次整理的流程中, seal阶段最模糊, 又很重要, 最需要之后深入:
dependency的意义
如何利用dependency和template影响codegen结果
seal阶段具体的事情, 和
buildChunkGraph
, 如何treeshaking/scopehoisting, 如何调整chunk
(本文完)
如果你觉得本文对你有帮助, 你可以请我喝一杯咖啡
本文遵循cc协议
你可以在注明出处和非商用的前提下任意复制及演绎