天天看點

這才是官方的tapable中文文檔起因

起因

搜尋引擎搜尋tapable中文文檔,你會看見各種翻譯,點進去一看,确實是官方的文檔翻譯過來的,但是webpack的文檔确實還有很多需要改進的地方,既然是開源的為什麼不去github上的tapable庫看呢,一看,确實,比webpack文檔上的描述得清楚得多.

tapable 是一個類似于nodejs 的EventEmitter 的庫, 主要是控制鈎子函數的釋出與訂閱,控制着webpack的插件系.webpack的本質就是一系列的插件運作.

Tapable

Tapable庫 提供了很多的鈎子類, 這些類可以為插件建立鈎子

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");
           

安裝

npm install --save tapable
           

使用

所有的鈎子構造函數,都接受一個可選的參數,(這個參數最好是數組,不是tapable内部也把他變成數組),這是一個參數的字元串名字清單

const hook = new SyncHook(["arg1", "arg2", "arg3"]);
           

最好的實踐就是把所有的鈎子暴露在一個類的hooks屬性裡面:

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

    /* ... */
}
           

其他開發者現在可以這樣用這些鈎子

const myCar = new Car();

// Use the tap method to add a consument
// 使用tap 方法添加一個消費者,(生産者消費者模式)
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
           

這需要你傳一個名字去标記這個插件:

你可以接收參數

myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
           

在同步鈎子中, tap 是唯一的綁定方法,異步鈎子通常支援異步插件

// promise: 綁定promise鈎子的API
myCar.hooks.calculateRoutes.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
    // return a promise
    return google.maps.findRoute(source, target).then(route => {
        routesList.add(route);
    });
});
// tapAsync:綁定異步鈎子的API
myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => {
    bing.findRoute(source, target, (err, route) => {
        if(err) return callback(err);
        routesList.add(route);
        // call the callback
        callback();
    });
});

// You can still use sync plugins
// tap: 綁定同步鈎子的API
myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
    const cachedRoute = cache.get(source, target);
    if(cachedRoute)
        routesList.add(cachedRoute);
})
           

類需要調用被聲明的那些鈎子

class Car {
    /* ... */

    setSpeed(newSpeed) {    
        // call(xx) 傳參調用同步鈎子的API
        this.hooks.accelerate.call(newSpeed);
    }

    useNavigationSystemPromise(source, target) {
        const routesList = new List();
        // 調用promise鈎子(鈎子傳回一個promise)的API
        return this.hooks.calculateRoutes.promise(source, target, routesList).then(() => {
            return routesList.getRoutes();
        });
    }

    useNavigationSystemAsync(source, target, callback) {
        const routesList = new List();
        // 調用異步鈎子API
        this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
            if(err) return callback(err);
            callback(null, routesList.getRoutes());
        });
    }
}
           

鈎子會用最有效率的方式去編譯(建構)一個運作你的插件的方法,他生成的代碼依賴于一下幾點:

  • 你注冊的插件的個數.
  • 你注冊插件的類型.
  • 你使用的調用方法(call, promise, async) // 其實這個類型已經包括了
  • 鈎子參數的個數 // 就是你new xxxHook(['ooo']) 傳入的參數
  • 是否應用了攔截器(攔截器下面有講)

這些确定了盡可能快的執行.

鈎子類型

每一個鈎子都可以tap 一個或者多個函數, 他們如果運作,取決于他們的鈎子類型

  • 基本的鈎子, (鈎子類名沒有waterfall, Bail, 或者 Loop 的 ), 這個鈎子隻會簡單的調用每個tap進去的函數
  • Waterfall, 一個waterfall 鈎子,也會調用每個tap進去的函數,不同的是,他會從每一個函數傳一個傳回的值到下一個函數
  • Bail, Bail 鈎子允許更早的退出,當任何一個tap進去的函數,傳回任何值, bail類會停止執行其他的函數執行.(類似 Promise.reace())
  • Loop, TODO(我.... 這裡也沒描述,應該是寫文檔得時候 還沒想好這個要怎麼寫,我嘗試看他代碼去補全,不過可能需要點時間.)

此外,鈎子可以是同步的,也可以是異步的,Sync, AsyncSeries 和 AsyncParallel 鈎子就反應了這個問題

  • Sync, 一個同步鈎子隻能tap同步函數, 不然會報錯.
  • AsyncSeries, 一個 async-series 鈎子 可以tap 同步鈎子, 基于回調的鈎子(我估計是類似chunk的東西)和一個基于promise的鈎子(使用

    myHook.tap()

    ,

    myHook.tapAsync()

    myHook.tapPromise()

    .).他會按順序的調用每個方法.
  • AsyncParallel, 一個 async-parallel 鈎子跟上面的 async-series 一樣 不同的是他會把異步鈎子并行執行(并行執行就是把異步鈎子全部一起開啟,不按順序執行).

