webpack5之核心源码解析

webpack系列目录

  1. webpack5之核心配置梳理
  2. webpack5之模块化原理
  3. webpack5之Babel/ESlint/浏览器兼容
  4. webpack5之性能优化
  5. webpack5之Loader和Plugin的实现
  6. 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下面安装

Snipaste_2022-03-28_20-52-45.png

Snipaste_2022-03-28_20-59-24.png

所以当我们在执行webpack命令的时候,实际是执行webpack/bin/webpack.js这个文件。

我们看下webpack/bin/webpack.js这个文件,过程主要如下

  1. 首先会定义一个cli对象
  2. 当判断cliinstalled属性是否为true
  3. 执行runCli

Snipaste_2022-03-28_21-03-28.png

就是说它会先判断webpack-cli这个包是否安装,如果没有会进入条件安装,否则执行runCli,一般情况下我们在我也用命令打包会同时安装webpackwebpack-cli这两包,所以主要是看看runCli

1. 执行runCli

Snipaste_2022-03-28_21-14-38.png

runcli里面主要做的是将cli里面的属性值进行拼接成路径,pkgPath就是webpack-cli/package.jsonpkg就是webpack-cli/package.json对象,而webpack-cli/package.json内的bin属性的对应的webpack-cli属性值就是bin/cli.js

Snipaste_2022-03-28_21-20-56.png

所以最后拼接的是webpack-cli/bin/cli.js这个路径,将调用这个文件

2. webpack-cli/bin/cli.js

1. 执行runCLI

这个文件其实主要执行的是runCLI,并且将命令后的参数一起传进去

Snipaste_2022-03-28_21-33-52.png

这里调用了runCLI的引用来自bootstrap这个文件,我们来看下这个文件

3. webpack-cli/lib/bootstrap.js

Snipaste_2022-03-28_21-36-15.png

1. 创建了WebpackCLI对象

2. 执行cli.run

而这个WebpackCLI来自webpack-cli.js

4.webpack-cli/lib/webpack-cli.js

run方法这里主要是执行了以下流程

1. 执行makeCommand

为了检查一些依赖包是否存在

Snipaste_2022-03-28_21-41-53.png

2. 执行makeOption

makeCommand方法里面执行makeOption方法,对我们传入的参数做了进一步处理

Snipaste_2022-03-28_21-46-20.png

3. 执行runWebpack

4. 执行createCompiler

而在runWebpack里面主要执行了createCompiler

Snipaste_2022-03-28_21-47-32.png

5. 执行webpack

而在createCompiler里面主要调用webpack这个方法,而这个webpack方法就是来自webpack包

Snipaste_2022-03-28_21-50-13.png

到这一步其实webpack已经打包完成了

webpack Compiler创建原理

在上述执行webpack函数创建了compiler,那这个是compiler是如何创建的呢,我们来看一下这个webpack方法。webpack来自webpack/lib/webpack.js

webpack/lib/webpack.js

Snipaste_2022-03-28_22-40-44.png

在wepack方法里面可以看到,不管是否有回调都会调用create返回compiler

1. 执行create

create方法中主要执行了createCompiler创建了compiler

Snipaste_2022-03-28_22-48-19.png

2. 执行createCompiler

Snipaste_2022-03-28_22-52-45.png

而在createCompiler主要做了

  1. new了一个Compiler对象
  2. plugin.apply注册了所有的插件
  3. 调用了environmentafterEnvironment环境hook
  4. 调用new WebpackOptionsApply().process将配置属性转为plugin注册
  5. 返回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注册

Snipaste_2022-03-28_23-07-26.png

其实在process方法中,我们将传入的属性转成webpackplugin注入到webpack生命周期内,如上图展示的部分属性做判断,存在就将内置的Plugin进行导入(所以plugin事实上贯穿webpack的整个构建流程),其实这个方法都是在做plugin.apply的调用注册,并将compiler对象传入进去,这些Plugin后续会通过tapable来实现钩子的监听, 并进行自己的处理逻辑

Compiler中run方法执行原理

webpack/lib/Compiler.js

在上述createCompiler中我们new了一个Compiler对象,这个构造方法主要做了什么呢,我们可以看下webpack/lib/Compiler.js这个文件

Snipaste_2022-03-28_23-16-08.png

new Compiler这个构造函数是会初始化各种各样的hooks,而之前说process里面的plugin里会注册这些hooks,这些hooks来自一个叫tapable的库来管理的,这是由webpack 官方自己来维护的一个库,对于tapable这个库的介绍使用可以看我另一篇webpack文章webpack5之Loader和Plugin的实现

现在我们来看看Compiler内的run方法,其实主要是执行之前plugin注册的hooks

Snipaste_2022-03-28_23-29-53.png

而在Compiler里面的run方法里,又定义了一个run方法,那我们看下这里做了什么

1. 执行run

  1. 首先执行了hooks.beforeRun,执行一些需要运行前操作的plugin
  2. 再执行了hooks.run,执行一些需要运行开始需要操作的plugin
  3. 执行compile方法,并传入了onCompiled编译完成的回调

