前言
前端的发展,离不开打包工具的助推。关于打包工具种类繁多,其中 Webpack 作为佼佼者被广泛使用。通过配置选项和 Loader、Plugin,我们可以实现各种功能。那么 Webpack 内部到底是怎么运行,通过本系列的源码学习,希望能给笔者有一个完整的认识。
1、准备工作
1.1、工具及版本说明
首先,在阅读本文之前。我们先约定下本系列文章约定的源码版本,如下:
对于 Node 版本,笔者使用的 14 的大版本,当然为了方便调试,建议使用不低于 10 的大版本。另外,我们使用 VSCode 进行调试和代码阅读。
1.2、约定及说明
阅读源码是一个学习的过程,当然也是一个比较痛苦的过程,尤其是 webpack 这种使用 hooks 来完成主流程的库。在主要步骤中会很跳跃,这一定程度上增加了源码阅读的难度,本文会集中在几个关键阶段或部分:compiler 、compilation 、Tapable 和 Hook、Module及其处理器 Loader、Plugin。并对行数过长的方法进行代码删减,保留一些关键代码的方法来让读者对整体执行流程有一个简单认识。然后,再根据自己的需要,按图索骥的对感兴趣的部分进行深入阅读。
2、源码分析
2.1、Webpack 执行入口
首先我们找到程序的入口位置,直接看 webpack 包(这个 node_modules\webpack 目录下)的 package.json 文件,并找到部分内容:
|
|
其他我们都不看,直接关注 main 这个属性,这就是我们在 Node 环境中执行 webpack 的入口。那么,我们得到完整的 webpack 代码路径:
node_modules\webpack\lib\webpack.js
|
|
通过以上的代码,主要完成了以下几个工作:
- 1、配置项处理
- 2、创建 compiler 对象
- 3、执行配置项里 plugins 的 apply 方法
- 4、执行环境配置相关的 hooks
- 5、执行 WebpackOptionsApply 的 process 方法,这个方法的代码我在下面讲解
- 6、执行 compiler 的 run 或 watch 方法,真正的执行编译的任务
2.2、来自 WebpackOptionsApply.process 的加餐
上一节中提到 WebpackOptionsApply 的 process 方法,这个方法很长,大概4、500行(包括注释部分)。
我们会针对这个方法进行重点解释。
|
|
ok,可以看到在进入到 compiler.run 之前,在这里又做了大量的工作。这里主要的工作集中在根据我们对于 webpack 的配置项进行插件的添加和 apply 方法的执行。我们在上面的代码讲解里有一个严格模式的插件,我们简单看下它的源码:
|
|
这里它主要通过钩子方法,可以在对代码生成一颗抽象语法树 ast 之后,拿到 ast,并做一些工作。这个地方其实给了我们一些启示,能够针对 ast 进行一些操作,当然还可以使用 Babel 作为可选方案。
2.3、贯穿始终的 Compiler
Compiler 和 Compilation 是两个在 webpack 的 plugin 中常用的两个对象。二者也包含了很多的 hooks,这些 hooks 代表了 webpack 在执行过程的不同阶段。其中,通过前面的代码我们看到了 Compiler 的创建过程,它包含了很多的配置项 options,里面又包含了 Loader 和 Plugin 信息。其生命周期一直到 webpack 结束,几乎等同于 webpack 实例本身。
而 Compilation 到此为止,我们还没有看到。它其实只是一次编程过程,包含了 Module、编译后的输出等,同时他也拥有 Compiler 对象实例。
在上面对于 webpack 执行入口的源码分析中,我们看到了 compiler.watch 或 compiler.run 才真正执行起来。我们一般在开发阶段中会使用到 webpack-dev-server ,它这里 Watch 模式默认开启。本文要讲以 compiler.watch 方法作为入口继续源码的分析。
源码位置 node_modules\webpack\lib\Compiler.js
|
|
前面的一些设置代码,我们可以忽略,直接看最后创建并返回了一个 Watching 对象。
|
|
ok,到这里可能有点混乱了,我们来理一理流程主线:
Compiler.watch() —->> Watching._go() —->> Compiler.hooks.watchRun —->> Compiler.compile(onCompiled) —->> onCompiled —->> 执行 Compiler.hooks.emit 和 Watching._done() —->> Compiler.hooks.done (通过Watching._done)。
这个主线里,我们重点要记住 Compiler.hooks.emit 和 Compiler.hooks.done。前者是即将进行 output,这是我们最后能够修改模块内容的机会,后者则是完成编译文件输出。另外,在这个主线里,我们看到缺了 Compiler.compile(onCompiled) 这一部分的内容。接下来,我们继续 Compiler 的源码部分
|
|
完成了上述代码的过程,我们对上面的 compiler.compile() 内部的流程主线可以归纳(侧重 hooks 部分):
compiler.compile() —->> compiler.hooks.normalModuleFactory 和 compiler.hooks.contextModuleFactory —->> compiler.hooks.beforeCompile —->> compiler.hooks.compile —->> compiler.hooks.thisCompilation —->> compiler.hooks.compilation —->> compiler.hooks.make —->> compilation.finish() —->> compilation.seal() —->> compiler.hooks.afterCompile —->> onCompiled()
hooks 本质上反映的是整个 webpack 运行的不同阶段。在上面这个主线中,我们其实重点关注两个点:
- compiler.hooks.make,从这里开始正式进入编译
- compilation.seal() 函数,它的执行标识着封闭编译,不再添加 module
然后我们看到从 compiler.hooks.make 直接到了 compilation.finish() 这一步。中间经历了什么,我们在下一节中进行源码分析。
另外,上述代码中生成了两个工厂对象:ContextModuleFactory 和 NormalModuleFactory 。其中,ContextModuleFactory 用来解析目录,为每个文件生成请求,并依据传递来的 regExp 进行过滤。最后匹配成功的依赖关系将被传入 NormalModuleFactory 。NormalModuleFactory 用来生成各类模块。从入口点开始,它会分解每个请求,解析文件内容以查找进一步的请求,然后通过分解所有请求以及解析新的文件来爬取全部文件。在最后阶段,每个依赖项都会成为一个模块实例。
2.4、大包干的 Compilation
在上一节的最后,我们缺了一大块内部,就是怎么就从 compiler.hooks.make 直接到了 compilation.finish() 这一步?开发过 plugin 的同学应该知道,通过 hooks 我们可以通过 tap 方法去钩住某个执行阶段,compiler.hooks.make 执行就会触发这些 hooks。我们通过调试或在 ./node_modules/webpack 下用关键词搜索 hooks.make.tap 可以发现很多插件。比如:DllEntryPlugin、MultiEntryPlugin、AutomaticPrefetchPlugin、PrefetchPlugin等,我们以 MultiEntryPlugin 为例来看一下它的源码实现:
|
|
通过插件在 compiler.hooks.make 上挂的钩子,成功的从 compiler 转译到了 compilation 的执行阶段。 compilation.addEntry(context, dep, name, callback) 方法从名字上,我们就可以看出是为编译添加入口,其实这并不难理解。接下来,我们看看 addEntry 方法之后都干了些什么:
|
|
这一块涉及到 compilation 中的几个方法,源码部分很多,笔者删除了大部分。主线捋一捋大致如下:通过 addEntry 找到入口模块然后把模块和依赖模块进行构建,构建过程是先构建当前模块,该过程伴随着缓存的处理和更新,真正的构建是通过具体模块对象的 module.build 方法进行(这个后面系列的文章中分析)。构建完当前模块后对分析出来的依赖模型就行构建,不断重复该过程直至所有的模块构建完毕。在这个过程中伴随着一些 hooks 的执行。
那么回到之前的那个问题,现在我们知道了如何从 compiler.hooks.make 顺滑的走到了 compilation.addEntry ,最终完成模块的构建工作。单个模块完成构建工作后执行了 compilation.hooks.succeedModule 钩子。那么,webpack 如何确定所有的模块以及构建完成最终执行了 compilation.finish() 呢?这个问题,我们放到下一篇文章中去介绍 Tapable 和 Hook。另外,我们在上面的源码分析中留下了一个问题就是模块如何构建,这个在系列三中去解答。