Skip to content

gweid/webpack-source-code

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

webpack 源码阅读

基于 webpack5.24

准备工作

学会调试是阅读 webpack 源码的基础,下面来配置下调试环境

1、首先,下载 webpack 的源码:https://github.com/webpack/webpack

2、接着,进入到 webpack 源码目录,通过 npm 装包

3、然后,在 webpack 源码目录同级目录新建一个自己的项目 myProgram,用于调试 webpack

做完上面三部,有基本目录结构:

webpack-source-code
├── webpack-master   // webpack 源码
├── myProgram        // 调试 webpack 源码的项目
│   ├── src
│   │   ├── utils
│   │   ├── └── math.js
│   │   └── index.js
│   ├── build.js
│   ├── package-lock.json
│   ├── package.json
│   └── webpack.config.js
├── .gitignore
└── readme.md

4、调试项目中,使用 webpack

build.js:

const webpack = require('../webpack-master/lib/webpack')
const config = require('./webpack.config')

// 执行 webpack 函数有传回调函数
// const compiler = webpack(config, (err, stats) => {
//   if (err) {
//     console.log(err)
//   }
// })

// 执行 webpack 函数没有有传回调函数
const compiler = webpack(config)
// 需要手动调用一下 compiler.run
compiler.run((err, stats) => {
  if (err) {
    console.log(err)
  }
})

在 build.js 文件中,执行 webpack 函数,将 webpack.config.js 配置传进去

webpack.config.js:

const path = require('path')

module.exports = {
  entry: './src/index.js',
  context: path.resolve(__dirname, "."), // 这个必须有,不然会有问题
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: "development",
	devtool: "source-map",
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader']
      }
    ]
  },
  plugins: []
}

5、debug 调试:使用 vscode 内置的 debug 工具

然后文件内写入:

后续即可通过断点,进行 webpack 源码单点调试

webpack 的 compiler

webpack 有两个主要的核心对象,一个是 compiler,另外一个是 compilation

1、从 webpack(config, callback) 函数开始

webpack 函数接收两个参数:

  • config:就是 webpack.config.js 中的配置
  • callback:回调函数,可传可不传

webpack 函数来自于 webpack/lib/webpack.js,进入到文件里面,可以看到:

webpack-master\lib\webpack.js

const webpack = (options, callback) => {
    const create = () => {
        // 1.定义 compiler、watch、watchOptions
        let compiler;
        let watch = false;
        let watchOptions;
        
         // ...
        
        // 2.通过 createCompiler 创建一个 compiler
        compiler = createCompiler(options);
        // 拿到 webpack.config.js 中的 watch、watchOptions
        watch = options.watch;
        watchOptions = options.watchOptions || {};
        
        // 返回 compiler, watch, watchOptions
	    return { compiler, watch, watchOptions };
    }
    
    /**
	* 判断在执行 webpack 函数的时候,有没有传入 callback 回调函数
	* 无论有没有传入回调函数,结果都是会返回一个 compiler 对象
	*/
    if (callback) {
        try {
            const { compiler, watch, watchOptions } = create();
            if (watch) {
                // config 文件有没有配置 watch,如果有,会监听文件改变,重新编译
                compiler.watch(watchOptions, callback);
            } else {
                compiler.run((err, stats) => {
                    compiler.close(err2 => {
                        callback(err || err2, stats);
                    });
                });
            }
            return compiler;
        } catch (err) {
        }
    } else {
        // 执行 webpack 的时候没有传入回调函数

        // 执行 create 函数,拿到 compiler, watch
        const { compiler, watch } = create();
        if (watch) {
        }
        return compiler;
    }
}

可以看出,在 webpack() 内,首先,定义了 create 函数,create 函数主要做的事是:

  1. 定义了 compiler 对象及一些其他参数
  2. 通过 createCompiler(options) 创建 compiler
  3. 将 compiler 返回

接着,会判断 webpack 函数执行的时候有没有传入 callback:

  • 传入了 callback,通过 create 函数拿到 compiler 对象,执行 compiler.run,并把 compiler 返回
    • 里面还有一层判断 config 文件有没有配置 watch,如果有,会监听文件改变,重新编译
  • 没有传入 callback,通过 create 函数拿到 compiler 对象,直接返回 compiler

所以,我们在调用 weback 函数的时候,callback 可以传也可以不传,不传 callback 就需要手动调用一下 compiler.run,例如:

const compiler = webpack(config)
compiler.run((err, stats) => {
  if (err) {
    console.log(err)
  }
})

2、创建 compiler

由上面的 webpack() 可以看出,compiler 由 createCompiler 这个函数返回,看看 createCompiler 这个函数

webpack-master\lib\Compiler.js

// 创建 compiler
const createCompiler = rawOptions => {
	// 格式化、初始化传进来的参数(如 output、devserver、plugin 给赋值一些默认的配置格式,防止后面使用时报错)
	// getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最终的 webpack 配置
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);

	// 通过 new Compiler 得到一个 compiler 对象
	const compiler = new Compiler(options.context);
	// 将 options(经过格式化后的 webpack.config.js )挂载到 compiler 上
	compiler.options = options;

	// 把 NodeEnvironmentPlugin 插件挂载到compiler实例上
	// NodeEnvironmentPlugin 插件主要是文件系统挂载到compiler对象上
	// 如 infrastructureLogger(log插件)、inputFileSystem(文件输入插件)、outputFileSystem(文件输出插件)、watchFileSystem(监听文件输入插件)
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);

	// 注册所有的插件
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				// 如果插件是一个函数,用 call 的形式调用这个函数,并把 compiler 当参数
				plugin.call(compiler, compiler);
			} else {
				// 如果插件是对象形式,那么插件身上都会有 apply 这个函数,调用插件的 apply 函数并把 compiler 当参数
				// 写一个 webpack 插件类似:class MyPlugin = { apply(compiler) {} }
				plugin.apply(compiler);
			}
		}
	}
	applyWebpackOptionsDefaults(options);

	// 调用 compiler 身上的两个钩子 environment、afterEnvironment
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();

	// WebpackOptionsApply().process 主要用来处理 config 文件中除了 plugins 的其他属性
	// 这个东西非常重要,会将配置的一些属性转换成插件注入到 webpack 中 
	// 例如:入口 entry 就被转换成了插件注入到 webpack 中 
	new WebpackOptionsApply().process(options, compiler);
	// 调用 initialize 钩子
	compiler.hooks.initialize.call();
	// 返回 compiler
	return compiler;
};

