Принцип работы загрузчиков: трансформация исходников

В системе сборки Webpack загрузчики (loaders) представляют собой цепочку преобразований, через которую проходит каждый импортируемый модуль до попадания в финальный бандл. Любой файл в проекте — JavaScript, TypeScript, CSS, изображения, шрифты или даже произвольные форматы — рассматривается как модуль, который может быть преобразован в JavaScript-представление.

Механизм загрузчиков построен вокруг принципа композиционного преобразования: каждый loader получает результат предыдущего и возвращает новое представление модуля. Это позволяет выстраивать сложные цепочки обработки исходного кода без изменения ядра Webpack.


Место загрузчиков в процессе сборки

Перед тем как модуль попадает в граф зависимостей, Webpack проходит несколько стадий обработки:

  1. Определение точки входа
  2. Построение графа зависимостей
  3. Применение загрузчиков к каждому модулю
  4. Преобразование модулей в исполняемый JavaScript
  5. Формирование чанков и финального бандла

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


Принцип цепочки загрузчиков

Для одного файла может быть задано несколько loaders. Они выполняются в строгом порядке:

  • справа налево (в конфигурации use)
  • снизу вверх (в массиве rules, если используется несколько правил)

Пример:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
};

Здесь сначала выполняется css-loader, затем результат передаётся в style-loader.

Логика цепочки:

исходный CSS
   ↓ css-loader
JS-модуль с экспортом стилей
   ↓ style-loader
вставка стилей в DOM

Каждый loader получает входные данные как строку или buffer и возвращает JavaScript-код или модифицированный ресурс.


Формат входных и выходных данных loader

Loader — это функция, которая принимает содержимое модуля:

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

source — строка или Buffer, содержащая содержимое файла.

Возвращаемое значение должно быть:

  • строкой JavaScript-кода
  • либо через this.callback
  • либо асинхронно через this.async

Пример асинхронного loader:

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

  setTimeout(() => {
    const result = source.replace(/foo/g, 'bar');
    callback(null, result);
  }, 100);
};

Контекст выполнения loader

Каждый loader выполняется в специальном контексте this, содержащем важные API:

this.resource

Путь к обрабатываемому файлу.

console.log(this.resource);

this.query

Параметры loader (устаревший способ, сейчас используется getOptions из loader-utils или this.getOptions()).

this.callback

Синхронная или асинхронная передача результата:

this.callback(null, transformedSource, sourceMap);

this.async

Переключение на асинхронный режим:

const callback = this.async();

this.cacheable

Указание, можно ли кешировать результат:

this.cacheable(true);

Pitch-этап загрузчиков

Каждый loader может иметь две фазы:

  • normal phase — основное преобразование
  • pitch phase — предварительный этап до выполнения цепочки

Pitch-функции выполняются слева направо, в обратном порядке относительно основного выполнения.

module.exports.pitch = function(remainingRequest) {
  return "code";
};

Если pitch возвращает значение, цепочка выполнения нормальных loaders прерывается.

Модель выполнения:

pitch loader1 → pitch loader2 → pitch loader3
→ normal loader3 → normal loader2 → normal loader1

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

  • раннего возврата результата
  • оптимизации
  • изменения логики цепочки

Порядок выполнения loaders

Порядок критически важен. Рассмотрим конфигурацию:

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

Фактический порядок:

pitch: a → b → c
normal: c → b → a

Если b-loader в pitch вернёт результат, c-loader и его normal-часть не выполнятся.


Inline loaders и ресурсы

Webpack поддерживает inline-указание loaders прямо в импорте:

import styles from 'css-loader!./styles.css';

Также доступны специальные префиксы:

  • ! — отключает normal loaders из конфигурации
  • !! — отключает все loaders из конфигурации
  • -! — отключает pre-loaders

Пример:

import data from '-!babel-loader!./file.js';

Это позволяет точно управлять цепочкой обработки для конкретного модуля.


Роль source maps в loaders

Загрузчики часто модифицируют код, поэтому важна поддержка source maps.

Loader может возвращать:

this.callback(null, code, map);

Source map позволяет сопоставить итоговый код с оригинальным файлом.

Типичный случай — Babel:

  • вход: ESNext
  • выход: ES5
  • map: соответствие строк и колонок

Без source maps отладка становится практически невозможной.


Асинхронные loaders

Любой loader может работать асинхронно, что важно при:

  • чтении файлов
  • обращении к API
  • генерации кода через сторонние инструменты

Пример:

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

  someAsyncTransform(source, (err, result) => {
    callback(err, result);
  });
};

Webpack ждёт завершения всех async loaders перед продолжением сборки.


Написание собственного loader

Loader — это модуль Node.js, экспортирующий функцию.

Минимальный пример:

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

Расширенный вариант с опциями:

const { getOptions } = require('loader-utils');

module.exports = function(source) {
  const options = getOptions(this);

  const result = source.replace(
    options.from,
    options.to
  );

  return result;
};

Работа с бинарными данными

Loader может обрабатывать не только строки, но и Buffers:

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

Также можно явно указать:

module.exports.raw = true;

Это означает, что вход будет Buffer, а не строка.


Кеширование результатов

Webpack кеширует результаты loader’ов для ускорения повторных сборок. Для корректной работы важно:

  • не использовать недетерминированные значения без необходимости
  • учитывать зависимости через this.addDependency

Пример:

this.addDependency('/path/to/file');

Это заставляет Webpack пересобрать модуль при изменении внешнего файла.


Цепочки преобразований в реальных сценариях

CSS pipeline

style-loader
css-loader
postcss-loader
sass-loader

Фактическое преобразование:

  1. SCSS → CSS (sass-loader)
  2. PostCSS трансформации (postcss-loader)
  3. JS-модуль CSS (css-loader)
  4. вставка в DOM (style-loader)

JavaScript pipeline

babel-loader
ts-loader
eslint-loader (устаревший подход)

Типичный сценарий:

  • TypeScript → JavaScript
  • Babel трансформирует синтаксис
  • дополнительные плагины оптимизируют код

Взаимодействие loaders с модулями Webpack

Loaders не работают изолированно. Они являются частью системы модулей Webpack, где каждый модуль имеет:

  • идентификатор
  • зависимости
  • исходный код
  • transformed code

Loader изменяет только source, не затрагивая dependency graph напрямую.


Ограничения и особенности

  • Loader должен быть детерминированным для корректного кеширования
  • Нельзя выполнять тяжёлые операции без необходимости
  • Порядок имеет критическое значение
  • Pitch может полностью изменить выполнение цепочки
  • Ошибки должны передаваться через callback или throw

Внутренний механизм выполнения

При сборке Webpack выполняет следующие шаги для каждого модуля:

  1. Получение файла
  2. Применение pre-loaders
  3. Pitch-фаза loaders
  4. Основная фаза loaders
  5. Генерация AST (если требуется)
  6. Формирование итогового JS

Каждый этап может быть прерван ошибкой или ранним возвратом результата.


Комбинирование loaders и plugins

Loaders работают на уровне модулей, а plugins — на уровне всей сборки. Часто они дополняют друг друга:

  • loader трансформирует код
  • plugin изменяет поведение сборки

Пример: минификация через loader vs plugin — разные уровни контроля.


Производительность цепочек loaders

На производительность влияют:

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

Оптимизация достигается сокращением количества преобразований и использованием кеша Webpack.


Модель данных loader pipeline

Каждый loader передаёт дальше:

(source, sourceMap, meta)

Эта модель позволяет сохранять контекст трансформаций и строить сложные пайплайны обработки исходников, не теряя информацию о происхождении кода.