# webpack插件机制

上一节讲到了npm scripts运行webpack构建时候,实际查找的node_modules/webpack/bin/webpack.js文件 并判断CLI是否安装,如果按照了CLI就调用webpack-cli对参数分组转换为webpack可识别的编译配置项,最后再次 调用webpack执行compiler实例。

通过webpack源码我们可以了解到,webpack函数会创建compiler实例。

//webpack.js
...
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
...
const webpack = (options, callback) => {
	...
	let compiler;
	// 如果有多个options,则创建多个compiler实例
	if (Array.isArray(options)) {
		compiler = new MultiCompiler(
			Array.from(options).map(options => webpack(options))
		);
	} else if (typeof options === "object") {
	    // 如果只有一个,则实例化一个compiler
	    // 默认配置初始化
		options = new WebpackOptionsDefaulter().process(options);
		compiler = new Compiler(options.context);
		compiler.options = options;
		new NodeEnvironmentPlugin({
        			infrastructureLogging: options.infrastructureLogging
        		}).apply(compiler);
		// 遍历插件并将compiler对象传递给插件,当compiler钩子触发时候实现plugin内部监听
		if (options.plugins && Array.isArray(options.plugins)) {
		    for (const plugin of options.plugins) {
		        if (typeof plugin === "function") {
		            plugin.call(compiler, compiler);
		        } else {
		            plugin.apply(compiler);
		        }
		    }
		}
		compiler.hooks.environment.call();
		compiler.hooks.afterEnvironment.call();
		// 注入内部插件
		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))
		) {
			const watchOptions = Array.isArray(options)
				? options.map(o => o.watchOptions || {})
				: options.watchOptions || {};
			return compiler.watch(watchOptions, callback);
		}
		compiler.run(callback);
	}
	return compiler;
};
...

而从Compiler.js源码可以看到,核心对象Compiler是对Tapable的继承和扩展。

//Compiler.js
...
const {
	Tapable,
	SyncHook,
	SyncBailHook,
	AsyncParallelHook,
	AsyncSeriesHook
} = require("tapable");

const Compilation = require("./Compilation");
...
class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			shouldEmit: new SyncBailHook(["compilation"]),
			done: new AsyncSeriesHook(["stats"]),
			beforeRun: new AsyncSeriesHook(["compiler"]),
			run: new AsyncSeriesHook(["compiler"]),
			emit: new AsyncSeriesHook(["compilation"]),
			afterEmit: new AsyncSeriesHook(["compilation"]),
			compilation: new SyncHook(["compilation", "params"]),
			beforeCompile: new AsyncSeriesHook(["params"]),
			compile: new SyncHook(["params"]),
			make: new AsyncParallelHook(["compilation"]),
			afterCompile: new AsyncSeriesHook(["compilation"]),
			watchRun: new AsyncSeriesHook(["compiler"]),
			failed: new SyncHook(["error"]),
			watchClose: new SyncHook([]),
			...
		};
        ...
	}
	watch(watchOptions, handler) {...}
	run(callback) {...}
	...
	compile(callback) {...}
}

module.exports = Compiler;

通过Comiler源码了解到,Compiler通过Compilation创建来的实例,分析Compilation.js源码可以看到,Compilation 通用继承至Tapable。