createCompiler 函数主要的逻辑就是:

  1. 格式化、初始化传进来的参数格式化、初始化传进来的参数(如 output、devserver、plugin 给赋值一些默认的配置格式,防止后面使用时报错)
    • getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最终的 webpack 配置
  2. 通过 new Compiler 得到一个 compiler 对象
  3. 将 options(经过格式化后的 webpack.config.js )挂载到 compiler 上
  4. NodeEnvironmentPlugin 把文件系统挂载到 compiler 对象上
    • 如 infrastructureLogger(log插件)、inputFileSystem(文件输入插件)、outputFileSystem(文件输出插件)、watchFileSystem(监听文件输入插件) 等
  5. 注册所有的插件
    • 如果插件是一个函数,用 call 的形式调用这个函数,并把 compiler 当参数
    • 如果插件是对象形式,那么插件身上都会有 apply 这个函数,调用插件的 apply 函数并把 compiler 当参数
  6. 调用 compiler 身上的一些钩子
  7. WebpackOptionsApply().process 处理 config 文件中除了 plugins 的其他属性
  8. 返回 compiler

看看 WebpackOptionsApply().process 这个函数,这个函数非常重要,会将配置的一些属性转换成插件注入到 webpack 中,例如:入口 entry 就被转换成了插件注入到 webpack 中

webpack-master\lib\WebpackOptionsApply.js

class WebpackOptionsApply extends OptionsApply {
    // ...
    
    /**
	 * 这个方法的作用:将传入的 webpack.config.js 的属性(例如 devtool)转换成 webpack 的 plugin 注入到 webpack 的生命周期中
	 * 导入后就是进行例如:new ChunkPrefetchPreloadPlugin().apply(compiler) 的过程
	 * 这些 plugin 后续将通过 tapable 实现钩子的监听,并进行自身逻辑的处理
	 * 
	 * 所以,在 webpack 中,插件是非常重要的,贯穿了整个 webpack 的生命周期
	 */
    process(options, compiler) {
        // 根据各种配置情况,决定是否使用一些 plugin
        if (options.externals) {
          //@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
          const ExternalsPlugin = require("./ExternalsPlugin");
          new ExternalsPlugin(options.externalsType, options.externals).apply(
            compiler
          );
        }
            if (options.externalsPresets.node) {
          const NodeTargetPlugin = require("./node/NodeTargetPlugin");
          new NodeTargetPlugin().apply(compiler);
        }
        if (options.externalsPresets.electronMain) {
          //@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
          const ElectronTargetPlugin = require("./electron/ElectronTargetPlugin");
          new ElectronTargetPlugin("main").apply(compiler);
        }

            //... 这里是一堆根据 webpack.config.js 的配置转换成 plugin 的操作

            // 处理入口,将 entry: '', 转换成 EntryOptionPlugin 插件进行注入
        new EntryOptionPlugin().apply(compiler);
        compiler.hooks.entryOption.call(options.context, options.entry);

        //....
    }
}

compiler 是通过 new Compiler 这个类得到的,而 new 一个类最主要的就是初始化这个类的 constructor,现在来看看 Compiler 这个类:

webpack-master\lib\Compiler.js

class Compiler {
    constructor(context) {
        // 初始化了一系列的钩子
        // 使用的是 tapable
        // tapable,简单来说,就是一个基于事件的流程管理工具,主要基于发布订阅模式实现
        this.hooks = Object.freeze({
            initialize: new SyncHook([]),
            
            // ...
            
            beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"])
            
            // ...
        })
        
        this.webpack = webpack;
        
        this.name = undefined;
        this.parentCompilation = undefined;
        this.root = this;
        this.outputPath = "";
        this.watching = undefined;
        
        // ...
    }
}

可以看出,new Compiler 最主要的就是做了两件事:

  • 使用 tapable 初始化了一系列的钩子
    • tapable 是什么?简单来说,就是一个基于事件的流程管理工具,主要基于发布订阅模式实现
    • 这里暂时不展开说 tapable,后面再单独解析
  • 初始化了一些参数,例如 this.outputPath 等等

自此,一个 compiler 对象就创建完成

3、compiler.run()

再回到 build.js 文件 和 webpack() 这个函数:

build.js:

// 执行 webpack 函数有传回调函数
// const compiler = webpack(config, (err, stats) => {
//   if (err) {
//     console.log(err)
//   }
// })

// 执行 webpack 函数没有有传回调函数
const compiler = webpack(config)
// 需要手动调用一下 compiler.run
compiler.run((err, stats) => {
  if (err) {
    console.log(err)
  }
})

webpack() 函数:

webpack-master\lib\webpack.js

const webpack = (options, callback) => {
    if (callback) {
        const { compiler, watch, watchOptions } = create();

        compiler.run((err, stats) => {
            // ...
        });

        return compiler;   
    }
}

可以看到,执行 webpack() 函数的重要一步就是调用 compiler.run,现在回到 Compiler 这个类身上,看看 run 方法:

webpack-master\lib\Compiler.js

class Compiler {
    // ...

    run(callback) {
    // ...
        
    // 处理错误的函数
		const finalCallback = (err, stats) => {
      // ...
            
			if (err) {
				this.hooks.failed.call(err);
			}
			this.hooks.afterDone.call(stats);
		};
        
    // 定义了一个 onCompiled 函数,主要是传给 this.compile 作为执行的回调函数
    const onCompiled = (err, compilation) => {}
        
    // run,主要是流程: beforeRun 钩子 --> beforeRun 钩子 --> this.compile
		// 如果遇到 error,就执行 finalCallback
		// 这里调用 beforeRun、run 主要就是提供 plugin 执行时机
		const run = () => {
            // 执行 this.hooks.beforeRun.callAsync,那么在 beforeRun 阶段注册的 plugin 就会在这时执行
			this.hooks.beforeRun.callAsync(this, err => {
				if (err) return finalCallback(err);
                
                // this.hooks.run.callAsync,在 run 阶段注册的 plugin 就会在这时执行
				this.hooks.run.callAsync(this, err => {
					if (err) return finalCallback(err);

					this.readRecords(err => {
						if (err) return finalCallback(err);

						this.compile(onCompiled);
					});
				});
			});
		};
        
    run();
    }
}

