webpack系列目录
- webpack5之核心配置梳理
- webpack5之模块化原理
- webpack5之Babel/ESlint/浏览器兼容
- webpack5之性能优化
- webpack5之Loader和Plugin的实现
- 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来摸索更。