2. 执行compile

当执行到this.compile就是开始准备编译了,我们来看看compile里面做了什么

Snipaste_2022-03-28_23-44-45.png

  1. 执行hooks.beforeCompile
  2. 执行hooks.compile
  3. 执行hooks.make
  4. 执行hooks.finishMake
  5. 执行hooks.afterCompile

其实hooks.make是最终的编译过程,而在hooks.compilehooks.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

Snipaste_2022-03-29_00-07-59.png

这个插件在apply里调用applyEntryOption,而里面又调用EntryPlugin插件

Snipaste_2022-03-29_00-11-47.png

EntryPlugin插件内可以看到注册了hooks.make

Snipaste_2022-03-29_00-12-39.png

而在注册回调中主要执行了compilation.addEntry,那我们来看看在compilation这个对象中主要做了什么

2. webpack/lib/Compilation.js

Snipaste_2022-03-29_00-24-23.png

在执行compilation.addEntry这里主要做了

  1. 执行_addEntryItem,用于添加入口的Item
  2. 执行addModuleTree
  3. addModuleTree中执行handleModuleCreation
  4. handleModuleCreation中执行factorizeModule,添加hooksfactorizeQueue队列中
  5. handleModuleCreation中执行addModule,添加module模块到addModuleQueue队列中
  6. addModule中执行buildModule,将需要构建的module模块添加到buildQueue队列中
  7. buildQueue队列中有一个processor属性,执行_buildModule
  8. _buildModule中执行module.needBuild判断模块是否需要构建
  9. 执行module.build
  10. 最后会在wepack/lib/NormalModule.js中执行build方法,开始构建模块

Snipaste_2022-03-29_00-44-51.png

module的build阶段

上面在处理module的最后在wepack/lib/NormalModule.js中执行build方法,开始构建模块,那现在我们来看看build做了哪些内容

wepack/lib/NormalModule.js

1. 执行doBuild

Snipaste_2022-03-29_11-21-45.png

2. 执行_doBuild

执行doBuild内的_doBuild方法

Snipaste_2022-03-29_11-18-33.png

3. 执行runLoaders

执行_doBuildrunLoaders,这个runLoaders来自独立的loader-runner库,我们之前配置的各种Loaders就是在这里处理的

4. 执行processResult

runLoaders执行结束后回执行processResult这个回调

Snipaste_2022-03-29_11-21-45.png

5. 执行parse

之后会调用parse解析AST

Snipaste_2022-03-29_11-33-12.png

而这个parse来自webpack/lib/javascript/JavascriptParser.js内的parse

Snipaste_2022-03-29_11-40-25.png

这个parse其实是用到了acorn这个库来解析javascript

Snipaste_2022-03-29_11-42-02.png

6. 执行handleBuildDone

解析完后会调用handleParseResult回调,里面执行handleBuildDone

Snipaste_2022-03-29_11-44-42.png

handleBuildDone里又执行了build里面传进来的回调

Snipaste_2022-03-29_11-44-42.png

Snipaste_2022-03-29_11-54-59.png

最终执行的webpack/lib/Compilation.js下的module.build传进来的回调

Snipaste_2022-03-29_11-55-52.png

7. _buildModule执行完成

_buildModule执行完成后,最终hooks.make执行完成,于是接下来会执行webpack/lib/Compiler.jscompilation.finishcompilation.seal方法

Snipaste_2022-03-29_14-18-06.png

seal这一步,就是开始将静态资源输出到构建目录了

输出静态资源到构建目录

1. 执行hooks.optimizeChunkModules

首先执行hooks.optimizeChunkModules,优化之前模块代码

Snipaste_2022-03-29_15-33-03.png

2. 执行codeGeneration

执行codeGeneration,生成代码

Snipaste_2022-03-29_15-34-24.png

3. 执行createChunkAssets

执行createChunkAssets,创建chunkAssets资源

Snipaste_2022-03-29_15-38-32.png

4. 执行getRenderManifest

执行createChunkAssets内的getRenderManifest,将所有的数据放到一个了manifest的对象中

Snipaste_2022-03-29_15-38-59.png

5. 执行emitAsset

执行emitAsset,输出资源,此时资源已存放在内存中

Snipaste_2022-03-29_15-41-50.png

6. 执行onCompiled

最终webpack/lib/Compilercompile完成后执行回调onCompiled

Snipaste_2022-03-29_15-43-54.png

7. 执行emitAssets

onCompiled回调里执行emitAssets

Snipaste_2022-03-29_15-45-53.png

8. 执行hooks.emit

最终在emitAssets内执行hooks.emit将资源导出到构建目录

Snipaste_2022-03-29_15-47-28.png

结尾

源码的介绍可能还是有欠缺不完全的地方,我们在查看源码的时候可以时候vscode的debugger工具,通过打断点来查看代码走向,通过上面的介绍应该能大致理清webpack执行的流程,但是更细节的地方还是希望大家能够debugger来摸索更。

猜你喜欢

转载自juejin.im/post/7080447622300827655