从上面可以看出,compiler.run 的主要逻辑:

  1. 定义了一个错误处函数 finalCallback
  2. 定义了一个 onCompiled 函数,作为 this.compile 执行的回调函数
  3. 定义了 run,主要流程就是:beforeRun 钩子 --> run 钩子 --> this.compile,如果遇到 error,就执行 finalCallback
  4. 执行 compiler.run 内部定义的 run()

而 compiler.run 内部定义的 run 实际上就是调用了 this.compile,也就是 Compiler 上的 compile 方法

4、compiler.compile ()

现在看看 compiler.run 里面调用的 compile 函数

webpack-master\lib\Compiler.js

class Compiler {
    // ...
    
    compile(callback) {
        // 初始化 compilation 的参数
		const params = this.newCompilationParams();
        
      // 钩子 beforeCompile
      this.hooks.beforeCompile.callAsync(params, err => {
      // 钩子 compile
			this.hooks.compile.call(params);
            
      // 通过 this.newCompilation 返回一个 compilation 对象
			const compilation = this.newCompilation(params);
            
            // 钩子 make, 使用 compilation 对模块执行编译的
            this.hooks.make.callAsync(compilation, err => {
                // 钩子 finishMake
                this.hooks.finishMake.callAsync(compilation, err => {
                    process.nextTick(() => {
                        
                        // 执行compilation的 finsh 方法 对 module 上的错误或者警告处理
                        compilation.finish(err => {
                            
                            // 执行 seal 对 module 代码进行封装输出
                            compilation.seal(err => {
                                
                                // 钩子 afterCompile
                                this.hooks.afterCompile.callAsync(compilation, err => {})
                            })
                        })
                    })
                })
            })
        }
    }
}

先忽略 compilation 相关的,那么可以看到,compiler.compile() 里面主要就是 5 个钩子的调用

  • 钩子 beforeCompile
  • 钩子 compile
  • 钩子 make:使用 compilation 对模块执行编译的
  • 钩子 finishMake
  • compilation.finish
  • compilation.seal:执行 seal 对 make 阶段处理过的 module 代码进行封装输出
  • 钩子 afterCompile

5、run() --> compile() 的一些 hook

tapable

1、tapable 是什么

tapablewebpack的核心模块,也是webpack团队维护的,是webpack plugin的基本实现方式。主要的作用就是:基于事件流程管理

2、tapable 的基本使用

以 AsyncSeriesHook这个 hook 为例:

const { AsyncSeriesHook } = require("tapable")

// 创建一个 SyncHook 类型的 hook
const run = new AsyncSeriesHook(["compiler"])

SyncHook 就是 tapable 中提供的某一个用于创建某一类 hook 的类,通过 new 来生成实例。构造函数 constructor 接收一个数组,数组有多少项,表示生成的这个实例注册回调的时候接收多少个参数

hook 的主要有两个实例:

  • tap:就是注册事件回调的方法
  • call:就是触发事件,执行 tap 中回调的方法

所以有,注册事件回调:

run.tapAsync('myRun', (compiler) => {
    console.log(compiler)
})

这样就通过 tab 把事件回调函数注册好了。而这个事件调用时机就是在执行 call 的时候调用

run.callAsync(compiler)

执行 call,就会调用之前注册的回调,并把 compiler 当做参数传过去

所以 tabpad 的使用主要就是三步:

  1. new 一个 hook 类
  2. 使用 tab 注册回调
  3. 在 call 的时候执行回调

3、一些常用的 tapable hook

可以参考 https://juejin.cn/post/6939794845053485093

4、tapable 基本原理

可以参考:https://juejin.cn/post/6946094725703139358

compilation

首先,了解一下 compilation 以及它与 complier 的一个区别

  • compiler:webpack 刚开始构建的时候就创建了,并且在整个 wbepack 的生命周期都存在

  • compilation:在准备编译某一个模块(例如 index.js)的时候才会创建,主要存在于 compile 到 make 这一段生命周期里面

问题:既然 compiler 存在于 webpack 整个生命周期,那么为什么不直接使用 compiler 而是要搞一个 compilation 出来?

比如,通过 watch 开启对文件的监听,如果文件发生变化,那就重新编译。如果这个时候使用 compiler,那么又要进行前面的一堆初始化操作,完全没有必要,只需要对文件重新编译就好,那么就可以创建一个新的 compilation 对文件重新编译。而如果修改了 webpack.config.js 文件,重新执行 npm run build,这个时候就需要使用 compiler 了。

1、compilation 的创建

回头看看 compiler.compile() 这个函数:

webpack-master\lib\Compiler.js

class Compiler {
    // ...

    compile(callback) {
        // 初始化 compilation 的参数
		    const params = this.newCompilationParams();

        this.hooks.beforeCompile.callAsync(params, err => {
        this.hooks.compile.call(params);

        // 通过 this.newCompilation 返回一个 compilation 对象
	      const compilation = this.newCompilation(params);
      })
    }
}

可以看出来,compilation 由 this.newCompilation() 返回,来看看这个函数:

webpack-master\lib\Compiler.js

class Compiler {
    // ...
    
    newCompilation(params) {
        // 调用 this.createCompilation() 返回 compilation
      const compilation = this.createCompilation();
      compilation.name = this.name;
      compilation.records = this.records;
      this.hooks.thisCompilation.call(compilation, params);
      this.hooks.compilation.call(compilation, params);
      return compilation;
	  }
}

可以看出,compilation 又由 this.createCompilation() 返回

webpack-master\lib\Compiler.js

class Compiler {
    // ...
    
