webpack - tapable

本文深入解析webpack中的Tapable库,它是实现事件钩子的关键,支持同步和异步操作。文章介绍了SyncHook、AsyncHook、Interception与context的概念,以及HookMap和MultiHook的使用。通过源码分析,揭示了Tapable如何创建并执行钩子,对于理解webpack插件开发和源码阅读具有指导意义。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Tapable

以下代码来自V2.2.1

Tapable是webpack最重要的库了,如果你需要阅读源码,需要写一个webpack plugin,那么最好先了解Tapable是什么

Tapable其实就是一个EventEmitter库,但它相比普通的EventEmitter,它还支持异步的Event,这样就可以支持异步插件。

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
} = Tapable 

上面是来自Tapable的库导出的函数方法,除了Sync的方法,还有Async。

SyncHook

const hook = new SyncHook(["arg1", "arg2"]);
hook.tap('LoggerPlugin', (arg1, arg2) => { console.log(arg1 , arg2) })
hook.tap('SumPlugin', (arg1, arg2) => { console.log(arg1 + arg2) })

hook.call(1,2)
// 1, 2
// 3 

同步的钩子很简单,调用call会执行执行所有被tapped的钩子。 同步方法除了普通的钩子还有其他的,比如:

  • SyncWaterfallHook WaterfallHook可以将上一个回调返回值传入下一个钩子
  • SyncBailHook 如果上一个回调有返回值,BailHook可以立即退出不再执行后面的钩子
  • SyncLoopHook SyncLoopHook在回调有返回值的时候会从第一个钩子重新开始执行,一直循环到所有钩子都返回undefined为止

AsyncHook

异步的钩子和同步钩子差异的地方在于有两个不同的关键字眼,ParalleSeries。一个可以并行执行钩子,一个可以按顺序执行钩子。

const asyncHook = new AsyncParallelHook(["arg"])
asyncHook.tapAsync('SetTimeoutPlugin', (arg, cb) => {
    setTimeout(() => {
        console.log('macrotask runed')
        cb()
    }, 2000)
})

asyncHook.tapPromise('PromisePlugin', (arg, cb) => {
    //return promise
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('microtask runed');
            resolve();
        }, 1000);
    })
})

asyncHook.callAsync("arg", () => {
    console.log("task clearout")
})
// microtask runed
// macrotask runed
// task clearout 

可以看到有两种注入异步钩子的方式,一种需要调用回调参数cb,一种需要返回Promise的形式。

上面的AsyncParallelHook以一种并行的方式执行,所以执行顺序跟着回调走.另外一种AsyncSeriesHook根据注册顺序走,只有前面的钩子执行回调函数才会继续下一个钩子,也就是说// macrotask runed会被先打印

Interception和context

hooks还提供了Interception,可以拦截hooks的行为。Context能做为上下文在Interception中传递

asyncHook.intercept({
    call: (...arg) => {
        console.log("Starting asyncHook");
    },
    loop: (...args) => {
        console.log('restart looping')
    },
    tap: (tapInfo) => {
        console.log('new hooks be tapped')
    }
    register: (tapInfo) => {
        // tapInfo = { type: "promise", name: "PromisePlugin", fn: ... }
        console.log(`${tapInfo.name} is doing its job`);
        return tapInfo; // may return a new tapInfo object
    }
})

//Context

hooks.intercept({
    context: true,
    tap: (context, tapInfo) => {
        // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
        console.log(`${tapInfo.name} is doing it's job`);

        // `context` starts as an empty object if at least one plugin uses `context: true`.
        // If no plugins use `context: true`, then `context` is undefined.
        if (context) {
            // Arbitrary properties can be added to `context`, which plugins can then access.
            context.hasMuffler = true;
        }
    }
});

hooks.tap({
    name: "NoisePlugin",
    context: true
}, (context, newSpeed) => {
    if (context && context.hasMuffler) {
        console.log("Silence...");
    } else {
        console.log("Vroom!");
    }
}); 

HookMap和MultiHook

Tapable还提供了其他辅助方法

//HookMap
const { HookMap } = require("tapable");
const keyedHook = new HookMap(key => new SyncHook(["arg"]))

keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });

const hook = keyedHook.get("some-key");
if(hook !== undefined) {
    hook.callAsync("arg", err => { /* ... */ });
}

//MultiHook
const { MultiHook } = require("tapable");

this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]); 

源码解析

Hook

github.com/webpack/tap…

hooks文件实现了需对对外暴露的方法,比如tap,tapAsync,tapAsync,还有一些处理intercept的方法。

_tap(type, options, fn) {
    if (typeof options === "string") {
        options = {
            name: options.trim()
        };
    } else if (typeof options !== "object" || options === null) {
        throw new Error("Invalid tap options");
    }
    if (typeof options.name !== "string" || options.name === "") {
        throw new Error("Missing name for tap");
    }
    if (typeof options.context !== "undefined") {
        deprecateContext();
    }
    options = Object.assign({ type, fn }, options);
    options = this._runRegisterInterceptors(options);
    this._insert(options);
} 

所有的tap都会调用_tap函数。首先处理options,然后调用_runRegisterInterceptors_insert

