Tapable- Источник механизма плагина для разрешения WebPack

Webpack
Tapable- Источник механизма плагина для разрешения WebPack

WebpackЕго успех заключается не только в мощных возможностях упаковки и построения, но и в гибком механизме подключаемых модулей.

Webpack — это, по сути, механизм потока событий, его рабочий процесс заключается в последовательном подключении различных плагинов, а ядром всего этого является Tapable.

изучениеWebpackВы часто можете увидеть приведенное выше введение. То есть учитьсяWebpackСуть в том, чтобы учитьсяTapable. учиться лучшеWebpackпринцип.

1. Нажимаемый

фактическиtapableОсновная идея чем-то похожа наnode.jsсерединаevents,базовыйОпубликовать/подписатьсямодель.

const EventEmitter = require('events');
const myEmitter = new EventEmitter();

// 注册事件对应的监听函数
myEmitter.on('start', (params) => {
    console.log("输出", params)
});

// 触发事件 并传入参数
myEmitter.emit('start', '学习webpack工作流'); // 输出 学习webpack工作流

2. Знакомство с цепляющими крючками

первый,tapableПредусмотрено 10 крючков следующим образом.

tapable钩子介绍

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

Во-вторых, краткое введение в использование всех хуков выглядит следующим образом: (вы можете бросить беглый взгляд на него, просто посмотрите вниз)

серийный номер имя хука способ исполнения Использовать баллы
1 SyncHook Синхронный последовательный Не заботьтесь о возвращаемом значении функции слушателя
2 SyncBailHook Синхронный последовательный Пока возвращаемое значение одной из функций прослушивателя не является неопределенным, пропустите всю остальную логику.
3 SyncWaterfallHook Синхронный последовательный Возвращаемое значение предыдущей функции прослушивателя может быть передано следующей функции прослушивателя.
4 SyncLoopHook Синхронизированный цикл Когда функция прослушивателя срабатывает, если функция прослушивателя возвращает true, функция прослушивателя будет выполняться повторно, а если она возвращает значение undefined, это означает выход из цикла.
5 AsyncParallelHook Асинхронный параллелизм Не заботьтесь о возвращаемом значении функции слушателя
6 AsyncParallelBailHook Асинхронный параллелизм Пока возвращаемое значение функции прослушивателя не равно нулю, выполнение следующей функции прослушивателя будет игнорироваться, и она перейдет непосредственно к функции обратного вызова, которая запускает привязку функции, например callAsync, а затем выполнит связанную функцию обратного вызова. .
7 AsyncSeriesHook Асинхронный последовательный Не заботьтесь о параметрах callback()
8 AsyncSeriesBailHook Асинхронный последовательный Если параметр callback() не равен нулю, функция обратного вызова, которая запускает привязку функции, например callAsync, будет выполняться напрямую.
9 AsyncSeriesWaterfallHook Асинхронный последовательный Второй параметр обратного вызова (ошибка, данные) в предыдущей функции мониторинга может использоваться как параметр следующей функции мониторинга.
10 AsyncSeriesLoopHook Асинхронный последовательный Может инициировать вызовы цикла обработчика.

3. Введение в использование вышеупомянутого хука

(1.1) Синхрук

Синхронный последовательный, не заботьтесь о возвращаемом значении функции слушателя.

Начнем с самого простогоSyncHook, на самом деле каждыйHookОни все одинаковые, и легко понять одно из других.

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

//所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
let queue = new SyncHook(['param1']); 

// 订阅tap 的第一个参数是用来标识订阅的函数的
queue.tap('event 1', function (param1) {
    console.log(param1, 1);
});

queue.tap('event 2', function (param1) {
    console.log(param1, 2);
});

queue.tap('event 3', function () {
    console.log(3);
});

// 发布的时候触发订阅的函数 同时传入参数
queue.call('hello');

// 控制台输出
/* hello 1
   hello 2
   3
*/

Как видите, события, на которые подписан этот хук, выполняются синхронно по порядку.

(1.2) Принцип SyncHook

Простое моделирование принципа.

class SyncHook{
    constructor(){
        this.taps = [];
    }

    // 订阅
    tap(name, fn){
        this.taps.push(fn);
    }

    // 发布
    call(){
        this.taps.forEach(tap => tap(...arguments));
    }
}

(2.1) SyncBailHook

посмотри сноваSyncBailHookиспользование.

Пока возвращаемое значение одной из функций прослушивателя не является неопределенным, вся остальная логика пропускается.