    createCompilation() {
      this._cleanupLastCompilation();
      // new Compilation 创建 compilation
      return (this._lastCompilation = new Compilation(this));
    }
}

到此,终于可以知道 compilation 其实就是通过 new 一个 Compilation 类得来,并且把 compiler 本身传递给 Compilation 的构造函数 constructor

2、compilation 的调用时机

回看上面 compiler.compile() 的代码:

webpack-master\lib\Compiler.js

class Compiler {
    //...

  	compile(callback) {
		// 初始化 compilation 的参数
		const params = this.newCompilationParams();
		
		// 钩子 beforeCompile
		this.hooks.beforeCompile.callAsync(params, err => {
			if (err) return callback(err);
			
			// 钩子 compile
			this.hooks.compile.call(params);
			
			// 通过 this.newCompilation 返回一个 compilation 对象
			const compilation = this.newCompilation(params);


			// 钩子 make,这个钩子里面就是真正使用 compilation 执行编译的
			this.hooks.make.callAsync(compilation, err => {

			});
		});
	}
}

这里面有一个非常重要的钩子调用,就是 this.hooks.make.callAsync,这个钩子里面就是真正使用 compilation 执行编译的。

那么这个钩子是什么时候被注册的呢?

1、createCompiler:

webpack-master\lib\Compiler.js

const createCompiler = rawOptions => {
    // ...
    
    new WebpackOptionsApply().process(options, compiler);
}

2、WebpackOptionsApply().process()

webpack-master\lib\WebpackOptionsApply.js

class WebpackOptionsApply extends OptionsApply {
    // ...
    
    process(options, compiler) {
        //...
        new EntryOptionPlugin().apply(compiler);
    }
}

3、再看 EntryOptionPlugin().apply(compiler)

webpack-master\lib\EntryOptionPlugin.js

class EntryOptionPlugin {
    apply(compiler) {
		compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
			EntryOptionPlugin.applyEntryOption(compiler, context, entry);
			return true;
		});
	}
    
    static applyEntryOption(compiler, context, entry) {
		if (typeof entry === "function") {
			const DynamicEntryPlugin = require("./DynamicEntryPlugin");
			new DynamicEntryPlugin(context, entry).apply(compiler);
		} else {
			const EntryPlugin = require("./EntryPlugin");
			for (const name of Object.keys(entry)) {
				const desc = entry[name];
				const options = EntryOptionPlugin.entryDescriptionToOptions(
					compiler,
					name,
					desc
				);
				for (const entry of desc.import) {
					new EntryPlugin(context, entry, options).apply(compiler);
				}
			}
		}
	}
}

可以看到,EntryOptionPlugin.apply() 主要做的事:就是调用了自身的 applyEntryOption 方法,里面对入口 entry 分情况处理,这里主要看 new EntryPlugin(context, entry, options).apply(compiler) 这个

4、然后来到 EntryPlugin.apply()

webpack-master\lib\EntryPlugin.js

class EntryPlugin {
    apply(compiler) {
      // ...

			compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
		});
	}
}

终于找到了 compiler.hooks.make.tapAsync,这就是注册了 make hook 回调函数的地方

通过流程图表示这个注册 compilation 回调的流程:

3、compilation 对模块的处理

由上面可知,compilation 是在 make 这个钩子里面执行的,而注册这个钩子的地方,绕了一圈,定位到了 lib\EntryPlugin.js 中 EntryPlugin 这个类的 apply 中 compiler.hooks.make.tapAsync 进行回调注册。现在接着从这个注册回调中分析:

webpack-master\lib\EntryPlugin.js

class EntryPlugin {
    apply(compiler) {
        // ...

		compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
			const { entry, options, context } = this;
            
            // 创建依赖
			const dep = EntryPlugin.createDependency(entry, options);
            
            // 通过 compilation 的 addEntry 添加入口,从入口开始编译
			compilation.addEntry(context, dep, options, err => {
				callback(err);
			});
		});
	}
}

可以看到,这个注册回调函数里面调用了 compilation.addEntry(),这个 Compilation 类里面的 addEntry 主要的作用就是添加入口模块

webpack-master\lib\Compilation.js

class Compilation {
    // 1 初始化 constructor
    conscructor() {
      this.addModuleQueue = new AsyncQueue({
        name: "addModule",
        parent: this.processDependenciesQueue,
        getKey: module => module.identifier(),
        // processor:处理方法,调用 this._addModule
        processor: this._addModule.bind(this)
		  });
      this.factorizeQueue = new AsyncQueue({
        name: "factorize",
        parent: this.addModuleQueue,
        // processor:处理方法,调用 this._factorizeModule
        processor: this._factorizeModule.bind(this)
      });
      this.buildQueue = new AsyncQueue({
        name: "build",
        parent: this.factorizeQueue,
        // processor:处理方法,调用 this._buildModule
        processor: this._buildModule.bind(this)
      });
    }
    
    // 2 这个函数的主要作用就是通过 _addEntryItem 添加入口,因为编译需要从入口开始
    addEntry(context, entry, optionsOrName, callback) {
      // ...

      // 添加入口
      this._addEntryItem(context, entry, "dependencies", options, callback);
    }
    
    // 3
    _addEntryItem(context, entry, target, options, callback) {
    // ...

    // 调用 addEntry 钩子
		this.hooks.addEntry.call(entry, options);
        // 调用 this.addModuleTree 将当前的模块加入到 module tree(模块树)
        this.addModuleTree({}, (err, module) => {})
    }

    // 4
    addModuleTree({ context, dependency, contextInfo }, callback) {
        // ...
        
        // 调用 handleModuleCreation 处理模块并
        this.handleModuleCreation({}, err => {})
    }

