webpack5 源码阅读

01 webpack启动

  1. 执行 npm run build 相当于命令行中执行 npx webpack
  2. 命令行中执行 npx webpack 时则会查找 node_modules 目录下的 bin 目录里的 webpack.cmd 文件
  3. 在 webpack.cmd 文件执行过程中最终会找到 webpack/bin/webpack.js , 调用 runCli 方法
  4. 执行 runCli 方法时会加载 webpack-cli 目录下的 bin/cli.js , 最终会执行 bootstrap.js 文件
  5. 在 bootstrap.js 当中会执行 new WebpackCLI 获取 cli 实例
  6. cli 调用 run 方法,回到 webpack-cli.js ,在 run 方法内部使用 commander 处理命令行参数
  7. 上述方法执行时内部会触发 action 定义的回调,在ation 回调内部会执行 loadCommandByName
  8. 在 loadCommandByName 内部会执行 makeCommand, 之后执行 runWebpack,内部执行 createComiler 方法
  9. 在 createCompiler 内部会将处理好的配置信息传递给 webpack 函数, 然后返回一个 compiler 对象,由它来控制打包过程
  1. const path = require('path')
  2. module.exports = {
  3. mode: 'development',
  4. devtool: 'source-map',
  5. context: path.resolve(__dirname, '.'),
  6. entry: './src/index.js',
  7. output: {
  8. filename: 'bundle.js',
  9. path: path.resolve(__dirname, 'dist')
  10. },
  11. module: {
  12. rules: [
  13. {
  14. test: /\.js$/,
  15. use: [
  16. { loader: 'babel-loader' }
  17. ]
  18. }
  19. ]
  20. }
  21. }

02 webapck 源码包使用

  • webpack仓库下载源码包,需要执行 npm install 安装本包所需要的依赖
  • 项目下新建自己的目录,包含入口和被依赖的文件,如果遇到 es6 module 语法不支持的情况。可以修改 eslintrc 文件
  • 在配置文件中设置 sourceType: ‘module’, 将 extends 继承的配置项中 node/recommended 去除(或许非必须)
  • 新建 webpack.config.js 文件,设置基础的配置
  • 新建 run.js 测试 webpack 使用(此处 node>14.17.x)
  1. const webpack = require('../lib/webpack.js') // 使用 lib 目录下的 webpack.js
  2. const config = require('./webpack.config')
  3. const compiler = webpack(config)
  4. compiler.run((err, stats) => {
  5. console.log(stats)
  6. })

03 webpack.js 执行分析

3.1 webpack 函数

判断是否传入了 callback , 无论传入与否都会调用 create 然后都会返回 compiler 对象

3.2 create 函数

  • 定义 compiler 对象
  • 判断用户传入的 options 是否为数组
  • 我们一般使用 webpack 时传入的参数都是对象,因此直接调用 createCompiler 方法
  • 返回 compiler 对象,最终由 webpack 函数的 return 返回

3.3 createCompiler 方法

  • 通过 new 操作创建 compiler 对象(内部会在 compiler 对象身上初始化很多的 hooks)
  • 注册所有的插件(配置文件当中设置的 plugins 选项值)
    • 如果配置中是以函数的方式传入插件则使用 call 方法进行调用处理
    • 我们使用 webpack时一般都是以操作对象的方式来设置插件,此时直接调用 apply 方法
  • 调用 environment | afterEnvironment 回调方法
  • 调用 process 方法处理 plugins 之外的所有配置选项

3.4 webpackOptionsApply 模块

  • 核心Process方法,接收 options 与 compiler 两个参数, 其中 options 就是传入的打包参数, compiler 就是之前的 compiler 对象实例
  • process 方法的核心就是将传入的 options 配置转成 webpack 的 Plugin 注入到 webpack 的生命周期当中
  • 例如: entry devtool output 等等,所以这里也有一个结论:webpack 内部将所有配置都处理成了 plugin
  • 处理成 plugin 的处理就是可以将 compiler 对象传递给 apply 方法,然后在当前插件的 apply 方法中就可以执行 tap 来触发钩子的回

04 compiler 实例化

核心就是调用 run.run

  • 在 compiler 类的构造方法中定义了很多的 hooks , 用于在 webpack 生命周期中进行事件的注册和回调
  • Run 方法
    • compiler 的下面会一个 run 方法,外层执行 webpack 方法的时候会返回 compiler ,内部会执行 copmiler.run()
    • 在 run 方法内部又定义了几个方法,例如 finalcallback onCompiled ….. ,但最终还是会执行它里面的 run 方法
    • run 方法执行时会触发两个钩子,最终会执行 compile 方法

4.1 compile 方法

4.1.1 compiler 与 compilation 区别
  • webpack 打包生命周期:before run beforeCompiler compile mak finishMake afterCompiler done
  • compiler
    • 在 webpack 构建之前就会被创建的一个对象,并且贯穿整个生命周期
    • 只要执行打包编译操作就会先创建一个 compiler 对象
  • compilation
    • 只有到了准备模块编译的时候才会创建 compilation 对象
    • 它是存在于 compile 与 make 这两个阶段之间
  • 理解
    • 如果配置中添加了 watch ,就意味着我们希望源代码发生改变的时候就重新编译模块
    • 此时 compiler 无须重新创建,还是使用之前的就可以, 而 compilation 是需要重新创建的,然后完成 watch 对应操作
    • 如果修改了 webpack 配置, 那么需要重新执行 npm run build,此时就会重新创建 compiler 对象

