likes
comments
collection
share

webpack核心库 - tapable的设计思路与核心源码解析

作者站长头像
站长
· 阅读数 10

前言

大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.

我的宗旨就是 万物皆可手写

最近也是在继续研究webpack,暂时不知道写点啥,那我们就是来炒一炒冷饭, 扒一扒tapable中的设计思路和关键逻辑

新手创作不易,有问题欢迎指出和轻喷,谢谢

照例附上本人手写的tapable库, 简化代码逻辑,注释齐全,适合学习

lzy19926/lzy-tapable: 手写tapable库 (github.com)


tapable简介

一个最简单的SyncHook的使用方式 它可以将回调函数tap到hook里, call的之后依次执行内部所有的回调函数

// 新建一个hook,传入compiler作为参数
const hook = new SyncHook(['compiler'])
// hook中添加一个回调
    hook.tap('001', (compiler, callback) => {
        console.log('----1',compiler);
    });
// hook中添加一个回调
    hook.tap('002', (compiler, callback) => {
        console.log('----2',compiler);
    });
// 执行hooks, 传入compiler,依次顺序执行所有回调
    const conpiler = {info:"这是compiler"}
    hook.call(conpiler);
    
// 打印结果
// ----1,{info:"这是compiler"}
// ----2,{info:"这是compiler"}

tapable库的主体一共有两个,这里我们拿SyncHook举例

  • Hook
  • HookCodeFactory
  • 其他Hook

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

// 创建SyncHookCodeFactory, 用于给SyncHook提供compile方法,同时实现content方法
class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();
// 实现TAP_ASYNC方法
const TAP_ASYNC = () => {
	throw new Error("tapAsync is not supported on a SyncHook");
};
// 实现TAP_PROMISE方法
const TAP_PROMISE = () => {
	throw new Error("tapPromise is not supported on a SyncHook");
};
// 实现COMPILE方法(通过创建的SyncHookCodeFactory)
const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};
// ES5的方式实现SyncHook
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;

module.exports = SyncHook;

怎么理解这个代码呢? 画个图它就长这样

  • Hook是各种SyncHook的基类,提供了各种方法
  • HookCodeFactory用于构造SyncHook对应的SyncHookCodeFactory,用于提供SyncHook中最重要的Compile方法

webpack核心库 - tapable的设计思路与核心源码解析

Hook的分类

一般来说,hook可以分为AsyncHookSyncHook两大类,一个仅支持同步处理函数,一个支持异步

又可以分为子类:Loop,Waterfall,Serise,Bail...

所以我们有这么多Hook

webpack核心库 - tapable的设计思路与核心源码解析

这些Hook的处理方式各不一样,比如最简单的syncHook就是 顺序执行

  • waterfall指: 下一个回调接收上一个回调的参数作为返回值
  • Bail指: 如果回调中断了,则直接退出
  • Series: 在上一个函数中需要手动调用callback,触发下一个回调
  • 其他

webpack核心库 - tapable的设计思路与核心源码解析

Compile方法会动态生成代码字符串并转化为函数,从而实现这些不同Hook的调用逻辑

Compile方法,js实现抽象类

这个方法是干什么的呢?点进去我们会发现,这是一个抽象方法,需要在外部实现这个方法 (这也是JS实现抽象方法的方式)

class Hook {
...
    	compile(options) {
		throw new Error("Abstract: should be overridden");
	}
}

而恰巧,在构造SyncHook的时候我们通过CodeFactory实现了这个方法

// 实现COMPILE方法(通过创建的SyncHookCodeFactory)
const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};
// 实现SyncHook
function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.compile = COMPILE;
	return hook;
}

可以发现,COMPILE方法实际执行的逻辑是factory.create,让我们进去看看

 create(options) {
        this.init(options)

        let fn;
        switch (type) {
            case "sync":
  // 使用new Function,通过字符串构造一个函数
fn = new Function(
	this.args(),
	'"use strict";\n' +
	this.header() + // 函数头部字符串
	this.contentWithInterceptors({// 函数主体字符串
		onError: err => `throw ${err};\n`,
		onResult: result => `return ${result};\n`,
		resultReturns: true,
		onDone: () => "",
		rethrowIfPossible: true
	})
);

                break;
            case "async":...
                break;
            case "promise":...
                break;
        }
        this.deInit()

        return fn
    }

其实这个方法主要做了一件事,就是动态生成一个函数,按照Hook的要求依次执行推入其中的回调函数 用伪代码解释这个过程

// 每次tap,都会往Hook中推入一个回调,在HOOK中作为一个tap保存起来
// _x保存了所有推入的回调
const _x = [fn1,fn2,fn3]

在我们call调用一个hook时, 会通过create构造出一个函数 (实际调用栈: _createCall->compile—>factory.create—>content—>callTaps)

注意这个content方法也是在创建Factory时实现的抽象类

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries();
	}
}

通过create构造出的函数大概长这样

// VM46975430
(function anonymous(compiler
) {
"use strict";
var _context;
var _x = this._x; 
var _fn0 = _x[0];
_fn0(compiler);  // 执行回调0
var _fn1 = _x[1];
_fn1(compiler);  // 执行回调1
var _fn2 = _x[2];
_fn2(compiler);  // 执行回调2
})

可以看到,这就是SyncHook的执行流程,然后我们执行这个函数即可

这样做的优点:

  • 通过字符串构造代码,更加灵活,可以方便的传入参数,插入回调,Error处理
  • 不同的Hook执行顺序和逻辑不一样,通过字符串构建更方便定义.

现在大家明白,为什么创建Hook的时候传参是一个字符串了吧

// 新建一个hook,传入compiler作为参数
const hook = new SyncHook(['compiler'])

总结

  1. tap时会收集传入的回调,等待hook.call的时候按hook顺序执行
  2. Hooks根据回调执行时机分为两大类: sync, async
  3. Hooks根据回调执行顺序分为四大类: 普通执行, Waterfall执行, Bail中断执行,Series手动触发执行
  4. 通过抽象出contnet方法和CodeFactory基类,于Hook基类继承组合成不同的Hook类
  5. content方法,通过字符串构造出不同的函数,实现各Hook的执行逻辑

照例附上本人手写的tapable库, 简化代码逻辑,注释齐全,适合学习

lzy19926/lzy-tapable: 手写tapable库 (github.com)