攔截器(interception)

所有鈎子都提供額外的攔截器API

// 注冊一個攔截器
myCar.hooks.calculateRoutes.intercept({
    call: (source, target, routesList) => {
        console.log("Starting to calculate routes");
    },
    register: (tapInfo) => {
        // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
        console.log(`${tapInfo.name} is doing its job`);
        return tapInfo; // may return a new tapInfo object
    }
})
           

call:

(...args) => void

當你的鈎子觸發之前,(就是call()之前),就會觸發這個函數,你可以通路鈎子的參數.多個鈎子執行一次

tap:

(tap: Tap) => void

每個鈎子執行之前(多個鈎子執行多個),就會觸發這個函數

loop:

(...args) => void

這個會為你的每一個循環鈎子(LoopHook, 就是類型到Loop的)觸發,具體什麼時候沒說

register:

(tap: Tap) => Tap | undefined

每添加一個

Tap

都會觸發 你interceptor上的register,你下一個攔截器的register 函數得到的參數 取決于你上一個register傳回的值,是以你最好傳回一個 tap 鈎子.

Context(上下文)

插件和攔截器都可以選擇加入一個可選的 context對象, 這個可以被用于傳遞随意的值到隊列中的插件和攔截器.

myCar.hooks.accelerate.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`.
        // 如果最少有一個插件使用 `context` 那麼context 一開始是一個空的對象
        // If no plugins use `context: true`, then `context` is undefined
        // 如過tap進去的插件沒有使用`context` 的 那麼内部的`context` 一開始就是undefined
        if (context) {
            // Arbitrary properties can be added to `context`, which plugins can then access.    
            // 任意屬性都可以添加到`context`, 插件可以通路到這些屬性
            context.hasMuffler = true;
        }
    }
});

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

HookMap

一個 HookMap是一個Hooks映射的幫助類

const keyedHook = new HookMap(key => new SyncHook(["arg"]))
           
keyedHook.tap("some-key", "MyPlugin", (arg) => { /* ... */ });
keyedHook.tapAsync("some-key", "MyPlugin", (arg, callback) => { /* ... */ });
keyedHook.tapPromise("some-key", "MyPlugin", (arg) => { /* ... */ });
           
const hook = keyedHook.get("some-key");
if(hook !== undefined) {
    hook.callAsync("arg", err => { /* ... */ });
}
           

鈎子映射接口(HookMap interface)

Public(權限公開的):

interface Hook {
    tap: (name: string | Tap, fn: (context?, ...args) => Result) => void,
    tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
    tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
    intercept: (interceptor: HookInterceptor) => void
}

interface HookInterceptor {
    call: (context?, ...args) => void,
    loop: (context?, ...args) => void,
    tap: (context?, tap: Tap) => void,
    register: (tap: Tap) => Tap,
    context: boolean
}

interface HookMap {
    for: (key: any) => Hook,
    tap: (key: any, name: string | Tap, fn: (context?, ...args) => Result) => void,
    tapAsync: (key: any, name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
    tapPromise: (key: any, name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
    intercept: (interceptor: HookMapInterceptor) => void
}

interface HookMapInterceptor {
    factory: (key: any, hook: Hook) => Hook
}

interface Tap {
    name: string,
    type: string
    fn: Function,
    stage: number,
    context: boolean
}
           

Protected(保護的權限),隻用于類包含的(裡面的)鈎子

interface Hook {
    isUsed: () => boolean,
    call: (...args) => Result,
    promise: (...args) => Promise<Result>,
    callAsync: (...args, callback: (err, result: Result) => void) => void,
}

interface HookMap {
    get: (key: any) => Hook | undefined,
    for: (key: any) => Hook
}
           

MultiHook

把其他的Hook 重定向(轉化)成為一個 MultiHook

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

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

OK 所有的内容我都已翻譯完成.

其中有很多不是直譯,這樣寫下來感覺就是按照原文的脈絡重新寫了一遍....,應該能更清楚明白,要不是怕丢臉我就給個原創了,哈哈.

之後, 我還會寫一篇完整的原創解析,直擊源碼,搞定tapable, 完全了解webpack插件系統(webpack本來就是一個插件的事件流), 好久沒寫原創了. 我自己也很期待.

來源:

https://segmentfault.com/a/1190000017420937

繼續閱讀