本地测试代码
日常工作中一般都是基于指令进行打包,如npx webpack。但在源码调试中,指令不方便打断点,而且webpack中的Compiler类中存在一个run方法,和指令打包结果输出一致,因此,在调试源码时使用run进行打包。具体代码如下(之后的源码分析都基于该代码):
// 引入webpack模块let webpack = require('webpack')/** 引入设置的webpack配置项{devtool: 'none',mode: 'development',context: process.cwd(),entry: './src/index.js',output: {filename: 'index.js',path: path.resolve('dist')}}*/let options = require('./webpack.config')let compiler = webpack(options)// 执行run方法执行编译compiler.run((err, stats) => {console.log(err)console.log(stats.toJson({entries: true,chunks: false,modules: false,assets: false}))})
在开始之前,简单说一下Tapable库,其核心原理是设计模式中的发布订阅模式。
const test = [];// 可以简单理解成通过 类型a.tap 往 test数组中添加函数a.tap => test.push(fn)// 类型a.call 通过循环test中每一项,执行对应的函数a.call => test.forEach(fn => fn())
从入口开始查找
一般查看源码时,都从package.json中的main入口查找。由此可以得出,打包从lib/webpack.js开始。
{"name": "webpack","version": "4.45.0",...,"main": "lib/webpack.js",...}
粗略阅读lib/webpack.js后,发现导出的是一个webpack对象。该对象主要执行了如下步骤:
- 根据传入的配置项,设置webpack默认配置项
- 创建compiler对象
- 给pulgin插件设置权限
- 挂载所有webpack内部插件
- 如果我们传入了回调函数,内部会偷偷调用run方法,就和之前写的测试代码中一致
- 返回compiler对象
/** options: 传入的webpack配置项* callback: 回调函数*/const webpack = (options, callback) => {// 声明compiler// compiler的主要引擎,记录了完整的上下文信息。在整个生命周期中,只会生成一次let compiler;// 如果是配置项类型是数组类型// falseif (Array.isArray(options)) {...// // 如果是配置项类型是对象类型} else if (typeof options === "object") {// 设置默认配置项options = new WebpackOptionsDefaulter().process(options);// 设置Compiler类compiler = new Compiler(options.context);compiler.options = options;// 给pulgin插件设置权限new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);// 如果存在pulgin,判断是函数类型还是对象if (options.plugins && Array.isArray(options.plugins)) {for (const plugin of options.plugins) {// 如果是函数类型,则将this指向compiler,并传参compilerif (typeof plugin === "function") {plugin.call(compiler, compiler);} else {// 调用apply方法并传参compiler// 此处也是为什么,写插件的时候需要提供一个apply方法plugin.apply(compiler);}}}// 此处是引入的Tapable// 执行Tapable中的environment和afterEnvironment钩子函数compiler.hooks.environment.call();compiler.hooks.afterEnvironment.call();// 挂载所有webpack内部插件入口compiler.options = new WebpackOptionsApply().process(options, compiler);} else {throw new Error("Invalid argument: options");}if (callback) {// 不是函数类型报错if (typeof callback !== "function") {throw new Error("Invalid argument: callback");}// 热更新相关,暂时不看if (options.watch === true ||(Array.isArray(options) && options.some(o => o.watch))) {...}// 内部调用run方法compiler.run(callback);}return compiler;}// 导出webpack对象exports = module.exports = webpack;
01 => webpack默认项
通过实例化WebpackOptionsDefaulter类,设置默认项
class OptionsDefaulter extends OptionsDefaulter {// 设置默认项constructor() {this.set("entry", "./src");this.set("context", process.cwd());this.set("target", "web");this.set("devtool", "make", options =>options.mode === "development" ? "eval" : false);this.set("output.filename", "[name].js");...}}
02 => 创建compiler对象
通过实例化WebpackOptionsDefaulter类,设置默认项
// Compiler 继承 Tapable 库class Compiler extends Tapable {constructor(context) {super();// 往this.hooks绑定对应的 Tapable 中的钩子函数// 后续代码中如果出现 compiler.hooks.xxx ,都是指向的 Tapable 中的钩子this.hooks = {done: new AsyncSeriesHook(["stats"]),beforeRun: new AsyncSeriesHook(["compiler"]),run: new AsyncSeriesHook(["compiler"]),emit: new AsyncSeriesHook(["compilation"]),compile: new SyncHook(["params"]),make: new AsyncParallelHook(["compilation"]),...}// 绑定一些属性,方便后续使用this.name = undefined;this.outputPath = "";this.options = {};this.context = context;...}}
03 => 给plugin插件设置权限
入口文件中,发现是通过实例化NodeEnvironmentPlugin类并调用内部apply方法给plugin增加功能,内部核心都是NodeJs的fs模块。具体源码如下:
class NodeEnvironmentPlugin {// 接受传入的参数constructor(options) {this.options = options || {};}apply(compiler) {...// 内部核心都是fs模块// 增加修改文件权限compiler.inputFileSystem = new CachedInputFileSystem(new NodeJsInputFileSystem(),60000);// 增加导出文件权限compiler.outputFileSystem = new NodeOutputFileSystem();}}
以NodeOutputFileSystem类为例,其源码如下:
const fs = require("fs");const path = require("path");const mkdirp = require("mkdirp");class NodeOutputFileSystem {constructor() {this.mkdirp = mkdirp;this.mkdir = fs.mkdir.bind(fs);this.rmdir = fs.rmdir.bind(fs);this.unlink = fs.unlink.bind(fs);this.writeFile = fs.writeFile.bind(fs);this.join = path.join.bind(path);}}module.exports = NodeOutputFileSystem;
04 => 挂载内部所有插件
最终找到了EntryOptionPlugin类,这里是打包流程的入口
class WebpackOptionsApply {process(options, compiler) {// 设置compiler的属性compiler.outputPath = options.output.path;compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;compiler.recordsOutputPath =options.recordsOutputPath || options.recordsPath;compiler.name = options.name;compiler.dependencies = options.dependencies;...// 这里是一个打包流程的入口// 实例化EntryOptionPlugin类并调用apply方法new EntryOptionPlugin().apply(compiler);// 调用Tapable的entryOption钩子compiler.hooks.entryOption.call(options.context, options.entry);...}}