    // 5
    handleModuleCreation(
		{factory, dependencies, originModule, contextInfo, context, recursive = true},
		callback
    )
    {
        // 创建了模块图
        const moduleGraph = this.moduleGraph;
		
        // 6
        this.factorizeModule(
            {currentProfile,factory,dependencies,originModule,contextInfo,context},
            (err, newModule) => {
                
                /**
				 * compilation.factorizeModule 执行 factorizeQueue 的 add 方法将模块添加到 factorizeQueue 队列
				 * factorizeQueue 通过 new AsyncQueue 得到
				 * 
				 * factorizeQueue.add()=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation.__factorizeModule
				 * 
				 * compilation._factorizeModule 中再调用 factory.create()
				 * 通过 compilation.factorizeModule 的回调函数中接收 factorizeQueue.add 返回的 newModule
				 * 最后通过 compilation.addModule 添加 newModule 模块
				 */
                
                // 9
                this.addModule(newModule, (err, module) => {
                    
                    /**
					 * compilation.addModule 执行 addModuleQueue.add 将模块添加到 addModuleQueue
					 * addModuleQueue 通过 new AsyncQueue 创建
					 * 
					 * addModuleQueue.add=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation._addModule
					 * 
					 * 在 compilation.addModule 的回调中继续调用 compilation.buildModule
					 */
                    
                    // 12
                    this.buildModule(module, err => {
                        
                        /**
						 * compilation.buildModule 执行 buildQueue.add 将 module 添加到 buildQueue
						 * buildQueue 通过 new AsyncQueue 创建
						 * 
						 * buildQueue.add=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation._buildModule
						 * 
						 * compilation._buildModule 里面执行 module.needBuild 判断模块需不需要构建
						 * 需要构建,执行 module.needBuild 的回调,回调里面执行 module.build 对模块进行构建
						 * this.buildModule 回调里面继续调用 compilation.processModuleDependencies
						 */
                        
                        // 15
                        this.processModuleDependencies(module, err => {
                            
                        })
                    })
                })
            }
        )
    }
     
    // 7
    factorizeModule(options, callback) {
		this.factorizeQueue.add(options, callback);
	}
    // 8
    _factorizeModule(
		{currentProfile,factory,dependencies,originModule,contextInfo,context},
		callback
	) 
    {
        // 对模块进行解析【执行NormalModuleFactory实例上的create方法】
		factory.create({}, (err, result) => {})
    }
    
    // 10
    addModule(module, callback) {
		// 将模块添加到 addModuleQueue
		this.addModuleQueue.add(module, callback);
	}
    // 11
    _addModule(module, callback) {
        // ...
        
        // 添加创建的 module 到 modules
        this._modules.set(identifier, module);
        this.modules.add(module);
        
        // ...
    }
    
    // 13
    buildModule(module, callback) {
		this.buildQueue.add(module, callback);
	}
    // 14
    _buildModule(module, callback) {
        // ...
        
        // 判断当前模块需不需要构建,需要构建就执行回调
        module.needBuild({}, (err, needBuild) => {
            // ...
            
		    // 对模块进行构建
			// 但是这里直接点进 module.build 里面,会发现里面只是抛出了一个错误
			// 其实这里是使用了多态,module.build 是类 Moudle 上的方法,同时 Module 是一个父类
			// 其他子类继承父类 Moudle,并对 build 方法进行重写
			// 所以这里的 module.build 方法其实是 NormalModule 类继承了 Module 类并对重写的 build 方法
		    module.build({}, err => {})
        })
    }
}
  1. compilation.addEntry:调用 compilation._addEntryItem 添加入口
  2. compilation._addEntryItem:调用 addEntry 钩子。然后通过 compilation.addModuleTree 将当前的模块加入到 module tree(模块树)
  3. compilation.addModuleTree:调用 compilation.handleModuleCreation 处理模块并对模块进行创建
  4. compilation.handleModuleCreation:
    • 创建了模块图
    • 调用 compilation.factorizeModule
  5. compilation.factorizeModule:
    • 执行 factorizeQueue 的 add 方法将模块添加到 factorizeQueue 队列
      • compilation.factorizeQueue.add()=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation.__factorizeModule=>factory.create()
      • 最终就是使用 factory.create() 去创建 module,factory.create调用的是 normalModuleFacotry 的 create 方法,normalModuleFacotry .create 中做了:
        • 触发 beforeResolve 事件
        • 触发 NormalModuleFactory 中的 factory 钩子。(在 NormalModuleFactory 的 constructor 中有一段注册 factory 事件的逻辑)执行 factory 逻辑,factory 做了两件事:
          • 获取文件的绝对路径、根据文件类型找到对应的 loaders、loaders 的绝对路径
          • 生成 normalModule 实例,并将文件路径和 loaders 路径存放到实例 conpilation 中
    • 执行回调,回调中执行 compilation.addModule
  6. compilation.addModule:
    • 执行 addModuleQueue.add 将模块添加到 addModuleQueue 队列
      • compilation.addModuleQueue.add()=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation.__addModule
      • 最终就是调用__addModule 方法将 module 添加到 compilation.modules 中,以便于在最后打包成 chunk 的时候使用
    • 执行回调,回调中继续调用 compilation.buildModule
  7. compilation.buildModule:
    • 执行 addModuleQueue.add 将模块添加到 addModuleQueue 队列
      • compilation.buildQueue.add()=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation.__buildModule
      • 最终就是通过 __buildModule 去编译模块

其实 compilation 对模块进行处理,简单来说就是:

  1. compilation.addEntry=>compilation._addEntryItem,通过入口将模块添加到 module tree(模块树)
  2. 然后调用了 compilation.handleModuleCreation,在里面通过不断的回调,执行了以下几步:
    1. compilation.factorizeModule 系列:处理模块并对模块进行创建,并且添加到 factorizeQueue 队列
    2. compilation.addModule 系列:添加 module 模块
    3. compilation.buildModule 系列:准备编译

简单的流程图:

进行模块编译构建(build)

总的来说,build 阶段主要做的以下几件事:

  • 使用 loader-runner 运行 Loader
  • Parser 解析出 AST
  • walkStatements 解析出依赖
  • 如果当前模块有依赖,那么继续递归进行 build 流程

1、从 compilation.buildModule 开始

模块的构建从 compilation.buildModule 开始

webpack-master\lib\Compilation.js

class Compilation {
    conscructor() {
		this.buildQueue = new AsyncQueue({
			name: "build",
			parent: this.factorizeQueue,
			// processor:处理方法,调用 this._buildModule
			processor: this._buildModule.bind(this)
		});
    }
    
