tapable 助你解析 webpack 的插件系统

tapable

tapable 导出了 9 个 hooks

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook
  • AsyncParallelHook
  • AsyncParallelBailHook
  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

上述 9 个 hooks 都继承自 Hook 这个 classjavascript

tapable Hook 解析

hook 对外提供了 isUsed call promise callAsync compile tap tapAsync tapPromise intercept 这些方法java

其中 tap 开头的方法是用来订阅事件的,call promise callAsync 是用来触发事件的,isUsed 返回了一个 boolean 值用来标记当前 hook 中注册的事件是否被执行完成。webpack

isUsed 源码web

isUsed() {
		return this.taps.length > 0 || this.interceptors.length > 0;
}
复制代码

tap tapAsync tapPromise 这三个方法第一个参数传入能够支持传入 string(通常是指 plugin 的名称) 或者一个 Tap 类型,第二个参数是一个回调用来接收事件被 emit 时的调用。数组

export interface Tap {
    name: string; // 事件名称,通常就是 plugin 的名字
    type: TapType; // 支持三种类型 'sync' 'async' 'promise'
    fn: Function;
    stage: number;
    context: boolean;
}
复制代码

call promise callAsync 这三个方法在传入参数的时候是依赖于 hook 被实例化的时候传入的 args 数组占位符的数量的,以下示例:promise

const sync = new SyncHook(['arg1', 'arg2']) // 'arg1' 'arg2' 为参数占位符
sync.tap('Test', (arg1, arg2) => {
  console.log(arg1, arg2) // a2
})
sync.call('a', '2')
复制代码

其中 promise 调用会返回一个 PromisecallAsync 默认支持传入一个 callbackbash

Sync 开头的 hook 不支持使用 tapAsynctapPromise,能够看下述的以 SyncHook 的源码为例app

const TAP_ASYNC = () => {
	throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
	throw new Error("tapPromise is not supported on a SyncHook");
};

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}

SyncHook.prototype = null;
复制代码

在这里面咱们能够看到 tapAsynctapPromise 是被重写了直接 throw errorasync

一个简单的使用示范

下面的例子会给你们带来一个简单地示范函数

class TapableTest {
  constructor() {
    this.hooks = {
      sync: new SyncHook(['context', 'hi']),
      syncBail: new SyncBailHook(),
      syncLoop: new SyncLoopHook(),
      syncWaterfall: new SyncWaterfallHook(['syncwaterfall']),
      asyncParallel: new AsyncParallelHook(),
      asyncParallelBail: new AsyncParallelBailHook(),
      asyncSeries: new AsyncSeriesHook(),
      asyncSeriesBail: new AsyncSeriesBailHook(),
      asyncSeriesWaterfall: new AsyncSeriesWaterfallHook(['asyncwaterfall']) 
    }
  }
  emitSync() {
    this.hooks.sync.call(this, err => {
        console.log(this.hooks.sync.promise)
        console.log(err)
    })
  }
  emitAyncSeries() { 
    this.hooks.asyncSeries.callAsync(err => {
        if (err) console.log(err)
    })
  }
}

const test = new TapableTest()
test.hooks.sync.tap('TestPlugin', (context, callback) => {
  console.log('trigger: ', context)
  callback(new Error('this is sync error'))
})
test.hooks.asyncSeries.tapAsync('AsyncSeriesPlugin', callback => {
    callback(new Error('this is async series error'))
})
test.emitSync()
test.emitAyncSeries()
复制代码

上述的运行结果能够这查看 runkit

下面来聊一聊 webpack 中的插件是如何依赖 tapable 的

webpack 插件被注入的时机

当咱们定义了 webpack 的配置文件后,webpack 会根据这些配置生成一个或多个 compiler ,而插件就是在建立 compiler 时被添加到 webpack 的整个运行期间的, 能够看下述源码:(相关源码能够在 webpack lib 下的 webpack.js 中找到)

const createCompiler = rawOptions => {
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
	const compiler = new Compiler(options.context);
	compiler.options = options;
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}
	applyWebpackOptionsDefaults(options);
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();
	new WebpackOptionsApply().process(options, compiler);
	compiler.hooks.initialize.call();
	return compiler;
};
复制代码

咱们能够看到遍历 options.plugins 这一段,这一段分了两种状况来进行插件的插入

  • 咱们的 plugin 能够以函数的方式被 webpack 调用,也就是说咱们能够用函数来写插件,这个函数的做用域是当前的 compiler,函数也会接收到一个 compiler
  • 能够传入一个包含 apply 方法的对象实例,apply 方法会被传入 compiler

因此这也就解释了为何咱们的插件须要 new 出来以后传入到 webpack

进入 Compiler 一探究竟

上一个中咱们了解到了 plugins 是什么时候被注入的,咱们能够看到在 plugin 的注入时传入了当前被实例化出来的 Compiler,因此如今咱们须要了解下 Compiler 中作了什么

进入 Compiler.js (也在 lib 中)咱们能够第一时间看到 Compilerconstructor 中定义了一个庞大的 hooks

this.hooks = Object.freeze({
			/** @type {SyncHook<[]>} */
			initialize: new SyncHook([]),

			/** @type {SyncBailHook<[Compilation], boolean>} */
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<[Stats]>} */
			done: new AsyncSeriesHook(["stats"]),
			/** @type {SyncHook<[Stats]>} */
			afterDone: new SyncHook(["stats"]),
			/** @type {AsyncSeriesHook<[]>} */
			additionalPass: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<[Compiler]>} */
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<[Compiler]>} */
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			emit: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
			assetEmitted: new AsyncSeriesHook(["file", "info"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			afterEmit: new AsyncSeriesHook(["compilation"])
      ...
})
复制代码

看到这些 hook 是否是很熟悉,全是 tapable 中的 hook,webpack 正是依赖于这些复杂的构建 hook 而完成了咱们的代码构建,因此在咱们编写 plugin 时就能够利用这些 hook 来完成咱们的特殊需求。

好比咱们常常用到的 HtmlWebpackPlugin ,咱们能够看下他是如何运行的,在 HtmlWebpackPluginapply 中咱们能够找到这样一段代码:

compiler.hooks.emit.tapAsync('HtmlWebpackPlugin', (compiler, callback) => {
  ...
})
复制代码

说明 HtmlWebpackPlugin 是利用了 Compileremithook 来完成的

经过深刻了解,webpack 是在庞大的插件上运行的,他本身内置了不少插件

上述内容若有错误,请指正

相关文章
相关标签/搜索