//Compilation.js
...
const {
	Tapable,
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	AsyncSeriesHook
} = require("tapable");
...
class Compilation extends Tapable {
	constructor(compiler) {
		super();
		this.hooks = {
			buildModule: new SyncHook(["module"]),
			rebuildModule: new SyncHook(["module"]),
			failedModule: new SyncHook(["module", "error"]),
			succeedModule: new SyncHook(["module"]),
			addEntry: new SyncHook(["entry", "name"]),
			failedEntry: new SyncHook(["entry", "name", "error"]),
			succeedEntry: new SyncHook(["entry", "name", "module"]),
			dependencyReference: new SyncWaterfallHook([
				"dependencyReference",
				"dependency",
				"module"
			]),

			finishModules: new AsyncSeriesHook(["modules"]),
			finishRebuildingModule: new SyncHook(["module"]),
			unseal: new SyncHook([]),
			seal: new SyncHook([]),
			beforeChunks: new SyncHook([]),
			afterChunks: new SyncHook(["chunks"]),
			optimizeDependenciesBasic: new SyncBailHook(["modules"]),
			optimizeDependencies: new SyncBailHook(["modules"]),
			optimizeDependenciesAdvanced: new SyncBailHook(["modules"]),
			afterOptimizeDependencies: new SyncHook(["modules"]),
			optimize: new SyncHook([]),
			optimizeModulesBasic: new SyncBailHook(["modules"]),
			optimizeModules: new SyncBailHook(["modules"]),
			optimizeModulesAdvanced: new SyncBailHook(["modules"]),
			afterOptimizeModules: new SyncHook(["modules"]),
			optimizeChunksBasic: new SyncBailHook(["chunks", "chunkGroups"]),
			optimizeChunks: new SyncBailHook(["chunks", "chunkGroups"]),
			optimizeChunksAdvanced: new SyncBailHook(["chunks", "chunkGroups"]),
			afterOptimizeChunks: new SyncHook(["chunks", "chunkGroups"]),
			optimizeTree: new AsyncSeriesHook(["chunks", "modules"]),
			afterOptimizeTree: new SyncHook(["chunks", "modules"]),
			optimizeChunkModulesBasic: new SyncBailHook(["chunks", "modules"]),
			optimizeChunkModules: new SyncBailHook(["chunks", "modules"]),
			optimizeChunkModulesAdvanced: new SyncBailHook(["chunks", "modules"]),
			afterOptimizeChunkModules: new SyncHook(["chunks", "modules"]),
			shouldRecord: new SyncBailHook([]),
			reviveModules: new SyncHook(["modules", "records"]),
			optimizeModuleOrder: new SyncHook(["modules"]),
			advancedOptimizeModuleOrder: new SyncHook(["modules"]),
			beforeModuleIds: new SyncHook(["modules"]),
			moduleIds: new SyncHook(["modules"]),
			optimizeModuleIds: new SyncHook(["modules"]),
			afterOptimizeModuleIds: new SyncHook(["modules"]),
			reviveChunks: new SyncHook(["chunks", "records"]),
			optimizeChunkOrder: new SyncHook(["chunks"]),
			beforeChunkIds: new SyncHook(["chunks"]),
			optimizeChunkIds: new SyncHook(["chunks"]),
			afterOptimizeChunkIds: new SyncHook(["chunks"]),
			recordModules: new SyncHook(["modules", "records"]),
			recordChunks: new SyncHook(["chunks", "records"]),
			beforeHash: new SyncHook([]),
			contentHash: new SyncHook(["chunk"]),
			afterHash: new SyncHook([]),
			recordHash: new SyncHook(["records"]),
			record: new SyncHook(["compilation", "records"]),
			beforeModuleAssets: new SyncHook([]),
			shouldGenerateChunkAssets: new SyncBailHook([]),
			beforeChunkAssets: new SyncHook([]),
			additionalChunkAssets: new SyncHook(["chunks"]),
			additionalAssets: new AsyncSeriesHook([]),
			optimizeChunkAssets: new AsyncSeriesHook(["chunks"]),
			afterOptimizeChunkAssets: new SyncHook(["chunks"]),
			optimizeAssets: new AsyncSeriesHook(["assets"]),
			afterOptimizeAssets: new SyncHook(["assets"]),
			needAdditionalSeal: new SyncBailHook([]),
			afterSeal: new AsyncSeriesHook([]),
			chunkHash: new SyncHook(["chunk", "chunkHash"]),
			moduleAsset: new SyncHook(["module", "filename"]),
			chunkAsset: new SyncHook(["chunk", "filename"]),
			assetPath: new SyncWaterfallHook(["filename", "data"]), // TODO MainTemplate
			needAdditionalPass: new SyncBailHook([]),
			childCompiler: new SyncHook([
				"childCompiler",
				"compilerName",
				"compilerIndex"
			]),

			log: new SyncBailHook(["origin", "logEntry"]),
			normalModuleLoader: new SyncHook(["loaderContext", "module"]),
			optimizeExtractedChunksBasic: new SyncBailHook(["chunks"]),
			optimizeExtractedChunks: new SyncBailHook(["chunks"]),
			optimizeExtractedChunksAdvanced: new SyncBailHook(["chunks"]),
			afterOptimizeExtractedChunks: new SyncHook(["chunks"])
		};
		...
	}
    ...
}

Compilation.prototype.applyPlugins = util.deprecate(...);

Object.defineProperty(Compilation.prototype, "moduleTemplate", {...});

module.exports = Compilation;

至此,我们可以了解到webpack的核心对象Compiler和Compilation都是继承至Tapable,实现Tapable和webpack的关联。 而Tapable主要是暴露出钩子函数,为插件提供挂载的钩子,换言之,主要是控制钩子函数的发布与订阅, 控制着webpack的插件系统。
所以可以总结到,webpack可以理解为一种基于事件流的编程范例,一系列的插件运行流程。通过监听插件上定义的 compiler和compilation的关键节点实行相应的事件操作。

# Tapable

Tapable是一个发布订阅的库,主要功能是提供钩子函数的发布与订阅, 也是webpack插件系统的实现。
Tapable暴露了很多钩子,为插件提供挂载的钩子

