webpack5 源码阅读
01 webpack启动
- 执行 npm run build 相当于命令行中执行 npx webpack
- 命令行中执行 npx webpack 时则会查找 node_modules 目录下的 bin 目录里的 webpack.cmd 文件
- 在 webpack.cmd 文件执行过程中最终会找到 webpack/bin/webpack.js , 调用 runCli 方法
- 执行 runCli 方法时会加载 webpack-cli 目录下的 bin/cli.js , 最终会执行 bootstrap.js 文件
- 在 bootstrap.js 当中会执行 new WebpackCLI 获取 cli 实例
- cli 调用 run 方法,回到 webpack-cli.js ,在 run 方法内部使用 commander 处理命令行参数
- 上述方法执行时内部会触发 action 定义的回调,在ation 回调内部会执行 loadCommandByName
- 在 loadCommandByName 内部会执行 makeCommand, 之后执行 runWebpack,内部执行 createComiler 方法
- 在 createCompiler 内部会将处理好的配置信息传递给 webpack 函数, 然后返回一个 compiler 对象,由它来控制打包过程
const path = require('path')
module.exports = {
mode: 'development',
devtool: 'source-map',
context: path.resolve(__dirname, '.'),
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: [
{ loader: 'babel-loader' }
]
}
]
}
}
02 webapck 源码包使用
- webpack仓库下载源码包,需要执行 npm install 安装本包所需要的依赖
- 项目下新建自己的目录,包含入口和被依赖的文件,如果遇到 es6 module 语法不支持的情况。可以修改 eslintrc 文件
- 在配置文件中设置 sourceType: ‘module’, 将 extends 继承的配置项中 node/recommended 去除(或许非必须)
- 新建 webpack.config.js 文件,设置基础的配置
- 新建 run.js 测试 webpack 使用(此处 node>14.17.x)
const webpack = require('../lib/webpack.js') // 使用 lib 目录下的 webpack.js
const config = require('./webpack.config')
const compiler = webpack(config)
compiler.run((err, stats) => {
console.log(stats)
})
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 流程
- 上述的操作执行完成之后就开始执行 finalCallback 操作, 而这个 callback 就是调用 seal 方法时传入
- seal 方法的调用是在 compiler 内完成
- 调用 seal 方法后会执行 afterCompile 钩子, 此时会执行 callback ,这个 callback 就是 compile 调用时传入
- 调用 compile 时传入的就是 onCompiled
- 执行 onCompiled 回调的时候会执行 emitAssets 方法,将打包后资源真正的写入到磁盘操作