let queue = new SyncBailHook(['param1']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。

// 订阅
queue.tap('event 1', function (param1) {// tap 的第一个参数是用来标识订阅的函数的
    console.log(param1, 1);
    return 1;
});

queue.tap('event 2', function (param1) {
    console.log(param1, 2);
});

queue.tap('event 3', function () {
    console.log(3);
});

// 发布
queue.call('hello', 'world');// 发布的时候触发订阅的函数 同时传入参数

// 控制台输出
/* hello 1 */

Как видите, до тех пор, пока возвращаемое значение одной из функций слушателя неundefined, то вся остальная логика пропускается.

(2.2) Принцип SyncBailHook

Простое моделирование принципа.

class SyncBailHook {
    constructor() {
        this.taps = [];
    }

    // 订阅
    tap(name, fn) {
        this.taps.push(fn);
    }

    // 发布
    call() {
        for (let i = 0, l = this.taps.length; i < l; i++) {
            let tap = this.taps[i];
            let result = tap(...arguments);
            if (result) {
                break;
            }
        }
    }
}

(3) Обзор SyncHook и SyncBailHook

вышесказанное2Поток выполнения хука показан на следующем рисунке:

сквозь это2Введение в хуки можно найтиtapableпредоставляет разнообразныеhookПриходите и помогите нам управлять выполнением событий.

tapableОсновная функция заключается в управлении потоком выполнения между серией зарегистрированных событий.Например, если я регистрирую три события, я могу надеяться, что они параллельны или выполняются синхронно и последовательно, или если одно из них терпит неудачу, последующие события не будет выполняться, эти функции могут бытьtapableизhookвыполнить.

Как и взаимосвязь между пробуждением, походом на работу и завтраком, пробуждение должно быть приоритетом. Но еда и поход на работу — не обязательно одно и то же. Что, если ты опоздаешь? Может быть, отказаться от завтрака!

吃饭

4. Интерпретация исходного кода Tapable

Помните суть, ядроcallа такжеtapдва метода.
Помните суть, ядроcallа такжеtapдва метода.
Помните суть, ядроcallа такжеtapдва метода.

Тогда давайте посмотримtapableисходный кодSyncHookКак это достигается следующим образом. Опять же, после прочтения одного, другие, естественно, будут поняты. Для понимания исходный код минимизирован, часть неосновного кода удалена.

// node_modules/tapable/lib/SyncHook.js
const factory = new SyncHookCodeFactory();
// 继承基础Hook类
class SyncHook extends Hook {
    // 重写Hook的compile方法
    compile(options) {
        // 开发者订阅的事件传
        factory.setup(this, options);
        // 动态生成call方法
    	return factory.create(options);
    }
}
module.exports = SyncHook;

Основной код очень прост, вы можете видетьSyncHookтолько что унаследовалHookбазовый класс. и переписанcompileметод.

Первый взгляд наHookбазовыйtapметод. Вы можете видеть каждый звонокtap, состоит в том, чтобы собрать текущийhookЭкземпляр всех подписанных событий вtapsмножество.

// node_modules/tapable/lib/Hook.js
// 订阅
tap(options, fn) {
    // 同步 整理配置项
    options = Object.assign({ type: "sync", fn: fn }, options);
    // 将订阅的事件存储在taps里面
    this._insert(options);
}

_insert(item) {
    // 将item 推进 this.taps
    this.taps[i] = item;
}

Тогда взглянитеHookбазовыйcallКак реализован метод.

// node_modules/tapable/lib/Hook.js
class Hook {
    constructor(args) {
    	this.taps = [];
    	this.call = this._call;
    }

    compile(options) {
    	// 继承类必须重写compile
    	throw new Error("Abstract: should be overriden");
    }
    
    // 执行compile生成call方法
    _createCall(type) {
    	return this.compile({
            taps: this.taps,
    		// ...等参数
    	});
    }
}

// 动态生成call方法
function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
    	// 创造call等函数
    	this[name] = this._createCall(type);
    	// 执行触发call等函数
    	return this[name](...args);
    };
}

// 定义_call方法
Object.defineProperties(Hook.prototype, {
    _call: {
    	value: createCompileDelegate("call", "sync"),
    	configurable: true,
    	writable: true
    },
});

С помощью приведенного выше кода мы можем найти, чтоcallЧто такое метод, переопределяяcompileсгенерированный метод. Тогда давайте посмотримcompileЧто именно делает метод.

Давайте взглянемSyncHookвесь код.

// node_modules/tapable/lib/SyncHook.js
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

// 继承工厂类
class SyncHookCodeFactory extends HookCodeFactory {
    // call方法个性化定制
    content({ onError, onDone, rethrowIfPossible }) {
    	return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
    	});
    }
}

const factory = new SyncHookCodeFactory();

// 继承基础Hook类
class SyncHook extends Hook {
    // 重写Hook的compile方法
    compile(options) {
        // 开发者订阅的事件传
        factory.setup(this, options);
        // 动态生成call方法
    	return factory.create(options);
    }
}

module.exports = SyncHook;