const CALL_DELEGATE = function(...args) {
    this.call = this._createCall("sync");
    return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function(...args) {
    this.callAsync = this._createCall("async");
    return this.callAsync(...args);
};
const PROMISE_DELEGATE = function(...args) {
    this.promise = this._createCall("promise");
    return this.promise(...args);
};

class Hook {
    constructor(args = [], name = undefined) {
        this._call = CALL_DELEGATE;
        this.call = CALL_DELEGATE;
        this._callAsync = CALL_ASYNC_DELEGATE;
        this.callAsync = CALL_ASYNC_DELEGATE;
        this._promise = PROMISE_DELEGATE;
        this.promise = PROMISE_DELEGATE;
    }
} 

hooks里的调用都会有一个副本,先说明这是因为暴露给用户的函数是通过new Function生成使用的,所以需要保存生产函数的方法,下次再重新赋值

_runRegisterInterceptors(options) {
    for (const interceptor of this.interceptors) {
        if (interceptor.register) {
            const newOptions = interceptor.register(options);
            if (newOptions !== undefined) {
                options = newOptions;
            }
        }
    }
    return options;
}

_resetCompilation() {
	this.call = this._call;
	this.callAsync = this._callAsync;
	this.promise = this._promise;
}

_insert(item) {
    this._resetCompilation();
    let before;
    if (typeof item.before === "string") {
        before = new Set([item.before]);
    } else if (Array.isArray(item.before)) {
        before = new Set(item.before);
    }
    let stage = 0;
    if (typeof item.stage === "number") {
        stage = item.stage;
    }
    let i = this.taps.length;
    while (i > 0) {
        i--;
        const x = this.taps[i];
        this.taps[i + 1] = x;
        const xStage = x.stage || 0;
        if (before) {
            if (before.has(x.name)) {
                before.delete(x.name);
                continue;
            }
            if (before.size > 0) {
                continue;
            }
        }
        if (xStage > stage) {
            continue;
        }
        i++;
        break;
    }
    this.taps[i] = item;
} 

_runRegisterInterceptors实现了interceptor.register的功能,用于返回新的options。insert第一行会运行_resetCompilation去重新赋值,前面提前说明了callcallAsyncpromise的方法是生成出来的,所以每次tap的时候都需要重新去生成函数。

SyncHook和AsyncParallelHook

github.com/webpack/tap…

github.com/webpack/tap… 每个hooks的代码都大同小异,它们都extends HookCodeFactory ,并且返回实例化的Hooks。

HookCodeFactory

github.com/webpack/tap…

所有的hooks都扩展于HookCodeFactory ,当hooks.call执行时,会调用HookCodeFactory.prototype.create方法通过new Function生成调用函数。

class HookCodeFactory {
    constructor(config) {
        this.config = config;
        this.options = undefined;
        this._args = undefined;
    }

    create(options) {
        this.init(options);
        let fn;
        switch (this.options.type) {
            case "sync":
                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":
                    fn = new Function(
                        this.args({
                            after: "_callback"
                        }),
                        '"use strict";\n' +
                            this.header() +
                            this.contentWithInterceptors({
                                onError: err => `_callback(${err});\n`,
                                onResult: result => `_callback(null, ${result});\n`,
                                onDone: () => "_callback();\n"
                            })
                    );
                    break;
            case "promise":
                let errorHelperUsed = false;
                const content = this.contentWithInterceptors({
                    onError: err => {
                        errorHelperUsed = true;
                        return `_error(${err});\n`;
                    },
                    onResult: result => `_resolve(${result});\n`,
                    onDone: () => "_resolve();\n"
                });
                let code = "";
                code += '"use strict";\n';
                code += this.header();
                code += "return new Promise((function(_resolve, _reject) {\n";
                if (errorHelperUsed) {
                    code += "var _sync = true;\n";
                    code += "function _error(_err) {\n";
                    code += "if(_sync)\n";
                    code +=
                            "_resolve(Promise.resolve().then((function() { throw _err; })));\n";
                    code += "else\n";
                    code += "_reject(_err);\n";
                    code += "};\n";
                }
                code += content;
                if (errorHelperUsed) {
                    code += "_sync = false;\n";
                }
                code += "}));\n";
                fn = new Function(this.args(), code);
                break;
        }
        this.deinit();
        return fn;
    }
} 

call会调用compile生成函数并保存起来,只有intercept和tap才会重新去编译。

const hook = new SyncHook(["arg1", "arg2"]);
hook.tap('LoggerPlugin', (arg1, arg2) => { console.log(arg1 , arg2) })
hook.tap('SumPlugin', (arg1, arg2) => { console.log(arg1 + arg2) })
hook.intercept({call: ()=> {}})
hook.call(1,2)

// compile后的匿名函数
ƒ anonymous(arg1, arg2) {
    "use strict";
    var _context;
    var _x = this._x;
    var _taps = this.taps;
    var _interceptors = this.interceptors;
    _interceptors[0].call(arg1, arg2); //调用拦截器
    var _fn0 = _x[0];
    _fn0(arg1, arg2);  //LoggerPlugin callback
    var _fn1 = _x[1];  //SumPlugin callback
    _fn1(arg1, arg2);
} 

通过字符拼接和new Function,可以创造出异步或者同步代码,并且还能够插入拦截代码。

结尾

Tapable是webpack比较重要的库,基本大部分重要的模块都有它,并需要依赖它来与其他功能模块通信,来完成webpack构建。理解Tapable能够让你更容易去阅读webpack源码或者你只是想写一个Plugin,比如应该在compiler的哪个钩子阶段才能拿到编译产生的compiltion,应该以怎样的方式注入一个钩子进去修改构建产物的信息等等。这种解耦的方式能够很好帮助webpack扩展功能,但是因为源码实现问题,导致async hook不能被await,会产生callback hell,让阅读变得困难

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值