в предыдущей статье"Интерпретация исходного кода Webpack: разъяснение основного процесса компиляции", у нас есть общее представление об основном процессе компиляции webpack, в котором мы пропустили важный контентTapable
. Плагин webpack предоставляет сторонним разработчикам возможность подключиться к процессу компиляции в движке webpack, а Tapable является основной основой плагина.
В этой статье сначала анализируются основные принципы Tapable, и на их основе пишется собственный плагин.
Tapable
Если вы читали исходный код webpack, вы не должны быть знакомы с tapable. Не будет преувеличением сказать, что tapable — это супер-экономка webpack для управления потоком событий.
Основная функция Tapable — последовательное выполнение зарегистрированных событий, когда они запускаются в соответствии с разными ловушками. Это типичная модель «публикация-подписка». Tapable предоставляет в общей сложности девять типов крючков в двух категориях.
КромеSync
а такжеAsync
В дополнение к классификации, вы также должны были заметитьBail
,Waterfall
,Loop
и другие ключевые слова, которые определяют обратные вызовы зарегистрированных событий.handler
триггерная последовательность.
-
Basic hook
: В соответствии с порядком регистрации событий с последующей реализациейhandler
,handler
не мешать друг другу; -
Bail hook
: Выполнять последовательно в соответствии с порядком регистрации событий.handler
, если любой изhandler
возвращаемое значение неundefined
, то остальныеhandler
не будет реализовано; -
Waterfall hook
: Выполнять последовательно в соответствии с порядком регистрации событий.handler
,Предыдущийhandler
Возвращаемое значение будет использоваться в качестве следующегоhandler
вход; -
Loop hook
: Выполнять последовательно в соответствии с порядком регистрации событий.handler
, если естьhandler
Возвращаемое значение неundefined
, цепочка событий сновас нуляначать выполнение, пока всеhandler
оба возвращаютсяundefined
Основное использование
мы начинаем сSyncHook
Например:
const {
SyncHook
} = require("../lib/index");
let sh = new SyncHook(["name"])
sh.tap('A', (name) => {
console.log('A:', name)
})
sh.tap({
name: 'B',
before: 'A' // 影响该回调的执行顺序, 回调B比回调A先执行
}, (name) => {
console.log('B:', name)
})
sh.call('Tapable')
// output:
B:Tapable
A:Tapable
Здесь мы определяем хук синхронизацииsh
, обратите внимание, что его конструктор принимает параметр типа массива["name"]
, который представляет собой список параметров, которые получит его зарегистрированное событие, чтобы сообщить вызывающей стороне о том, что обратный вызов записывается.handler
какие параметры будут получены. В примере каждый обратный вызов события будет получатьname
параметр.
крючкомtap
Методы могут регистрировать обратные вызовыhandler
,передачаcall
метод для запуска ловушки и выполнения зарегистрированной функции обратного вызова по очереди.
в обратном вызове регистрацииB
когда входящийbefore
параметр,before: 'A'
, что напрямую влияет на порядок выполнения обратного вызова, то есть обратный вызов B сработает перед обратным вызовом A. Кроме того, вы также можете указать обратный вызовstage
для сортировки обратных вызовов.
Интерпретация исходного кода
Крюк базового класса
Из вышеприведенного примера мы видим, что на хуке два внешних интерфейса:tap
а такжеcall
,tap
Отвечает за регистрацию обратных вызовов событий,call
Отвечает за запуск событий.
Хотя Tapable предоставляет несколько типов хуков, все хуки наследуются от базового класса.Hook
, и процесс их инициализации аналогичен. Здесь мы все еще используемSyncHook
Например:
// 工厂类的作用是生成不同的compile方法,compile本质根据事件注册顺序返回控制流代码的字符串。最后由`new Function`生成真实函数赋值到各个钩子对象上。
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
// 覆盖Hook基类中的tapAsync方法,因为`Sync`同步钩子禁止以tapAsync的方式调用
const TAP_ASYNC = () => {
throw new Error("tapAsync is not supported on a SyncHook");
};
// 覆盖Hook基类中的tapPromise方法,因为`Sync`同步钩子禁止以tapPromise的方式调用
const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};
// compile是每个类型hook都需要实现的,需要调用各自的工厂函数来生成钩子的call方法。
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name); // 实例化父类Hook,并修饰hook
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
метод касания
при исполненииtap
Когда метод регистрирует обратный вызов, как он выполняется?
существуетHook
В базовом классе околоtap
Код выглядит следующим образом:
class Hook{
constructor(args = [], name = undefined){
this.taps = []
}
tap(options, fn) {
this._tap("sync", options, fn);
}
_tap(type, options, fn) {
// 这里省略入参预处理部分代码
this._insert(options);
}
}
Мы видим, что в конечном итоге выполнитсяthis._insert
метод, в то время какthis._insert
Задача состоит в том, чтобы поставить обратный вызовfn
вставляется внутрьtaps
массив, и согласноbefore
илиstage
Параметр для настройкиtaps
Сортировка массивов. Конкретный код выглядит следующим образом:
_insert(item) {
// 每次注册事件时,将call重置,需要重新编译生成call方法
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循环体中,依据before和stage调整回调顺序
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; // taps暂存所有注册的回调函数
}
звонит лиtap
,tapAsync
илиtapPromise
, перезвонюhandler
подготовка кtaps
Массив, который был сгенерирован перед опорожнениемcall
метод(this.call = this._call
).
метод вызова
После регистрации обратного вызова события, как инициировать событие дальше. такой же,call
Есть также три метода вызова:call
,callAsync
,promise
, соответствующий тремtap
способ зарегистрироваться. запустить синхронизациюSync
Использовать напрямую при перехвате событийcall
метод, запускающий асинхронностьAsync
Необходимо использовать при перехвате событийcallAsync
илиpromise
метод, продолжайте смотреть наHook
Базовый классcall
Как определить:
const CALL_DELEGATE = function(...args) {
// 在第一次执行call时,会依据钩子类型和回调数组生成真实执行的函数fn。并重新赋值给this.call
// 在第二次执行call时,直接运行fn,不再重复调用_createCall
this.call = this._createCall("sync");
return this.call(...args);
};
class Hoook {
constructor(args = [], name = undefined){
this.call = CALL_DELEGATE
this._call = CALL_DELEGATE
}
compile(options) {
throw new Error("Abstract: should be overridden");
}
_createCall(type) {
// 进入该函数体意味是第一次执行call或call被重置,此时需要调用compile去生成call方法
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
}
_createCall
позвонюthis.compile
метод для компиляции и генерации фактического вызоваcall
метод, но вHook
в базовом классеcompile
это пустая реализация. это требует наследованияHook
父类的子类必须实现这个方法(即抽象方法)。 назадSyncHook
Посмотреть вcompiler
Реализация:
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
const COMPILE = function(options) {
// 调用工厂类中的setup和create方法拼接字符串,之后实例化 new Function 得到函数fn
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.compile = COMPILE;
return hook;
}
существуетSyncHook
в классеcompile
вызовет фабричный классHookCodeFactory
изcreate
метод, здесьcreate
Интерьер временный,factory.create
возврат скомпилированfunction
, и, наконец, присвоеноthis.call
метод.
здесьHook
использовал трюк -ленивая функция, при первом указанииthis.call
метод, он будет работать доCALL_DELEGATE
В теле функцииCALL_DELEGATE
будет переназначенthis.call
, чтобы при следующем выполнении напрямую выполнить присвоенное значениеthis.call
метод без необходимости генерировать его сноваcall
процесс, оптимизирующий производительность.
Ленивые функции имеют два основных преимущества:
- Высокая эффективность: ленивая функция выполняет логику вычислений только при первом запуске, а затем возвращает результат первого выполнения при повторном запуске функции, что экономит много времени выполнения;
- Задержка выполнения: в некоторых сценариях необходимо оценить некоторую информацию об окружающей среде, и после ее определения нет необходимости повторно оценивать. можно понимать как
嗅探程序
. Например, его можно переписать с использованием ленивой загрузки следующим образом.addEvent
:
function addEvent(type, element, fun) {
if (element.addEventListener) {
addEvent = function(type, element, fun) {
element.addEventListener(type, fun, false);
};
} else if (element.attachEvent) {
addEvent = function(type, element, fun) {
element.attachEvent("on" + type, fun);
};
} else {
addEvent = function(type, element, fun) {
element["on" + type] = fun;
};
}
return addEvent(type, element, fun);
}
Фабричный класс HookCodeFactory
Как упоминалось в предыдущем разделе,factory.create
возврат скомпилированfunction
назначить наcall
метод.
Каждый тип хука создаст фабричный класс, отвечающий за объединение и планирование обратных вызовов.handler
функциональная строка для синхронизации, черезnew Function()
Метод создания экземпляра для создания функции выполнения.
Расширение: новая функция
Есть три способа определения функций в JavaScript:
// 定义1. 函数声明
function add(a, b){
return a + b
}
// 定义2. 函数表达式
const add = function(a, b){
return a + b
}
// 定义3. new Function
const add = new Function('a', 'b', 'return a + b')
Первые два определения функции являются «статическими», а так называемое «статическое» — когда функция определена, ее функция определена. Третий метод определения функции - "динамический", так называемый "динамический" означает, что функция функции может изменяться в процессе выполнения программы.
Определение 1 также отличается от определения 2. Наиболее важным отличием является «подъемное» поведение JavaScript-функций и объявлений переменных. Здесь он не будет расширяться.
Например, мне нужно динамически построить функцию, которая складывает n чисел:
let nums = [1,2,3,4]
let len = nums.length
let params = Array(len).fill('x').map((item, idx)=>{
return '' + item + idx
})
const add = new Function(params.join(','), `
return ${params.join('+')};
`)
console.log(add.toString())
console.log(add.apply(null, nums))
строка функции печатиadd.toString()
, вы можете получить:
function anonymous(x0,x1,x2,x3) {
return x0+x1+x2+x3;
}
функцияadd
Входные параметры функции и опыт работы функцииnums
Длина генерируется динамически, так что вы можете контролировать количество входящих параметров в соответствии с реальной ситуацией, а функция обрабатывает только эти параметры.
new Function
По сравнению с первыми двумя, метод объявления функции немного пострадает с точки зрения производительности, и каждое создание экземпляра будет потреблять производительность. Второй,new Function
Объявленная функция не поддерживает "замыкание", сравните следующий код:
function bar(){
let name = 'bar'
let func = function(){return name}
return func
}
bar()() // "bar", func中name读取到bar词法作用域中的name变量
function foo(){
let name = 'foo'
let func = new Function('return name')
return func
}
foo()() // ReferenceError: name is not defined
Причина в том, чтоnew Function
Лексическая область действия относится к глобальной области видимости.
factory.create
Основная логика основана на типе хукаtype
, объединяя строку управления временем обратного вызова следующим образом:
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
})
);
мы начинаем сSyncHook
Например:
let sh = new SyncHook(["name"]);
sh.tap("A", (name) => {
console.log("A");
});
sh.tap('B', (name) => {
console.log("B");
});
sh.tap("C", (name) => {
console.log("C");
});
sh.call();
Вы можете получить следующую строку функции:
function anonymous(name) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(name);
var _fn1 = _x[1];
_fn1(name);
var _fn2 = _x[2];
_fn2(name);
}
в_x
затем указывает наthis.taps
массив, доступ к каждому по порядкуhandler
и выполнитьhandler
.
Для получения дополнительных примеров крючков вы можете просмотретьRunKit
Почему Tapable так «сложно» динамически генерировать тело функции? Поскольку это поклонник «оптимизированной эффективности выполнения», функции, которые не генерируют новые стеки вызовов в максимально возможной степени, являются наиболее эффективными для выполнения.
пользовательский плагин веб-пакета
Плагин для саморазвития
Нормативная вилка должна соответствовать следующим критериям:
- Это именованная функция или класс JS;
- Указан в цепочке прототипов
apply
метод; - Укажите явный обработчик событий и зарегистрируйте обратные вызовы;
- Обрабатывать определенные данные внутри экземпляров веб-пакета (
Compiler
илиCompilation
); - После завершения функции вызовите обратный вызов, переданный веб-пакетом и т. д.;
в条件4、5
Не обязательно, только плагины со сложными функциями будут соответствовать пяти вышеперечисленным условиям одновременно.
в статье"Интерпретация исходного кода Webpack: разъяснение основного процесса компиляции"Мы знаем, что есть два очень важных внутренних объекта webpack,compiler
а такжеcompilation
объект, в обоихhooks
Различные типы хуков определяются заранее, и эти хуки будут запускаться в соответствующие моменты времени в течение всего процесса компиляции. Пользовательский плагин должен «зацепить» этот момент времени и выполнить соответствующую логику.
список хуков компилятора список хуков компиляции
Плагины, которые автоматически загружают ресурсы
После использования webpack для упаковки ресурсов будет создан локальный проект.dist
Папка используется для хранения упакованных статических ресурсов.В настоящее время вы можете написать подключаемый модуль веб-пакета, который автоматически загружает файлы ресурсов в CDN и загружает их в CDN вовремя после успешного завершения каждого пакета.
Когда вы указываете функциональность плагина, вам необходимо зарегистрировать свои обратные вызовы с помощью соответствующих хуков. В этом примере нам нужно загрузить упакованные и выходные статические файлы в CDN с помощьюcompiler钩子列表
Запрос знаюcompiler.hooks.afterEmit
является совместимым крючком, этоAsyncSeriesHook
Типы.
Этот плагин реализован в соответствии с пятью основными условиями:
const assert = require("assert");
const fs = require("fs");
const glob = require("util").promisify(require("glob"));
// 1. 它是一个具名的函数或者JS类
class AssetUploadPlugin {
constructor(options) {
// 这里可以校验传入的参数是否合法等初始化操作
assert(
options,
"check options ..."
);
}
// 2. 在原型链上指定`apply`方法
// apply方法接收 webpack compiler 对象入参
apply(compiler) {
// 3. 指定一个明确的事件钩子并注册回调
compiler.hooks.afterEmit.tapAsync( // 因为afterEmit是AsyncSeriesHook类型的钩子,需要使用tapAsync或tapPromise钩入回调
"AssetUploadPlugin",
(compilation, callback) => {
const {
outputOptions: { path: outputPath }
} = compilation; // 4. 处理 webpack 内部实例的特定数据
uploadDir(
outputPath,
this.options.ignore ? { ignore: this.options.ignore } : null
)
.then(() => {
callback(); // 5. 完成功能后调用webpack传入的回调等;
})
.catch(err => {
callback(err);
});
});
}
};
// uploadDir就是这个插件的功能性描述
function uploadDir(dir, options) {
if (!dir) {
throw new Error("dir is required for uploadDir");
}
if (!fs.existsSync(dir)) {
throw new Error(`dir ${dir} is not exist`);
}
return fs
.statAsync(dir)
.then(stat => {
if (!stat.isDirectory()) {
throw new Error(`dir ${dir} is not directory`);
}
})
.then(() => {
return glob(
"**/*",
Object.assign(
{
cwd: dir,
dot: false,
nodir: true
},
options
)
);
})
.then(files => {
if (!files || !files.length) {
return "未找到需要上传的文件";
}
// TODO: 这里将资源上传至你的静态云服务器中,如京东云、腾讯云等
// ...
});
}
module.exports = AssetUploadPlugin
существуетwebpack.config.js
Этот плагин можно импортировать и создать в:
const AssetUploadPlugin = require('./AssetUploadPlugin')
const config = {
//...
plugins: [
new AssetUploadPlugin({
ignore: []
})
]
}
Суммировать
Гибкая конфигурация webpack выигрывает отTapable
Обеспечьте мощную систему ловушек, чтобы каждый процесс компиляции можно было «зацепить», что еще более мощно. Поскольку так называемые «три человека образуют толпу», когда система является подключаемой, ее масштабируемость будет значительно улучшена.Tapable
Его также можно применять к конкретным бизнес-сценариям, таким как流程监控
,日志记录
,埋点上报
Подождите, всякий раз, когда вам нужно «подключиться» к определенному процессу,Tapable
Он имеет свои сценарии применения.
наконец
Кодовые слова непросты, если:
- Эта статья полезна для вас, пожалуйста, не скупитесь на свои ручонки, чтобы понравиться мне;
- Если есть что-то, что вы не понимаете или ошибаетесь, пожалуйста, прокомментируйте, и я активно отвечу или внесу опечатки;
- Я с нетерпением жду продолжения изучения технических знаний вместе со мной, пожалуйста, следуйте за мной;
- Пожалуйста, укажите источник;
Ваша поддержка и внимание - самая большая мотивация для меня продолжать творить!