можно увидетьcompileв основном выполнятьfactoryметод, в то время какfactoryдаSyncHookCodeFactory, который наследуетHookCodeFactoryкласс, тогдаfactoryэкземпляр называетсяsetupметод.

setupчто будетtapsМетоды событий, на которые подписаны, унифицированы дляthis._x;

// node_modules/tapable/lib/HookCodeFactory.js
setup(instance, options) {
    // 将taps里的所有fn 赋值给 _x
    instance._x = options.taps.map(t => t.fn);
}

тогда посмотри еще разfactoryэкземпляр называетсяcreateметод.

// node_modules/tapable/lib/HookCodeFactory.js
create(options) {
    this.init(options);
    let fn;
    switch (this.options.type) {
        case "sync":
            fn = new Function(
                // 参数
                this.args(),
                // 函数体
                '"use strict";\n' +
                // 获取一些需要的变量
                this.header() +
                // 事件运行逻辑
                this.content({
                    onError: err => `throw ${err};\n`,
                    onResult: result => `return ${result};\n`,
                    resultReturns: true,
                    onDone: () => "",
                    rethrowIfPossible: true
                })
        );
        break;
    }
}

createВсе входящие события будут собраны. последнее поколениеcallметод. Ниже приведен окончательный результат нашего случая.callметод.

function anonymous(param1) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(param1);
    var _fn1 = _x[1];
    _fn1(param1);
    var _fn2 = _x[2];
    _fn2(param1);
}

если вы подпишитесь5событие, приведенный выше код становится5последовательное выполнение функций. и параметр должен быть созданhookЭкземпляр объявлен. иначеtapПараметры, переданные в событие, бесполезны~

Приведенный выше код по-прежнему сильно сокращен, поэтому вы можете сразу перейти к исходному коду, который очень лаконичен и прост для понимания. Автору респект. 👍

Подводя итог, ядроcallа такжеtapдва метода.其实还有tapAsyncПодождите... но принцип тот же.tapСобирайте подписанные события и запускайтеcallметод согласноhookТип динамически генерирует соответствующее тело выполнения. Как показано ниже, другиеhookРеализация такая же.

Hook设计原理

5. Применение Tapable в Webpack

WebpackПроцесс можно разделить на следующие три этапа:

воплощать в жизньwebpack, создастcompilerпример.

// node_modules/webpack/lib/webpack.js
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");

const webpack = (options, callback) => {
	// ...省略了多余代码...
    let compiler;
    if (typeof options === "object") {
    	compiler = new Compiler(options.context);
    } else {
    	throw new Error("Invalid argument: options");
    }
})

мы обнаруживаемCompilerнаследуетсяTapableиз. нашли в то же времяwebpackжизненный циклhooksВсе виды крючков.

// node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable {
    constructor(context) {
    super();
        this.hooks = {
            /** @type {AsyncSeriesHook<Stats>} */
            done: new AsyncSeriesHook(["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, Buffer>} */
            assetEmitted: new AsyncSeriesHook(["file", "content"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterEmit: new AsyncSeriesHook(["compilation"]),
        
            // ....等等等很多 大家看下源码吧.... 不看也没有关系
        }
    }
}

а затем инициализироватьwebpackВ процессе настройки он будет циклически проходить через наши сконфигурированные иwebpackВсе плагины по умолчаниюplugin.

// 订阅在options中的所有插件
if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
}

Этот процесс будетpluginвсе вtapСобытия собираются в каждый жизненный циклhookсередина. Наконец, согласно каждомуhookвоплощать в жизньcallПорядок методов (он же жизненный цикл). ты можешь положить всеpluginреализовано.

В качестве примера ниже приведен код часто используемого нами плагина горячего обновления, который подписывается наadditionalPassЖдатьhook.

热更新插件
То естьwebpackЕго рабочий процесс может сочетать различные плагиныpluginПричина тандема, и в основе всего этого то, чтоTapable.

Шесть, плюнь слот

Хотя дизайн плагина очень гибкий, мы можем писать операции плагинаwebpackвесь жизненный цикл. Однако я также обнаружил некоторые проблемы, связанные с дизайном плагина, то есть опыт чтения исходного кода очень плохой:

(1) Соединение ослаблено. использоватьtapableКрючки аналогичны режиму мониторинга событий. Хотя они могут быть эффективно развязаны, регистрация крючков практически не связана с вызовом.

(2) Я вижу, что модуль в исходном коде предоставляет несколько хуков, но я не знаю, когда и где будет вызываться хук, и какие методы прописаны на хуке, когда и где. Раньше их решали путем поиска в кодовой базе ключевых слов.

(3) Есть много крючков.webpackКрючки внутри очень большие, и их количество достигает180+,

Ссылка на ссылку

В этой статье в основном говорится о принципе, пониманииtapable. Чтобы узнать об использовании других хуков, смотрите эту статью.