    handleModuleCreation(
		{factory, dependencies, originModule, contextInfo, context, recursive = true},
		callback
    )
    {
        this.factorizeModule(
            {currentProfile,factory,dependencies,originModule,contextInfo,context},
            (err, newModule) => {

                this.addModule(newModule, (err, module) => {

                    this.buildModule(module, err => {
                        
                        /**
						 * compilation.buildModule 执行 buildQueue.add 将 module 添加到 buildQueue
						 * buildQueue 通过 new AsyncQueue 创建
						 * 
						 * buildQueue.add=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation._buildModule
						 * 
						 * compilation._buildModule 里面执行 module.needBuild 判断模块需不需要构建
						 * 需要构建,执行 module.needBuild 的回调,回调里面执行 module.build 对模块进行构建
						 * this.buildModule 回调里面继续调用 compilation.processModuleDependencies
						 */
                        
                        // 15
                        this.processModuleDependencies(module, err => {
                            
                        })
                    })
                })
            }
        )
    }
    
    buildModule(module, callback) {
		this.buildQueue.add(module, callback);
	}
    _buildModule(module, callback) {
        // ...
        
        // 判断当前模块需不需要构建,需要构建就执行回调
        module.needBuild({}, (err, needBuild) => {
            // ...
            
		    // 对模块进行构建
			// 但是这里直接点进 module.build 里面,会发现里面只是抛出了一个错误
			// 其实这里是使用了多态,module.build 是类 Moudle 上的方法,同时 Module 是一个父类
			// 其他子类继承父类 Moudle,并对 build 方法进行重写
			// 所以这里的 module.build 方法其实是 NormalModule 类继承了 Module 类并对重写的 build 方法
		    module.build({}, err => {})
        })
    }
}

compilation.buildModule=>compilation.buildQueue.add()=>setImmediate(root._ensureProcessing)=>AsyncQueue._ensureProcessing=>AsyncQueue._startProcessing => compilation.__buildModule

可以看出最后是执行了 compilation.__buildModule,这个方法里面:

  • module.needBuild:判断当前模块需不需要构建,需要构建就执行回调
  • 回调中调用 module.build 对模块开始进行构建

这里注意一下,module.build点进去是:

class Module extends DependenciesBlock  {
  // ...
    
  build(options, compilation, resolver, fs, callback) {
		const AbstractMethodError = require("./AbstractMethodError");
		throw new AbstractMethodError();
	}
}

发现,这里的 Module类的 build 就只是抛出了一个错误。其实这里用的是多态的概念, Module 只是一个父类,后面的子类可以继承 Module,并对里面的 build 方法进行改写。这里实际上执行的并不是 Module 的 build,而是它的子类 NormalModule 的 build。

可以通过光标在 Module 上,鼠标右键打开菜但,点击 find all Implementations 找到

打开 webpack\lib\NormalModule.js 文件,可以看到:

webpack-master\lib\NormalModule.js

class NormalModule extends Module {
    // ...
    
    build(options, compilation, resolver, fs, callback) {
        
    }
}

NormalModule 类继承于 Module 类,并重写了 build

所以,实际上,module.build 就是调用的 NormalModule 类的 build 方法

2、模块构建 build

module.build 也就是 NormalModule 类的 build 方法:

webpack-master\lib\NormalModule.js

class NormalModule extends Module {
    // ...
    
    build(options, compilation, resolver, fs, callback) {
        // ...
        
        // 将 doBuild 函数执行后的返回值返回
        return this.doBuild(options, compilation, resolver, fs, err => {})
    }
}

NormalModule 类的 build 方法中将 doBuild 函数的结果返回。来到 doBuild 方法:

webpack-master\lib\NormalModule.js

const { runLoaders } = require("loader-runner");

class NormalModule extends Module {
    // ...
    
    doBuild(options, compilation, resolver, fs, callback) {
        // ...
        
        // 定义 processResult 函数,主要用来处理 runLoaders 执行后的结果
        const processResult = (err, result) => {})
        
        // doBuild 的核心:执行所有的 loader 对匹配到的模块【test: /\.js$/】进行转换
        // 转换后的结果交给 processResult 处理
        // runLoaders 来自 webpack 官方维护的 loader-runner 库
        runLoaders(
          {
            resource: this.resource, // 需要编译的模块路径
            loaders: this.loaders, // 传入 loader
            context: loaderContext,
            // processResource:需要做的进一步操作
            processResource: (loaderContext, resource, callback) => {
                        // ...

                        loaderContext.addDependency(resource);
                        // 读取模块文件
                        fs.readFile(resource, callback);
            }
          },
          (err, result) => {
                    // ...
            processResult(err, result.result);
          }
        );
    }
}

doBuild 中定义了 processResult,用来处理 runLoaders 执行后的结果

runLoaders: runLoaders 来自 webpack 官方维护的 loader-runner 库,主要用来执行 loader 对匹配到的模块进行转换(通常是从各类资源类型转译为 JavaScript 文本,例如图片、css、vue文件等),使其转换成 js 标准模块,最后在回调中将转换后的结果交给 processResult 去处理

webpack-master\lib\NormalModule.js

class NormalModule extends Module {
    // ...
    
    doBuild(options, compilation, resolver, fs, callback) {
        // ...
        
        // 定义 processResult 函数,主要用来处理 runLoaders 执行后的结果
        const processResult = (err, result) => {
            const source = result[0];
            
            return callback();
        })

		runLoaders({}, (err, result) => {
                // ...
				processResult(err, result.result);
			}
		);
    }
}

processResult 做的事就是拿到 loader 转换之后的结果 const source = result[0],最后调用 callback,这个 callback 是 doBuild 上的 callback

webpack-master\lib\NormalModule.js

class NormalModule extends Module {
    // ...
    
    build(options, compilation, resolver, fs, callback) {
        // ...
        
        // 将 doBuild 函数执行后的返回值返回
        return this.doBuild(options, compilation, resolver, fs, err => {
            // ...
            
            result = this.parser.parse(this._ast || this._source.source(), {
                current: this,
                module: this,
                compilation: compilation,
                options: options
            })
        })
    }
}

