Загрузчик с несколькими возвращаемыми значениями (pitch)

В системе загрузчиков Webpack каждый loader может содержать не только основную функцию преобразования модуля, но и дополнительную стадию выполнения — pitch-фазу. Она реализуется через метод pitch, который позволяет перехватывать процесс загрузки до того, как будет вызвана основная функция цепочки загрузчиков.

Pitch-фаза используется для управления порядком выполнения, оптимизации цепочки loader’ов и даже полного пропуска последующих этапов обработки.


Модель выполнения loader-цепочки

Webpack обрабатывает загрузчики в два этапа:

  1. Pitch-фаза — обход цепочки слева направо
  2. Normal-фаза — выполнение loader’ов справа налево

Для конфигурации:

module: {
  rules: [
    {
      test: /\.txt$/,
      use: ['loader-a', 'loader-b', 'loader-c']
    }
  ]
}

Фактический порядок выполнения выглядит так:

  • Pitch: loader-a → loader-b → loader-c
  • Normal: loader-c → loader-b → loader-a

Сигнатура pitch-функции

Каждый loader может экспортировать pitch:

module.exports = function (source) {
  return source;
};

module.exports.pitch = function (
  remainingRequest,
  precedingRequest,
  data
) {
  // логика pitch
};

Параметры:

  • remainingRequest — строка с оставшимися loader’ами и модулем
  • precedingRequest — уже пройденные loader’ы
  • data — общий объект для передачи данных между pitch и normal фазой

Поведение при возврате значения из pitch

Ключевая особенность pitch-фазы — возможность прервать дальнейшее выполнение цепочки.

Если pitch возвращает значение, Webpack:

  • прекращает выполнение остальных pitch-функций
  • пропускает normal-фазу для последующих loader’ов
  • начинает выполнение normal-фазы с текущего loader’а

Пример:

module.exports.pitch = function () {
  return 'result-from-pitch';
};

В этом случае normal-фаза всех последующих loader’ов не будет вызвана.


Механизм “короткого замыкания”

Pitch-фаза работает как система раннего выхода.

Рассмотрим цепочку:

use: ['a', 'b', 'c']

Если loader b возвращает значение в pitch, происходит следующее:

  • a.pitch выполняется
  • b.pitch выполняется и возвращает результат
  • c.pitch не вызывается
  • normal-фаза запускается с b, затем a

Loader c полностью исключается из процесса.


Пример управления потоком выполнения

module.exports.pitch = function (remainingRequest) {
  if (remainingRequest.includes('special')) {
    return `export default "optimized path"`;
  }
};

Здесь loader может заменить всю цепочку обработки, если выполняется определённое условие.


Использование remainingRequest

remainingRequest содержит строку вида:

/path/to/loader-b.js!/path/to/loader-c.js!/src/file.txt

Это позволяет:

  • анализировать оставшуюся цепочку loader’ов
  • принимать решения о пропуске обработки
  • формировать альтернативный результат

Пример:

module.exports.pitch = function (remainingRequest) {
  const request = remainingRequest.split('!').pop();

  if (request.endsWith('.txt')) {
    return `module.exports = ${JSON.stringify(request)}`;
  }
};

precedingRequest и контекст цепочки

precedingRequest содержит уже обработанную часть цепочки:

/path/to/loader-a.js!

Это используется для:

  • диагностики цепочки
  • условного поведения loader’а
  • кэширования результатов на основе контекста

data-объект как механизм передачи состояния

Webpack предоставляет общий объект data, который сохраняется между pitch и normal фазой одного loader’а.

Пример использования:

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.startTime = Date.now();
};

module.exports = function (source) {
  const duration = Date.now() - this.data.startTime;
  return source + `\n// processed in ${duration}ms`;
};

Особенности:

  • данные доступны только внутри одного loader’а
  • сохраняются между фазами pitch и normal
  • не передаются другим loader’ам

Пропуск normal-фазы

Если pitch возвращает undefined, выполнение продолжается стандартным образом:

  • все pitch-функции выполняются
  • normal-фаза запускается в обратном порядке

Если pitch возвращает строку или Buffer:

  • normal-фаза для оставшихся loader’ов пропускается
  • начинается выполнение с текущего loader’а

Взаимодействие pitch и async loader’ов

Pitch-фаза может быть синхронной или асинхронной:

module.exports.pitch = async function () {
  const data = await fetchSomething();
  return `export default ${JSON.stringify(data)}`;
};

Однако в классическом Webpack loader API предпочтение отдаётся callback-стилю:

module.exports.pitch = function () {
  const callback = this.async();

  setTimeout(() => {
    callback(null, 'export default "async pitch"');
  }, 100);
};

Практическое применение pitch-фазы

Pitch используется в сценариях:

  • кеширование результатов до основной обработки
  • подмена всей цепочки loader’ов
  • оптимизация обработки файлов
  • условное отключение последующих loader’ов
  • генерация модулей без чтения исходного файла

Пример: loader с кешированием через pitch

const cache = new Map();

module.exports.pitch = function (remainingRequest) {
  if (cache.has(remainingRequest)) {
    return cache.get(remainingRequest);
  }
};

module.exports = function (source) {
  const result = source.toUpperCase();
  cache.set(this.resourcePath, result);
  return result;
};

Особенности порядка выполнения

Важно учитывать:

  • pitch выполняется слева направо
  • normal выполняется справа налево
  • return в pitch меняет весь поток выполнения
  • data не разделяется между loader’ами
  • remainingRequest содержит полную цепочку оставшихся loader’ов

Типичные ошибки при работе с pitch

Часто встречающиеся проблемы:

  • возврат значения без понимания остановки цепочки
  • попытка использовать data между разными loader’ами
  • неправильная интерпретация remainingRequest
  • создание побочных эффектов в pitch без учета пропуска normal-фазы

Влияние pitch на архитектуру loader’ов

Pitch-фаза фактически превращает loader в управляемый фильтр цепочки, позволяя:

  • реализовывать условные пайплайны
  • динамически менять структуру обработки
  • строить оптимизированные пути трансформации модулей

Это делает систему loader’ов не просто последовательностью функций, а управляемым графом исполнения с возможностью раннего завершения и ветвления логики.