4.1.2 compile 执行
  • 核心操作就是创建一个 compilation 对象,然后由它完成本次打包编译工作
  • 通过不同的 hooks 钩子来触发相应的回调, 然后组合在一起形成完整的 compile 流程
  • beforeCompile compile make finishMake afterMake
  • 由上述的名词不难推出真正的打包编译操作应该是在 make 阶段,但是此处只显示了 make 钩子 call, 需要定位它的注册

05 make钩子

思考:去哪找 make 钩子的注册

5.1 注册时机

  • make 是用来实现 webpack 打包的,而打包最初需要的应该是明确入口,入口是在 options 中配置的
  • 在 compiler 对象的创建阶段就已经将所有的 options 都转为了插件,便于在整个生命周期中进行使用
  • 定位 new Compiler 再次找到 process 方法的调用,定位 entry 的处理, 这里需要注意 make 是由 compiler.run 引起的一个流程,因此配置转plugin 是在 new Compiler 时完成,换句话说 make 钩子触发之前, 相关的钩子注册工作已经完成了。所以可以正常使用

5.2 注册位置

  • process 负责将 options 插件化,其中就包含了 Entry 的处理
  • 在上述的方法中执行了 new EntryOptionPlugin().apply 操作,在apply 内部触发了 applyEntryOption 静态方法
  • 在 applyEntryOption 这个静态方法中对 entry 进行了判断和处理, 我们一般传入是非函数
  • 对 entry 进行遍历, 然后执行 new EntryPlugin(context, entry, options).apply 方法,在内部执行了 make 的 tap

5.3 make 钩子任务

追溯到 webpack 的打包流程, make 钩子被触发时就会执行这里的任务

  • 在 make 钩子注册的回调任务当中执行了 addEntry 方法,该方法是属于 compilation 的成员方法
  • 在 addEntry 当中执行了 _addEntryItem 私有方法
  • 上述的私有方法中调用了 addModuleTree 处理依赖树
  • 在 addModuleTree 方法中调用了 handleModuleCreation 方法来处理并创建 module
  • 在上述方法中调用了 factorizeModule 来获取 newModule
  • 获取 newModule 之后调用 addModule 将 newModule 添加至相应的队列中
  • 同时还调用了 buildModule 将 module 添加至相应的队列,队列的身上存在处理模块的 processor
  • 通过执行 processor 的 _buildModule 方法来完成最终的 build , 内部执行了 module.build 方法进行最终实现

所以make钩子的注册的任务核心就是完成 模块的 build 操作

06 build 流程

6.1 报错
  • 查看 build 具体的实现时发现定位到了 Module 当中的错误提示,这是因为 Module 是一个抽象,专门用于继承,内部并没有真正的实现 build 方法
  • 此处真正实现 build 方法的是 NormalModule 模块,它内部有一个 build

6.2 runLoaders
  • 分析 build 执行发现它的内部又执行了 doBuild 操作
  • 在 doBuild 方法内部执行了 runLoaders 方法,且这个方法是一个外部方法,第三方库导入此处不研究
  • 执行 runLoaders 的目的最终也是为了读取相应路径的文件内容,然后交由 processResult 处理
  • 执行 processResult 时,不论过程如何最终都会执行 callback , 此 callback 就是在执行 doBuild 的时候传入的
  • 在 callback 内部有一个核心的 parse 操作,该步是将 loader 读取到的内容交由 ast 语法树进行实现(acorn库)
  • 之所以需要 ast 是因为要判断当前被打包模块是否依赖了其它模块,如果依赖了则可以递归进行处理

到此模块的 build 算是结束了,余下的就是对 build 的内容进入处理

07 seal 流程

在 make 工作完成之后会经历 finishMake 阶段,之后调用了 seal

核心就是将 webpack 打包之后的 modules 数据保存起来

7.1 seal 流程

  • compilation 在之前已经将本次打包需要处理的模块都放到了 modules 当中
  • 遍历处理好的 modules ,然后依据配置生成不同的 chunk
  • 不同的 chunk 可能会存在不同的优化处理
  • 将优化处理好的 chunk 保存在 compilation 的 assets 属性上

7.2 seal 逻辑

  • 生成相应的 chunk 图
  • 触发各种 optimize 相关的钩子
  • 优化操作完成之后,执行 optimizeChunkModules 方法的回调
  • 在上述方法的内部执行 codeGeneration 实现最终 code 的生成工作

7.3 codeGeneration 方法

  • 内部最终调用了 _runCodeGenerationJobs ,传入 callback
  • _runCodeGenerationJobs 方法内部使用了asyncLib 库遍历jobs ,同时也传入了 callback
  • 不论上述的处理结果如何,最终都执行了调用之处传入的 callback ,而这个 callback 是我们调用 codeGeneration 方法时传入的
  • 在 codeGeneration 的 callback 当中我们会执行 createChunkAssets 方法来实现最终 chunkAssets的生成

7.4 createChunkAssets 方法

  • 定义 manifest 对象,将所有的数据都保存在这个对象上
  • 调用 manifest 对象的 render 方法,拿到最终的 source
  • 拿到 source 之后执行 emitAsset 方法将 source 写入到相应的 file

7.5 emitAsset 方法

  • 核心就是将打包之后的资源放到 assets 属性上
  • this.assets[file] = source

7.6 emitAsset 流程

  1. 上述的操作执行完成之后就开始执行 finalCallback 操作, 而这个 callback 就是调用 seal 方法时传入
  2. seal 方法的调用是在 compiler 内完成
  3. 调用 seal 方法后会执行 afterCompile 钩子, 此时会执行 callback ,这个 callback 就是 compile 调用时传入
  4. 调用 compile 时传入的就是 onCompiled
  5. 执行 onCompiled 回调的时候会执行 emitAssets 方法,将打包后资源真正的写入到磁盘操作