可以看到执行 doBuild 的 callback 中有一步是 this.parser.parse,这一步就是进行 AST 转换

这个 this.parser.parse 方法主要就是: JavascriptParser 类的 parse 方法

webpack-master\lib\javascript\JavascriptParser.js

const { Parser: AcornParser } = require("acorn");

const parser = AcornParser;

class JavascriptParser extends Parser {
    // ...
    
    parse(source, state) {
        ast = JavascriptParser._parse(source, {
            sourceType: this.sourceType,
            onComment: comments,
            onInsertedSemicolon: pos => semicolons.add(pos)
        });
        
        if (this.hooks.program.call(ast, comments) === undefined) {
			this.detectMode(ast.body);
			this.preWalkStatements(ast.body);
			this.prevStatement = undefined;
			this.blockPreWalkStatements(ast.body);
			this.prevStatement = undefined;
			this.walkStatements(ast.body);
		}
    }
    
    static _parse(code, options) {
        let ast
        ast = parser.parse(code, parserOptions)
    }
}

可以看出,在 parse 中继续调用 _parse 方法生成 ast,_parse 中生成 ast 利用了 acorn 库的 Parser

问题:为什么还要进行 ast 转换?

因为需要分析模块代码,看看当前模块还需要依赖于哪些模块,对依赖的模块继续进行编译,这是一步递归的过程。

调用 acorn 将 JS 文本解析为 AST

遍历 AST,触发各种钩子

  1. blockPreWalkStatements 中进一步解析当前模块的依赖
  2. 调用 module 对象的 addDependency 将依赖对象加入到 module 依赖列表中

AST 遍历完毕后,调用 module.handleParseResult 处理模块依赖

对于 module 新增的依赖,调用 handleModuleCreate ,递归解析依赖

所有依赖都解析完毕后,构建阶段结束

这个过程中数据流 module => ast => dependences => module ,先转 AST 再从 AST 找依赖

接下来就回到了 compilation.processModuleDependencies 对 module 递归进行依赖收集

webpack-master\lib\Compilation.js

class Compilation {
    conscructor() {
        this.processDependenciesQueue = new AsyncQueue({
			name: "processDependencies",
			parallelism: options.parallelism || 100,
			processor: this._processModuleDependencies.bind(this)
		});
    }
    
    handleModuleCreation() {
        this.factorizeModule(
            {currentProfile,factory,dependencies,originModule,contextInfo,context},
            (err, newModule) => {

                this.addModule(newModule, (err, module) => {

                    this.buildModule(module, err => {

                        this.processModuleDependencies(module, err => {
                            
                        })
                    })
                })
            }
        )
    }
    
    processModuleDependencies(module, callback) {
		this.processDependenciesQueue.add(module, callback);
	}
    _processModuleDependencies(module, callback) {
        // ...
        
        const processDependenciesBlock = block => {
			if (block.dependencies) {
				currentBlock = block;
				for (const dep of block.dependencies) processDependency(dep);
			}
			if (block.blocks) {
				for (const b of block.blocks) processDependenciesBlock(b);
			}
		};
        
        // 收集依赖
        processDependenciesBlock(module);
        
        // 循环执行 compilation.handleModuleCreation 再进行模块转换、依赖收集
        asyncLib.forEach(
			sortedDependencies,
			(item, callback) => {
				this.handleModuleCreation(item, err => {}});
			},
			err => {}
		);
    }
}
  • 执行 compilation.processModuleDependencies(module, callback) 并且传入 buildModule 生成的 module实例;

  • 执行 compilation._processModuleDependencies(module, callback),通过 processDependenciesBlock 进行收集依赖;

  • 循环执行 compilation.handleModuleCreation 再进行模块转换、依赖收集

总结就是收集依赖模块,并对依赖模块再进行相同的模块处理

到这里,make 阶段算是完结。

对处理完成的模块 module 封装输出

webpack-master\lib\Compiler.js

class Compiler {
    // ...
    
    compile(callback) {
        // 初始化 compilation 的参数
		const params = this.newCompilationParams();
        
        // 钩子 beforeCompile
        this.hooks.beforeCompile.callAsync(params, err => {
            // 钩子 compile
			this.hooks.compile.call(params);
            
            // 通过 this.newCompilation 返回一个 compilation 对象
			const compilation = this.newCompilation(params);
            
            // 钩子 make
            this.hooks.make.callAsync(compilation, err => {
                // 钩子 finishMake
                this.hooks.finishMake.callAsync(compilation, err => {
                    process.nextTick(() => {
                        
                        // 执行compilation的finsh方法 对modules上的错误或者警告处理
                        // finsh中会执行compilation.hooks.finishModules钩子
                        compilation.finish(err => {
                            
                            // 执行seal 对 module 代码进行封装输出
                            compilation.seal(err => {
                                
                                // 钩子 afterCompile
                                this.hooks.afterCompile.callAsync(compilation, err => {})
                            })
                        })
                    })
                })
            })
        }
    }
}

又回到 compiler.compile,可以看到,在 make 钩子函数完成之后:

  1. 会调用 finishMake,然后执行当前 compilation 上的 finsh 方法,对生成的 modules时产生的错误或者警告进行处理。
  2. compilation.finish 回调里面继续执行 compilation.seal 对上一个阶段处理完成的 modules 进行封装输出,会生成 chunk 和 assets 等信息,根据不同的 template 生成要输出的代码

这里有几个概念,先看看:

  • module:模块,就是不同的资源文件,包括 js/css/图片 等文件,在编译阶段,webpack 会根据个个模块的依赖关系组合生成 chunk
  • moduleGraph:各个 module 之间的依赖关系,生成 chunkGraph 要用到
  • chunk:由一个或者多个 module 组成
  • chunkGroup:由一个或者多个 chunk 组成,生成 chunkGraph 要用到
  • chunkGraph:用于储存 module、chunk、chunkGroup 三者之间的关系

回到 compilation.seal:seal 函数主要完成从 modulechunks 的转化

webpack-master\lib\Compilation.js

class Compilation {
    // ...
    
