Написание пользовательских плагинов для веб-пакетов начинается с понимания Tapable.

внешний интерфейс Webpack

в предыдущей статье"Интерпретация исходного кода 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процесс, оптимизирующий производительность.

Ленивые функции имеют два основных преимущества:

  1. Высокая эффективность: ленивая функция выполняет логику вычислений только при первом запуске, а затем возвращает результат первого выполнения при повторном запуске функции, что экономит много времени выполнения;
  2. Задержка выполнения: в некоторых сценариях необходимо оценить некоторую информацию об окружающей среде, и после ее определения нет необходимости повторно оценивать. можно понимать как嗅探程序. Например, его можно переписать с использованием ленивой загрузки следующим образом.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 так «сложно» динамически генерировать тело функции? Поскольку это поклонник «оптимизированной эффективности выполнения», функции, которые не генерируют новые стеки вызовов в максимально возможной степени, являются наиболее эффективными для выполнения.

пользовательский плагин веб-пакета

Плагин для саморазвития

Нормативная вилка должна соответствовать следующим критериям:

  1. Это именованная функция или класс JS;
  2. Указан в цепочке прототиповapplyметод;
  3. Укажите явный обработчик событий и зарегистрируйте обратные вызовы;
  4. Обрабатывать определенные данные внутри экземпляров веб-пакета (CompilerилиCompilation);
  5. После завершения функции вызовите обратный вызов, переданный веб-пакетом и т. д.;

в条件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Он имеет свои сценарии применения.

наконец

Кодовые слова непросты, если:

  • Эта статья полезна для вас, пожалуйста, не скупитесь на свои ручонки, чтобы понравиться мне;
  • Если есть что-то, что вы не понимаете или ошибаетесь, пожалуйста, прокомментируйте, и я активно отвечу или внесу опечатки;
  • Я с нетерпением жду продолжения изучения технических знаний вместе со мной, пожалуйста, следуйте за мной;
  • Пожалуйста, укажите источник;

Ваша поддержка и внимание - самая большая мотивация для меня продолжать творить!

Ссылаться на