webpack 之 tapable 學習

前言

webpack 你們應該都耳熟能詳了。我的感受,webpack的本質就是讓一堆的 LoaderPluginwebpack的可支配範圍內,有序可控的執行,最終生成一堆可在瀏覽器中執行的 code 和 一些狀態信息。而這些 LoaderPlugin,有用戶自定義的,也有webpack 本身內部定義的。javascript

Loader 的運行機制,不是這篇文章講述的內容,有須要的朋友,能夠看下我以前的這篇文章:webpack之 loaderhtml

webpack的設計思想仍是很好的,我以爲這個思想和漸進加強有殊途同歸之妙。它本身實現一套打包的主流程,而後在 ComplierComplation 對象上暴露出一些鉤子,這些鉤子起到了相似於生命週期的做用,容許用戶在不一樣的打包階段經過鉤子來增長用戶所需的各類各樣的功能。webpack聰明的將一些不肯定因素拋給使用者去處理,也就是:標準我來定,細節和擴展你本身弄。java

舉個栗子,Complier對象上的鉤子,就有這麼多 compiler鉤子node

那麼這些鉤子是什麼呢?它們其實都是 tapable 的實例。webpack

那麼咱們怎麼調用這些鉤子呢?經過編寫 webpack Plugingit

今天咱們來分析下 webpack 插件機制的基石 - tapable。考慮到 wepback4,是使用 tapable1.1.0 版本,因此這篇文章就用 1.1.0 的版原本分析。github

注意:1.x.x版本和0.x.x版本,使用方式是有區別的,代碼也被重構過。web

若是對 tapable 還未有了解的朋友,能夠參考下這裏:npm

tapable@v1.1.0segmentfault

這纔是官方的tapable中文文檔

概念與用法

概念

我以爲tapable 整個庫其實就是一套 發佈訂閱模式 的實現,相似 nodejs 的 EventEmitter

有的道友說是生產者消費者模式, 發佈訂閱模式本質上也是一種生產者消費者模式,至於他們的區別,應該就是發佈訂閱模式的功能更單一,而生產者消費者模式的抽象級別更高。究竟是 發佈訂閱模式 仍是 生產者消費者模式,我不能肯定,有待考究,聰明的你若是知道的話,能夠評論告訴下我哦。

用法

tapable 支持三種方式註冊插件名稱 ,分別是 tap, tapAsync, tapPromise

tap 表示使用的同步鉤子,tapAsynctapPromise 表示使用的是異步鉤子。

與此對應的,支持三種調用方式 call, callAsyncpromise,注意須要一一對應。

SyncHook 舉個栗子

const { SyncHook } = require('tapable');

// 初始化時傳入參數名稱
const myHook = new SyncHook(['name', 'age']);

// 添加事件
myHook.tap('pluginName1', (name, age) => console.log('pluginName1', name, age))
myHook.tap('pluginName2', (name, age) => console.log('pluginName2', name, age))

// 觸發
myHook.call('jk', 26);

// 輸出
// pluginName1 jk 26
// pluginName2 jk 26
複製代碼

有木有發現和咱們使用 addEventListener 添加事件很是類似?是的,就是這麼像。因此不要怕它,咱們只須要注意, 聲明 Hook 時,傳入預置的參數名稱,而後用 tap監聽事件,用 call傳入參數觸發事件。

用起來和 jQueryontrigger 一個意思,固然內部處理流程是不同。

這裏咱們用的是 SyncHook,還有 Async 類型的 Hook,也就是異步鉤子,用起來也是相似的。

tapAsync監聽,callAsync 觸發

tapPromise監聽,promise 觸發

代碼以下:

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

class Model {
    constructor() {
        this.hooks = {
            asyncHook: new AsyncParallelHook(['name']),
            promiseHook: new AsyncParallelHook(['age'])
        };
    }

    callAsyncHook(name, callback) {
        this.hooks.asyncHook.callAsync(name, err => {
            if (err) return callback(err);
            callback(null);
        });
    }

    callPromiseHook(age) {
        return this.hooks.promiseHook.promise(age).then(res => console.log(res));
    }
}

const model = new Model();

// Async 方式監聽事件
model.hooks.asyncHook.tapAsync('AsyncPluginName', (name, callback) => {
    const pluginName = 'AsyncPluginName'; 
    setTimeout( () => {
        console.log(pluginName, name);
    }, 2000);
});
model.callAsyncHook('jk');
// 2秒後輸出:AsyncPluginName jk

// Promise 方式監聽事件
model.hooks.promiseHook.tapPromise('PromisePluginName', (age) => {
    const pluginName = 'PromisePluginName';
    return new Promise((resolve, reject) => {        
        setTimeout(() => {
            console.log(pluginName, age);
            resolve(pluginName);
        }, 4000);
    });
});
model.callPromiseHook(26);
// 4秒後輸出:PromisePluginName 26
複製代碼

實踐:編寫一個 Plugin

Webpack Plugin 有一套固定格式,這裏以 Async event hooks 舉例

其餘的類型,可參考官方文檔

const pluginName = 'HelloWorldPlugin';

class HelloWorldPlugin {
    apply(complier) {
        compiler.hooks.someHook.tapAsync(
            pluginName,
            (compilation, callback) => {
                // Do something async... 
                setTimeout( () => {
                    console.log('Done with async work...');
                    callback();
                }, 1000);
            }
        )
    }
}

module.exports = HelloWorldPlugin;
複製代碼

有的同窗可能會好奇,爲何在 webpack 的 plugins 參數裏,配置一些插件的實例,就會出現神奇的效果。

由於 webpack 在初始化 時,會遍歷 plugins 參數中的實例,依次調用實例的 apply 方法,並將 complier 做爲參數。

源碼出自: webpack\lib\webpack.js

到這裏應該能明白,爲何 webpack 的插件須要按照那樣的格式去寫了。

基於 HTMLWebpackPlugin 擴展一個 Plugin

目錄結構如圖

MyPlugin.js 代碼以下,我這裏是直接 copy HTMLWebpackPlugin 官方文檔的。

const HtmlWebpackPlugin = require('html-webpack-plugin');

const pluginName = 'MyPlugin';

class MyPlugin {
    apply(compiler) {
        compiler.hooks.compilation.tap(pluginName, (compilation) => {
            console.log('The compiler is starting a new compilation...')

            // Staic Plugin interface |compilation |HOOK NAME | register listener 
            HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
                pluginName, // <-- Set a meaningful name here for stacktraces
                (data, cb) => {
                    // Manipulate the content
                    data.html += 'The Magic Footer'
                    // Tell webpack to move on
                    cb(null, data)
                }
            )
        })
    }
}

module.exports = MyPlugin;
複製代碼

注意一點:HTMLWebpackPlugin 默認安裝 3.2.0 版本,這個版本還只是支持舊版的插件寫法,沒有 HtmlWebpackPlugin.getHooks 這個方法。我這裏安裝的是最新的master分支。

一切就緒後,npm run prod,可看到 dist/index.html的內容以下:

一個簡單的插件算是寫好並運行成功了。

寫在最後

但願本文能對讀者有幫助。

若是有錯誤的地方,還請指出。

謝謝閱讀。

代碼在此:webpack-plugin-test

參考

tapable@v1.1.0

Writing a Plugin

html-webpack-plugin

這纔是官方的tapable中文文檔

不知足於只會使用系列: tapable

生產者消費者模式與訂閱發佈者模式的區別

推薦兩篇好文:

Webpack基本架構淺析

Webpack 核心模塊 tapable 解析

相關文章
相關標籤/搜索