const {
	SyncHook, //同步钩子
	SyncBailHook, //同步熔断钩子
	SyncWaterfallHook, //同步流水钩子
	SyncLoopHook,   //同步循环钩子
	AsyncParallelHook, //异步并发钩子
	AsyncParallelBailHook,  //异步并发熔断钩子
	AsyncSeriesHook,    //异步串行钩子
	AsyncSeriesBailHook,    //异步串行熔断钩子
	AsyncSeriesWaterfallHook    //异步串行流水钩子
 } = require("tapable");

Tapable钩子类型

类型 方法
Hook 所有钩子的后缀
Waterfall 同步方法,但是会传值给下一个函数
Bail 熔断:当函数有任何返回值,就会在当前执行函数停止
Loop 监听函数返回true表示继续循环,返回undefine表示结束循环
Sync 同步方法
AsyncSeries 异步串行钩子
AsyncParallel 异步并行执行钩子

# Tapable的使用

一、new Hook新建钩子
由于Tapable暴露出来的是类方法,所有通过new新建钩子函数。
其中,类方法接受数组参数options(非必传)。类方法根据传参,接受通用数量的参数。

// 创建同步钩子
const hook = new SyncHook(["arg1", "arg2", "arg3"])

二、钩子的发布与订阅
Tapable提供了同步和异步绑定钩子的方法,并且它们都有绑定事件和执行事件对应的方法。

异步 同步
绑定:tapAsync/tapPromise/tap 绑定:tap
执行:callAsync/promise 执行:call

三、hook基本用法

// 创建同步钩子
const hook1 = new SyncHook(["arg1", "arg2", "arg3"])
// 绑定事件到事件流
hook1.tap('hook1',(arg1,arg2,arg3)=>console.log(arg1,arg2,arg3)) //1,2,3
// 执行绑定的事件
hook1.call(1,2,3)

创建Car类,并定义同步钩子、异步钩子并在钩子上绑定和执行方法。

const {
    SyncHook,
    AsyncSeriesHook
} = require('tapable')

class Car {
    constructor(){
        this.hooks = {
            accelerate: new SyncHook(['newspeed']),
            brake: new SyncHook(),
            calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
        }
    }
}

const myCar = new Car()

// 绑定同步钩子
myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'))

// 绑定同步钩子并传参
myCar.hooks.accelerate.tap('LoggerPlugin', newSpeed => console.log('Accelerating to'+newSpeed))

//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise('calculateRoutes tapPromise',(source, target, routesList)=>{
    console.log('source',source)
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log(`tapPromise to ${source} ${target} ${routesList}`)
            resolve()
        },1000)
    })
})

//执行同步钩子
myCar.hooks.brake.call()
myCar.hooks.accelerate.call(10)
//执行异步钩子
myCar.hooks.calculateRoutes.promise('Async','hook','demo').then(()=>{
    console.log('succ')
},err=>{
    console.err(err)
})

运行结果:

//D:\github\blogCode\webpack\Tapable-demo>node car.js
WarningLampPlugin
Accelerating to10
source Async
tapPromise to Async hook demo
succ

# 模拟插件执行

至此,我们可以了解到了webpack通过Compiler提供hooks钩子函数,为插件plugin提供内部监听。

# 模拟Compiler.js

模拟Compiler.js,通过Tapable的钩子函数定义钩子并提供run方法。

module.exports = class Compiler {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(['newspeed']), 
            brake: new SyncHook(), 
            calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
        }
    }
    run(){
        this.accelerate(10)
        this.break()
        this.calculateRoutes('Async', 'hook', 'demo')
    }
    accelerate(speed) {
        this.hooks.accelerate.call(speed);
    }
    break() {
        this.hooks.brake.call();
    }
    calculateRoutes() {
        this.hooks.calculateRoutes.promise(...arguments).then(() => {
    }, err => {
        console.error(err);
    });
    }
}

# 模拟插件实现

模拟插件实现,绑定钩子函数

const Compiler = require('./Compiler')
class MyPlugin{
    constructor() {
    }
    apply(compiler){
        compiler.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
        compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => 
            console.log(`Accelerating to ${newSpeed}`)
        );
        compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync", 
            (source, target, routesList) => {
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    console.log(`tapPromise to ${source} ${target} ${routesList}`)
                    resolve();
                },1000)
            });
        });
    }
}

# 模拟插件执行

模拟插件执行的过程,通过实例化compiler并遍历plugins,将compiler传递给plugin实现插件监听。

const myPlugin = new MyPlugin();
const options = {
    plugins: [myPlugin]
}
const compiler = new Compiler();
for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
    } else {
        plugin.apply(compiler);
    }
}
compiler.run();

代码链接:https://github.com/PCAaron/blogCode/tree/master/webpack/Tapale-demo