Ху Нин: Группа поддержки платформы отдела передовых технологий WeDoctor в последнее время верит в счастье~
tapable — это библиотека, похожая на EventEmitter в Node.js, но больше ориентированная на запуск и обработку пользовательских событий. Webpack отделяет реализацию от процесса через tapable, и все конкретные реализации существуют в виде плагинов.
Отношения между Tapable и webpack
- Что такое вебпак?
По сути, webpack — это сборщик статических модулей для современных приложений JavaScript. Когда webpack обрабатывает приложение, он внутренне строит граф зависимостей, который сопоставляется с каждым модулем, требуемым проектом, и генерирует один или несколько пакетов.
- Важные модули веб-пакета
- Вход
- выход
- загрузчик (преобразует исходный код модуля)
- плагин (внедряйте логику расширения в определенное время в процессе сборки веб-пакета, чтобы изменить результат сборки или сделать то, что вы хотите)
Плагины являются основой веб-пакета. Сам веб-пакет построен поверх той же системы плагинов, которую вы используете в своей конфигурации веб-пакета.
- процесс сборки вебпака
Механизм потока событий в сущности веб-пакета, это отдельная серия подключаемых модулей рабочего процесса, для достижения ядра всего этого Tapable. webpack в ядре отвечает за компиляцию компилятора и отвечает за создание экземпляра Tapable для компиляции пакета (webpack5 назад). После webpack5 наступает время запуска расписания путем определения хуков с именами атрибутов. Tapable должен действовать как сложная модель подписки на публикацию.
Возьмите компилятор в качестве примера:
// webpack5 前,通过继承
...
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
...
class Compiler extends Tapable {
constructor(context) {
super();
...
}
}
// webpack5
...
const {
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
...
class Compiler {
constructor(context) {
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
...
})
}
...
}
Как использовать Tapable
Tapable предоставляет 9 классов Hooks. Роль этих классов Hooks состоит в том, чтобы создать процесс выполнения путем создания экземпляра и предоставить методы регистрации и выполнения.Разные классы Hook приведут к различным процессам выполнения.
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
Каждый хук может быть зарегистрирован несколько раз, способ его срабатывания зависит от типа хука.
Классифицируются по синхронным, асинхронным (последовательным, параллельным)
- Синхронизация: может быть зарегистрирована только синхронными функциями, такими как myHook.tap().
- Asyncerseries: могут быть зарегистрированы в синхронных, основанных на обратном вызовах, основанных на обещаниях, таких как myhook.tap (), myhook.tapasync (), myhook.toverise (). Заказ исполнения является серийный
- AsyncParallel: можно зарегистрировать с помощью синхронных функций на основе обратного вызова и обещаний, таких как myHook.tap(), myHook.tapAsync(), myHook.tapPromise(). Порядок выполнения параллельный
Сортировать по режиму исполнения
- Базовый: выполнять каждую функцию события, не заботясь о возвращаемом значении функции.
- Залог: выполнить каждую функцию события и вернуть первый результат результата !== undefined и не продолжать выполнение.
- Водопад: если результат предыдущей функции события result !== undefined, результат будет использоваться в качестве первого параметра последней функции события.
- Цикл: продолжайте перебирать функции событий, пока все результаты функций не приведут к результату === undefined
Как использовать
Использование класса хука
Проще говоря, следующие шаги
- Создание экземпляра конструктора Hook
- Зарегистрироваться (один или несколько раз)
- Выполнить (передать параметры)
- При необходимости вы также можете добавить мониторинг-перехватчики на весь процесс (включая регистрацию и выполнение)
В качестве примера возьмем простейший SyncHook:
// 简单来说就是实例化 Hooks 类
// 接收一个可选参数,参数是一个参数名的字符串数组
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
// 注册
// 第一个入参为注册名
// 第二个为注册回调方法
hook.tap("1", (arg1, arg2, arg3) => {
console.log(1, arg1, arg2, arg3);
return 1;
});
hook.tap("2", (arg1, arg2, arg3) => {
console.log(2, arg1, arg2, arg3);
return 2;
});
hook.tap("3", (arg1, arg2, arg3) => {
console.log(3, arg1, arg2, arg3);
return 3;
});
// 执行
// 执行顺序则是根据这个实例类型来决定的
hook.call("a", "b", "c");
//------输出------
// 先注册先触发
1 a b c
2 a b c
3 a b c
Приведенный выше пример относится к синхронному случаю. Если регистрация асинхронная, то:
let { AsyncSeriesHook } = require("tapable");
let queue = new AsyncSeriesHook(["name"]);
console.time("cost");
queue.tapPromise("1", function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(1, name);
resolve();
}, 1000);
});
});
queue.tapPromise("2", function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(2, name);
resolve();
}, 2000);
});
});
queue.tapPromise("3", function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(3, name);
resolve();
}, 3000);
});
});
queue.promise("weiyi").then((data) => {
console.log(data);
console.timeEnd("cost");
});
Класс HookMap использует
A HookMap is a helper class for a Map with Hooks
Официально рекомендуется создавать экземпляры всех хуков для хуков свойств класса, например:
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
/* ... */
setSpeed(newSpeed) {
// following call returns undefined even when you returned values
this.hooks.accelerate.call(newSpeed);
}
}
Регистрация и исполнение:
const myCar = new Car();
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
myCar.setSpeed(1)
А HookMap — вспомогательный класс для этого рекомендуемого метода написания. Конкретное использование:
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 использует
A helper Hook-like class to redirect taps to multiple other hooks
Эквивалентно предоставлению вспомогательного класса, в котором хранится список хуков:
const { MultiHook } = require("tapable");
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
Принцип Tapable
Ядро состоит в том, чтобы хранить и запускать зарегистрированный обратный вызов через Hook, а также контролировать процесс выполнения регистрации через HookCodeFactory.
Во-первых, давайте посмотрим на структуру файла lib Tapable.Основной код хранится в папке lib. Где index.js — это запись для всех доступных классов. Hook и HookCodeFactory — основные классы, основная функция которых — регистрация и запуск процесса. Также есть два вспомогательных класса HookMap и MultiHook и служебный класс util-browser. Остальные 9 видов хуков, упомянутых в приведенной выше классификации, являются производными от Hook и HookCodeFactory в качестве базового класса. Вся структура очень проста и понятна. как показано на рисунке:
Далее поговорим о двух наиболее важных классах, которые также являются ядром исходного кода Tapable.
Hook
Сначала загляните в свойства Hook, вы увидите, что в свойствах есть знакомые методы регистрации: tap, tapAsync, tapPromise. Метод выполнения: call, promise, callAsync. И сохраните все зарегистрированные краны элемента. Параметры конструктора — это параметры при создании экземпляра каждого хука. Из атрибутов можно узнать, что класс Hook предоставляет самые основные методы регистрации и выполнения для подклассов, которые его наследуют.
class Hook {
constructor(args = [], name = undefined) {
this._args = args;
this.name = name;
this.taps = [];
this.interceptors = [];
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;
this._x = undefined;
this.compile = this.compile;
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;
}
...
}
Так как же класс Hook собирает элементы регистрации? Как показано в коде:
class Hook {
...
tap(options, fn) {
this._tap("sync", options, fn);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
_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);
// 执行注册的 interceptors 的 register 监听,并返回执行后的 options
options = this._runRegisterInterceptors(options);
// 收集到 taps 中
this._insert(options);
}
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if (newOptions !== undefined) {
options = newOptions;
}
}
}
return options;
}
...
}
Видно, что все три метода регистрации реализованы через _tap, но входящий тип отличается. _tap выполняет две основные функции.
- Выполнить interceptor.register и вернуть параметры
- Соберите записи реестра в список this.taps, отсортированные по стадии и ранее. (стадия и до являются необязательными параметрами при регистрации)
После сбора элементов регистрации следующим шагом будет выполнение этого процесса:
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() {
...
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;
...
}
compile(options) {
throw new Error("Abstract: should be overridden");
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
}
Можно сказать, что процесс выполнения такой же и, наконец, возвращает значение после выполнения компиляции через _createCall. Как видно из вышеизложенного, процесс выполнения Tapable является синхронным, асинхронным последовательным, асинхронным параллельным, циклическим и т. д. Таким образом, класс Hook предоставляет только компиляцию абстрактного метода, так что же такое компиляция? Это приводит к следующему основному классу, HookCodeFactory.
HookCodeFactory
Как следует из названия, этот класс представляет собой фабрику, возвращающую hookCode. Давайте сначала посмотрим, как используется фабрика. Вот как используется один из классов ловушек, AsyncSeriesHook:
const HookCodeFactory = require("./HookCodeFactory");
class AsyncSeriesHookCodeFactory extends HookCodeFactory {
content({ onError, onDone }) {
return this.callTapsSeries({
onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true),
onDone
});
}
}
const factory = new AsyncSeriesHookCodeFactory();
// options = {
// taps: this.taps,
// interceptors: this.interceptors,
// args: this._args,
// type: type
// }
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function AsyncSeriesHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = AsyncSeriesHook;
hook.compile = COMPILE;
...
return hook;
}
Обязанность HookcodeFactory — назначить исполняемый код для hook.compile, чтобы Hook получил возможность выполнения. Взгляните на внутреннюю операционную логику этого класса:
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
...
create(options) {
...
this.init(options);
// type
switch (this.options.type) {
case "sync": fn = new Function(省略...);break;
case "async": fn = new Function(省略...);break;
case "promise": fn = new Function(省略...);break;
}
this.deinit();
return fn;
}
init(options) {
this.options = options;
this._args = options.args.slice();
}
deinit() {
this.options = undefined;
this._args = undefined;
}
}
Окончательный результат компиляции — это fn, возвращаемый функцией create, а fn создается функцией new Function(). Тогда дело в этой новой функции.
Сначала поймите синтаксис новой функции
new Function ([arg1[, arg2[, ...argN]],] functionBody)
- arg1, arg2, ... argN: Имена аргументов, используемых функцией, должны быть правильно названы. Имя параметра представляет собой строку допустимых идентификаторов JavaScript или список допустимых строк, разделенных запятыми, например "x", "theValue" или "a,b".
- functionBody: строка, содержащая оператор JavaScript, включающий определение функции.
Основное использование:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
// expected output: 8
Методы, использующие конструктор функций:
class HookCodeFactory {
create() {
...
fn = new Function(this.args({...}), code)
...
return fn
}
args({ before, after } = {}) {
let allArgs = this._args;
if (before) allArgs = [before].concat(allArgs);
if (after) allArgs = allArgs.concat(after);
if (allArgs.length === 0) {
return "";
} else {
return allArgs.join(", ");
}
}
}
This.args() — это имя параметра, передаваемое при возврате к выполнению, которое обеспечивает соответствующее значение параметра для следующего кода.
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
})
)
header() {
let code = "";
if (this.needContext()) {
code += "var _context = {};\n";
} else {
code += "var _context;\n";
}
code += "var _x = this._x;\n";
if (this.options.interceptors.length > 0) {
code += "var _taps = this.taps;\n";
code += "var _interceptors = this.interceptors;\n";
}
return code;
}
contentWithInterceptors() {
// 由于代码过多这边描述一下过程
// 1. 生成监听的回调对象如:
// {
// onError,
// onResult,
// resultReturns,
// onDone,
// rethrowIfPossible
// }
// 2. 执行 this.content({...}),入参为第一步返回的对象
...
}
Соответствующее тело функции генерируется заголовком и contentWithInterceptors. this.content вызывает разные методы в зависимости от типа хука.Следующий код вызывает callTapsSeries:
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
HookCodeFactory имеет три способа генерации кода:
// 串行
callTapsSeries() {...}
// 循环
callTapsLooping() {...}
// 并行
callTapsParallel() {...}
// 执行单个注册回调,通过判断 sync、async、promise 返回对应 code
callTap() {...}
- Параллельный (параллельный) принцип: параллельная ситуация возникает только тогда, когда она асинхронна, поэтому после выполнения всех кранов оцените, равен ли счетчик 0, и если он равен 0, выполните обратный вызов конца (счетчик равен 0, может быть, потому что краны все выполнено, есть Это может быть потому, что возвращаемое значение не неопределенное, вручную установленное в 0)
- Принцип цикла: сгенерируйте код do{}while(__loop) и присвойте значение после выполнения _loop для управления циклом.
- Серийный: генерировать и выполнять код в порядке нажатий.
- callTap: выполнить один зарегистрированный обратный вызов
- синхронизировать: выполнять по порядку
var _fn0 = _x[0];
_fn0(arg1, arg2, arg3);
var _fn1 = _x[1];
_fn1(arg1, arg2, arg3);
var _fn2 = _x[2];
_fn2(arg1, arg2, arg3);
- Принцип асинхронности: инкапсулировать одно касание в функцию _next[index].После выполнения текущей функции вызывается обратный вызов, и следующая функция _next[index] продолжает выполняться.Например,следующий код сгенерировано:
function _next1() {
var _fn2 = _x[2];
_fn2(name, (function (_err2) {
if (_err2) {
_callback(_err2);
} else {
_callback();
}
}));
}
function _next0() {
var _fn1 = _x[1];
_fn1(name, (function (_err1) {
if (_err1) {
_callback(_err1);
} else {
_next1();
}
}));
}
var _fn0 = _x[0];
_fn0(name, (function (_err0) {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
}));
- Обещание: инкапсулируйте одно касание в функцию _next[index]. Когда текущая функция будет выполнена, будет вызываться promise.then(), а затем продолжится выполнение следующей функции _next[index]. Например, следующий код генерируется:
function _next1() {
var _fn2 = _x[2];
var _hasResult2 = false;
var _promise2 = _fn2(name);
if (!_promise2 || !_promise2.then)
throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise2 + ')');
_promise2.then((function (_result2) {
_hasResult2 = true;
_resolve();
}), function (_err2) {
if (_hasResult2) throw _err2;
_error(_err2);
});
}
function _next0() {
var _fn1 = _x[1];
var _hasResult1 = false;
var _promise1 = _fn1(name);
if (!_promise1 || !_promise1.then)
throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')');
_promise1.then((function (_result1) {
_hasResult1 = true;
_next1();
}), function (_err1) {
if (_hasResult1) throw _err1;
_error(_err1);
});
}
var _fn0 = _x[0];
var _hasResult0 = false;
var _promise0 = _fn0(name);
if (!_promise0 || !_promise0.then)
throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')');
_promise0.then((function (_result0) {
_hasResult0 = true;
_next0();
}), function (_err0) {
if (_hasResult0) throw _err0;
_error(_err0);
});
Комбинируя вышеуказанный порядок выполнения и метод выполнения, мы получаем текущие 9 классов хуков. Если в будущем потребуется больше шаблонов, расширение можно завершить, просто увеличив порядок выполнения или режим выполнения.
как показано на рисунке:
Как помочь вебпаку
Плагины могут использовать доступные открытые методы для внедрения пользовательских шагов сборки в веб-пакет, которые будут запускаться в процессе сборки.
Вся процедура WebPack построен для генерации крюка крюк (тип крючка, то есть примеры примеров применяемых девяти типов), хранятся в крючках объекта. Примеры подключаемого доступа к соответствующему крючкому крюку или компилятору Compilation, регистрируйте (нажмите, Tapasync, TapPromise). Когда WebPack для выполнения соответствующих шагов будет выполнен (вызов, callasync, обещание) крюку, тем самым выполняя зарегистрированный обратный вызов. Для ConsologonBuildWebpackplugin Custom Plugin-in в качестве примера:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 构建过程开始!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
Вы можете увидеть обратный вызов, когда регистрация ловушек (TAP) в Apply находится в фазе RUN. Из класса Compiler вы можете узнать об экземпляре назначения AsyncSerieshook в объекте Hooks и вызвать зарегистрированный соответствующий обратный вызов с помощью this.hook.run.callasync при выполнении:
class Compiler {
constructor(context) {
this.hooks = Object.freeze({
...
run: new AsyncSeriesHook(["compiler"]),
...
})
}
run() {
...
const run = () => {
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
};
...
}
}
Как показано на рисунке, для процесса выполнения пользовательского плагина:
Суммировать
- Tapable предоставляет 9 хуков, основными методами являются регистрация, выполнение, перехватчик.
- Способ реализации tapable заключается в объединении строк в соответствии с типом ловушки и типом регистрации и передачей их в конструктор функций для создания нового объекта функции.
- Webpack использует tapable для обработки всего шага сборки. Он реализует гибкие требования к настройке для каждого этапа строительства.
Если у вас есть какие-либо комментарии, приветствуйте качество трех ссылок одним щелчком мыши, сокровище ~.
использованная литература
[1]Введение в плагин в официальной документации веб-пакета:Веб-пакет.doc: China.org/concepts/reviews…
[2] Введение, связанное с Tapable:woohoo. Main Peak Training.com/grow/HTML/1…
[3] исходный код с вкладками:GitHub.com/Веб-пакет/Taping…
[4] исходный код веб-пакета:GitHub.com/Webpack/Веб…