    _runCodeGenerationJobs(jobs, callback) {
        // 根据 template 生成代码
        this._codeGenerationModule();
    },
    
    _codeGenerationModule() {},
    
    createChunkAssets(callback) {
        asyncLib.forEach(
			this.chunks,
            (chunk, callback) => {
                asyncLib.forEach(
                    this.chunks,
                    (chunk, callback) => {
                        // ...
                        
                        // 开始输出资源
					    this.emitAsset(file, source, assetInfo);
                    },
                    callback
                )
            },
            callback
        )
    },
    
    // 当模块解析完,就来到了 seal 阶段,对处理过的代码进行封装输出
	// 目的是将 module 生成 chunk,并封存到 compilation.assets 中
	// 在这个过程可以做各种各样的优化
    seal(callback) {
        // 生成 chunkGraph 实例
		const chunkGraph = new ChunkGraph(this.moduleGraph);
        
        // 遍历 compilation.modules ,记录下模块与 chunk 关系
        for (const module of this.modules) {
			ChunkGraph.setChunkGraphForModule(module, chunkGraph);
		}
        
        // 触发 seal 钩子
		this.hooks.seal.call();
        
    // 循环 this.entries 创建 chunks
    for (const [name, { dependencies, includeDependencies, options }] of this.entries) {
      // ...
    }
        
        // 用于创建 chunkGraph、moduleGraph
		buildChunkGraph(this, chunkGraphInit);
        
        // 触发钩子 optimize,代表优化开始
		this.hooks.optimize.call();
        
    // 执行各种优化 module
		while (this.hooks.optimizeModules.call(this.modules)) {
			/* empty */
		}
		// 钩子 afterOptimizeModules,代表 module 已经优化完
		this.hooks.afterOptimizeModules.call(this.modules);
		
		// 执行各种优化 chunk
		while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {
			/* empty */
		}
		// 钩子 afterOptimizeChunks,代表 chunk 已经优化完
		this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
        
        // 优化 modules 树
        this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
            
            // 触发 afterOptimizeTree 钩子,代表 modules 树优化结束
			this.hooks.afterOptimizeTree.call(this.chunks, this.modules);
            
            // 对代码进行优化阶段
			this.hooks.optimizeChunkModules.callAsync(
                this.chunks,
                this.modules,
                err => {
                    // 触发一系列各种优化的钩子,省略代码
                    
                    // 生成 module 的 hash
					this.createModuleHashes();
                    
                    //  调用 codeGeneration 方法用于生成编译好的代码
                    this.codeGeneration(err => {
                        // 执行代码生成的方法 _runCodeGenerationJobs
						// _runCodeGenerationJobs 中会执行 this._codeGenerationModule,这个方法会根据 tempalte 生成代码 
                        this._runCodeGenerationJobs(codeGenerationJobs, err => {
                            // ...
                            
                            // 创建 moduleAssets 资源
							this.createModuleAssets();
                            
                            // 创建 chunkAssets 资源
                            // createChunkAssets 里面会调用 compilation.emitAsset 开始输出
                            this.createChunkAssets(err => {});
                        })
                    })
                }
            )
        })
    }
}

总结一下 compilation.seal 的主要流程:

  1. 创建 chunkGraph 实例

  2. 遍历 compilation.modules ,记录下模块与 chunk 关系

  3. 循环 this.entries 入口文件创建 chunks

  4. 执行各种优化 module、chunk、module tree

  5. 调用 compilation.codeGeneration 方法用于生成编译好的代码

  6. 执行代码生成的方法 compilation._runCodeGenerationJobs

    • _runCodeGenerationJobs 中会执行 compilation._codeGenerationModule,这个方法会根据 tempalte 生成代码
  7. 执行 compilation.createChunkAssets 创建 chunkAssets 资源,createChunkAssets 里面会调用 compilation.emitAsset 将生成的代码放到 compilation.assets 中,然后一路回调,最后的回调就是用的createChunkAssets(callback) 中的 callback,然后一路找回调,会发现最后调用的回调是 compilation.seal(callback)这里的,而 compilation.seal 在 compiler.compile 中被调用,此时,又回到了 compiler

这一步的关键逻辑是将 module 按规则组织成 chunks ,webpack 内置的 chunk 封装规则:

  • entry 及 entry 触达到的模块,组合成一个 chunk
  • 使用动态引入语句引入的模块,各自组合成一个 chunk( 例如:import('./xxx/xx.js').then(res => {}) )
class Compiler {
    compile(callback) {
        compilation.seal(err => {
            this.hooks.afterCompile.callAsync(compilation, err => {
                // ...
                return callback(null, compilation);
            })
        })
    }
}

继续找回调,发现 compilation.seal 被 compiler.compile 调用时又使用了 compiler.compile 的 callback,再看看 compiler.compile 被调用的情况:

class Compiler {
    run(callback) {
        / 定义了一个 onCompiled 函数,主要是传给 this.compile 作为执行的回调函数
        const onCompiled = (err, compilation) => {
            process.nextTick(() => {
                // ...
                this.emitAssets(compilation, err => {
                    this.emitRecords(err => {})
                })
            })
        })
        
        const run = () => {
            this.compile(onCompiled);
        }
        
        run();
    }
}

终于找到,compiler.compile 调用的时候传入了 onCompiled 作为回调,onCompiled 中调用 compiler.emitAssets 和 compiler.emitRecords 对资源做输出。

到此,webpack 源码主体流程基本完成

总结

在 Webpack 的主流程中,比较重要的几个节点:

  1. webpack():执行 webpack 函数,
  2. createCompiler
  3. compiler.run()
  4. compiler.compile()
  5. compiler.hooks.make

附录

参考的一些优秀文章:

[万字总结] 一文吃透 Webpack 核心原理

webpack编译流程详解

webpack执行过程

webpack构建流程分析

【webpack进阶】你真的掌握了loader么?- loader十问

webpack核心模块tapable用法解析

Webpack 基石 tapable 揭秘

一些 webpack4 的流程图