webpack5之核心源码解析
webpack 命令启动原理
我们启动 webpack 通常是方法是在 npm scripts 中配置 webpack 命令,比如构建打包命令
"scripts" {
"build": "webpack --config ./config/webpack.common.js --env production"
}
1. webpack/bin/webpack.js
我们用 webpack 命令启动是借助了webpack-cli
工具,当我们安装 webpack 时,webpack 的 package.json 中有一行bin
属性,那么 npm 会将bin
里面的webpack
属性名作为文件名,bin
目录下的webpack.js
这个文件里的作为内容的新的文件,复制到node_modules/.bin
下面安装
所以当我们在执行 webpack 命令的时候,实际是执行webpack/bin/webpack.js
这个文件。
我们看下webpack/bin/webpack.js
这个文件,过程主要如下
- 首先会定义一个
cli
对象 - 当判断
cli
内installed
属性是否为true - 执行
runCli
就是说它会先判断webpack-cli
这个包是否安装,如果没有会进入条件安装,否则执行runCli
,一般情况下我们在我也用命令打包会同时安装webpack
和webpack-cli
这两包,所以主要是看看runCli
1. 执行 runCli
runcli 里面主要做的是将 cli 里面的属性值进行拼接成路径,pkgPath
就是webpack-cli/package.json
,pkg
就是webpack-cli/package.json
对象,而webpack-cli/package.json
内的bin
属性的对应的webpack-cli
属性值就是bin/cli.js
所以最后拼接的是webpack-cli/bin/cli.js
这个路径,将调用这个文件
2. webpack-cli/bin/cli.js
1. 执行 runCLI
这个文件其实主要执行的是runCLI
,并且将命令后的参数一起传进去
这里调用了runCLI
的引用来自bootstrap 这个文件,我们来看下这个文件
3. webpack-cli/lib/bootstrap.js
1. 创建了 WebpackCLI 对象
2. 执行 cli.run
而这个WebpackCLI
来自webpack-cli.js
4.webpack-cli/lib/webpack-cli.js
run
方法这里主要是执行了以下流程
1. 执行 makeCommand
为了检查一些依赖包是否存在
2. 执行 makeOption
makeCommand
方法里面执行makeOption
方法,对我们传入的参数做了进一步处理
3. 执行 runWebpack
4. 执行 createCompiler
而在runWebpack
里面主要执行了createCompiler
5. 执行 webpack
而在createCompiler
里面主要调用webpack
这个方法,而这个webpack
方法就是来自 webpack 包
到这一步其实 webpack 已经打包完成了
webpack Compiler 创建原理
在上述执行 webpack 函数创建了compiler
,那这个是compiler
是如何创建的呢,我们来看一下这个 webpack 方法。webpack 来自webpack/lib/webpack.js
webpack/lib/webpack.js
在 wepack 方法里面可以看到,不管是否有回调都会调用create
返回compiler
1. 执行 create
在create
方法中主要执行了createCompiler
创建了 compiler
2. 执行 createCompiler
而在createCompiler
主要做了
new
了一个Compiler
对象plugin.apply
注册了所有的插件- 调用了
environment
和afterEnvironment
环境hook
- 调用
new WebpackOptionsApply().process
将配置属性转为plugin
注册 - 返回
compiler
3. 执行 compiler.run
在webpack
方法内首先会判断是否有callback
回调,如果存在回调会执行compiler.run
,如果不存在直接返回compiler
,所以我们在外面在执行webpack
方法获取compiler
后,我们即可以传入一个回调方法,也可以调用run
方法。
webpack/lib/WebpackOptionsApply.js
1. 插件注入 plugin.apply()
在webpack
中的createCompiler
里我们调用了new WebpackOptionsApply().process
,我们来看看这里到底怎么实现将配置属性转为plugin
注册
其实在process
方法中,我们将传入的属性转成webpack
的plugin
注入到 webpack 生命周期内,如上图展示的部分属性做判断,存在就将内置的 Plugin 进行导入(所以 plugin 事实上贯穿 webpack 的整个构建流程),其实这个方法都是在做plugin.apply
的调用注册,并将compiler
对象传入进去,这些 Plugin 后续会通过tapable
来实现钩子的监听, 并进行自己的处理逻辑
Compiler 中 run 方法执行原理
webpack/lib/Compiler.js
在上述createCompiler
中我们new
了一个Compiler
对象,这个构造方法主要做了什么呢,我们可以看下webpack/lib/Compiler.js
这个文件
当new Compiler
这个构造函数是会初始化各种各样的hooks
,而之前说process
里面的plugin
里会注册这些hooks
,这些hooks
来自一个叫tapable
的库来管理的,这是由webpack
官方自己来维护的一个库,对于tapable
这个库的介绍使用可以看我另一篇 webpack 文章webpack5 之 Loader 和 Plugin 的实现。
现在我们来看看Compiler
内的run
方法,其实主要是执行之前plugin
注册的hooks
。
而在Compiler
里面的run
方法里,又定义了一个run
方法,那我们看下这里做了什么
1. 执行 run
- 首先执行了
hooks.beforeRun
,执行一些需要运行前操作的plugin
- 再执行了
hooks.run
,执行一些需要运行开始需要操作的plugin
- 执行
compile
方法,并传入了onCompiled
编译完成的回调
2. 执行 compile
当执行到this.compile
就是开始准备编译了,我们来看看compile
里面做了什么
- 执行
hooks.beforeCompile
- 执行
hooks.compile
- 执行
hooks.make
- 执行
hooks.finishMake
- 执行
hooks.afterCompile
其实hooks.make
是最终的编译过程,而在hooks.compile
和hooks.make
之间执行了const compilation = this.newCompilation(params);
,并将compilation
传入了hooks.make
。
这里的 Compilation 与 Compiler 有什么区别呢
Compiler
- 在 webpack 构建的之初就会创建的一个对象, 并且在 webpack 的整个生命周期都会存在
(before - run - beforeCompiler - compile - make - finishMake - afterCompiler - done)
- 只要是做 webpack 的编译, 都会先创建一个
Compiler
- 如果修改 webpack 配置需要重新
npm run build
Compilation
- 存在于
compile - make
阶段 watch
源代码,每次发生改变就需要重新编译模块,创建一个新的Compilation
对象
Compilation 对 Module 的处理
上述的hooks.make
只是一个 hook 的调用,我们要去找注册在这个钩子上的回调,我们可以前往process
内的new EntryOptionPlugin().apply(compiler)
这个entry
插件
1. webpack/lib/EntryPlugin.js
这个插件在apply
里调用applyEntryOption
,而里面又调用EntryPlugin
插件
EntryPlugin
插件内可以看到注册了hooks.make
而在注册回调中主要执行了compilation.addEntry
,那我们来看看在compilation
这个对象中主要做了什么
2. webpack/lib/Compilation.js
在执行compilation.addEntry
这里主要做了
- 执行
_addEntryItem
,用于添加入口的 Item - 执行
addModuleTree
- 在
addModuleTree
中执行handleModuleCreation
- 在
handleModuleCreation
中执行factorizeModule
,添加hooks
到factorizeQueue
队列中 - 在
handleModuleCreation
中执行addModule
,添加module
模块到addModuleQueue
队列中 - 在
addModule
中执行buildModule
,将需要构建的module
模块添加到buildQueue
队列中 buildQueue
队列中有一个processor
属性,执行_buildModule
_buildModule
中执行module.needBuild
判断模块是否需要构建- 执行
module.build
, - 最后会在
wepack/lib/NormalModule.js
中执行build
方法,开始构建模块
module 的 build 阶段
上面在处理module
的最后在wepack/lib/NormalModule.js
中执行build
方法,开始构建模块,那现在我们来看看build
做了哪些内容
wepack/lib/NormalModule.js
1. 执行 doBuild
2. 执行_doBuild
执行doBuild
内的_doBuild
方法
3. 执行 runLoaders
执行_doBuild
内runLoaders
,这个runLoaders
来自独立的loader-runner
库,我们之前配置的各种Loaders就是在这里处理的
4. 执行 processResult
runLoaders
执行结束后回执行processResult
这个回调
5. 执行 parse
之后会调用parse
解析AST
树
而这个parse
来自webpack/lib/javascript/JavascriptParser.js
内的parse
这个parse
其实是用到了acorn
这个库来解析javascript
6. 执行 handleBuildDone
解析完后会调用handleParseResult
回调,里面执行handleBuildDone
handleBuildDone
里又执行了build
里面传进来的回调
最终执行的webpack/lib/Compilation.js
下的module.build
传进来的回调
7. _buildModule 执行完成
当_buildModule
执行完成后,最终hooks.make
执行完成,于是接下来会执行webpack/lib/Compiler.js
的compilation.finish
和compilation.seal
方法
到seal
这一步,就是开始将静态资源输出到构建目录了
输出静态资源到构建目录
1. 执行 hooks.optimizeChunkModules
首先执行hooks.optimizeChunkModules
,优化之前模块代码
2. 执行 codeGeneration
执行codeGeneration
,生成代码
3. 执行 createChunkAssets
执行createChunkAssets
,创建chunkAssets
资源
4. 执行 getRenderManifest
执行createChunkAssets
内的getRenderManifest
,将所有的数据放到一个了 manifest 的对象中
5. 执行 emitAsset
执行emitAsset
,输出资源,此时资源已存放在内存中
6. 执行 onCompiled
最终webpack/lib/Compiler
的compile
完成后执行回调onCompiled
7. 执行 emitAssets
onCompiled
回调里执行emitAssets
,
8. 执行 hooks.emit
最终在emitAssets
内执行hooks.emit
将资源导出到构建目录
结尾
源码的介绍可能还是有欠缺不完全的地方,我们在查看源码的时候可以时候 vscode 的 debugger 工具,通过打断点来查看代码走向,通过上面的介绍应该能大致理清 webpack 执行的流程,但是更细节的地方还是希望大家能够 debugger 来摸索更。