Контекст загрузчика: this и доступные утилиты

Каждый загрузчик в Webpack выполняется не в изолированной функции, а внутри специального контекста. Внутри функции загрузчика значение this содержит набор методов, свойств и служебных утилит, через которые происходит взаимодействие с системой сборки.

Контекст загрузчика предоставляет:

  • доступ к информации о текущем модуле;
  • управление зависимостями;
  • переключение режима синхронного и асинхронного выполнения;
  • получение параметров загрузчика;
  • работу с source map;
  • взаимодействие с файловой системой;
  • передачу метаданных между загрузчиками;
  • управление кэшированием;
  • получение данных о конфигурации Webpack.

Без понимания контекста this невозможно писать полноценные кастомные загрузчики.


Базовая структура загрузчика

Минимальный загрузчик выглядит следующим образом:

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

Webpack вызывает функцию загрузчика и передаёт:

  • содержимое файла;
  • контекст через this.

Контекст становится доступным автоматически:

module.exports = function(source) {
    console.log(this);

    return source;
};

Что содержит this

Контекст загрузчика представляет собой объект LoaderContext.

Внутри него доступны:

Свойство / метод Назначение
this.resourcePath путь к обрабатываемому файлу
this.query параметры загрузчика
this.async() переход в асинхронный режим
this.callback() возврат результата вручную
this.cacheable() управление кэшированием
this.emitFile() создание дополнительных файлов
this.addDependency() добавление зависимости
this.getOptions() получение опций
this.rootContext корень проекта
this.mode режим сборки
this.sourceMap наличие поддержки source map
this.fs доступ к файловой системе
this.loadModule() загрузка другого модуля
this.resolve() резолв пути
this.emitWarning() предупреждение
this.emitError() ошибка

Свойство this.resourcePath

Содержит абсолютный путь к файлу, который обрабатывается загрузчиком.

Пример:

module.exports = function(source) {
    console.log(this.resourcePath);

    return source;
};

Результат:

/Users/project/src/index.js

Свойство активно используется:

  • для анализа расширения файла;
  • генерации имён;
  • условной логики;
  • получения соседних файлов.

Пример:

const path = require('path');

module.exports = function(source) {
    const ext = path.extname(this.resourcePath);

    if (ext === '.txt') {
        return `export default ${JSON.stringify(source)}`;
    }

    return source;
};

Свойство this.rootContext

Содержит корневую директорию проекта.

Пример:

module.exports = function(source) {
    console.log(this.rootContext);

    return source;
};

Результат:

/Users/project

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

  • построения относительных путей;
  • поиска конфигурационных файлов;
  • работы с alias;
  • генерации путей для output.

Пример получения относительного пути:

const path = require('path');

module.exports = function(source) {
    const relative = path.relative(
        this.rootContext,
        this.resourcePath
    );

    console.log(relative);

    return source;
};

Получение параметров через this.getOptions()

Современный способ чтения параметров загрузчика — метод getOptions().

Конфигурация

module: {
    rules: [
        {
            test: /\.txt$/,
            use: {
                loader: './loaders/text-loader.js',
                options: {
                    uppercase: true
                }
            }
        }
    ]
}

Загрузчик

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

    if (options.uppercase) {
        return source.toUpperCase();
    }

    return source;
};

Валидация опций

Для проверки структуры параметров обычно используется пакет schema-utils.

npm install schema-utils

Пример:

const { validate } = require('schema-utils');

const schema = {
    type: 'object',
    properties: {
        uppercase: {
            type: 'boolean'
        }
    }
};

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

    validate(schema, options);

    return source;
};

Асинхронный режим через this.async()

По умолчанию загрузчик синхронный.

Для асинхронной обработки используется:

const callback = this.async();

Метод возвращает функцию завершения.


Пример асинхронного загрузчика

const fs = require('fs');

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

    fs.readFile('data.txt', 'utf-8', (err, data) => {
        if (err) {
            return callback(err);
        }

        const result = source + '\n' + data;

        callback(null, result);
    });
};

Почему нельзя просто вернуть Promise

Webpack loader API исторически основан на callback-модели.

Некоторые версии Webpack умеют работать с Promise, однако официальным и наиболее совместимым способом остаётся this.async().


Метод this.callback()

Позволяет вернуть результат вручную.

Сигнатура:

this.callback(error, content, sourceMap, meta);

Параметры callback

Аргумент Назначение
error ошибка
content новый код
sourceMap карта исходников
meta произвольные метаданные

Пример

module.exports = function(source, map) {
    const result = source.replace(/DEBUG/g, '');

    this.callback(null, result, map);
};

После вызова callback возвращать значение уже не нужно.


Работа с Source Map

Если предыдущий загрузчик передал source map, она приходит вторым аргументом.

module.exports = function(source, map) {
    console.log(map);

    return source;
};

Передача source map дальше

Если загрузчик изменяет код, карту необходимо передавать дальше:

module.exports = function(source, map) {
    const transformed = source.replace(/foo/g, 'bar');

    this.callback(null, transformed, map);
};

Проверка поддержки source map

module.exports = function(source) {
    console.log(this.sourceMap);

    return source;
};

Метод this.cacheable()

Webpack кэширует загрузчики автоматически.

Однако можно явно указать поведение.

Отключение кэширования

module.exports = function(source) {
    this.cacheable(false);

    return source;
};

Это необходимо, если результат зависит от:

  • времени;
  • случайных значений;
  • внешнего API;
  • состояния файлов вне графа зависимостей.

Добавление зависимостей через this.addDependency()

Если загрузчик использует сторонние файлы, Webpack должен знать о них.

Пример:

const fs = require('fs');

module.exports = function(source) {
    const file = '/config/theme.json';

    this.addDependency(file);

    const data = fs.readFileSync(file, 'utf-8');

    return source + data;
};

Теперь при изменении theme.json произойдёт пересборка.


Контекстные зависимости

Для отслеживания целой директории используется:

this.addContextDependency(directory);

Пример:

module.exports = function(source) {
    this.addContextDependency('/templates');

    return source;
};

Webpack начнёт следить за всеми файлами каталога.


Генерация файлов через this.emitFile()

Загрузчик может создавать дополнительные файлы.

Пример:

module.exports = function(source) {
    const content = JSON.stringify({
        generated: true
    });

    this.emitFile(
        'meta.json',
        content
    );

    return source;
};

После сборки файл появится в output-директории.


Метод this.emitWarning()

Позволяет выводить предупреждения.

module.exports = function(source) {
    if (source.includes('eval')) {
        this.emitWarning(
            new Error('Использование eval нежелательно')
        );
    }

    return source;
};

Предупреждение отображается в консоли, но сборка продолжается.


Метод this.emitError()

Регистрирует ошибку сборки.

module.exports = function(source) {
    if (!source.includes('export')) {
        this.emitError(
            new Error('Модуль не содержит export')
        );
    }

    return source;
};

Метод this.resolve()

Позволяет использовать внутренний механизм резолвинга Webpack.


Пример

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

    this.resolve(
        this.context,
        './utils.js',
        (err, result) => {
            if (err) {
                return callback(err);
            }

            console.log(result);

            callback(null, source);
        }
    );
};

Webpack корректно обработает:

  • alias;
  • extensions;
  • modules;
  • symlink;
  • tsconfig paths.

Метод this.loadModule()

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


Пример

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

    this.loadModule(
        './template.js',
        (err, code) => {
            if (err) {
                return callback(err);
            }

            const result = code + source;

            callback(null, result);
        }
    );
};

Доступ к файловой системе через this.fs

Webpack предоставляет собственную файловую систему.

В dev server она может быть виртуальной.

Пример:

module.exports = function(source) {
    this.fs.readFile(
        '/data/config.json',
        'utf-8',
        (err, content) => {
            console.log(content);
        }
    );

    return source;
};

Использование this.fs предпочтительнее прямого fs.


Свойство this.mode

Содержит режим сборки.

module.exports = function(source) {
    console.log(this.mode);

    return source;
};

Возможные значения:

development
production
none

Практическое применение

module.exports = function(source) {
    if (this.mode === 'production') {
        return source.replace(/console\.log\(.*?\);?/g, '');
    }

    return source;
};

Свойство this.target

Позволяет определить целевую платформу.

Пример:

module.exports = function(source) {
    console.log(this.target);

    return source;
};

Возможные значения:

web
node
electron

Передача метаданных между загрузчиками

Четвёртый аргумент callback позволяет передавать служебные данные.


Первый загрузчик

module.exports = function(source) {
    const meta = {
        tokens: ['a', 'b', 'c']
    };

    this.callback(
        null,
        source,
        null,
        meta
    );
};

Второй загрузчик

module.exports = function(source, map, meta) {
    console.log(meta.tokens);

    return source;
};

Свойство this.data

Используется для обмена состоянием между pitch-фазой и основной фазой загрузчика.


Пример

module.exports.pitch = function() {
    this.data.start = Date.now();
};

module.exports = function(source) {
    const end = Date.now();

    console.log(end - this.data.start);

    return source;
};

Метод this.getLogger()

Современный API логирования Webpack.


Пример

module.exports = function(source) {
    const logger = this.getLogger('my-loader');

    logger.info('Начало обработки');

    return source;
};

Преимущества перед console.log

getLogger():

  • интегрируется со статистикой Webpack;
  • поддерживает уровни логирования;
  • может отключаться настройками infrastructureLogging;
  • выводит форматированные сообщения.

Контекст выполнения и стрелочные функции

Стрелочные функции нельзя использовать как загрузчики.

Неправильно:

module.exports = (source) => {
    console.log(this);

    return source;
};

this не будет содержать LoaderContext.


Правильный вариант

module.exports = function(source) {
    console.log(this);

    return source;
};

Полезные свойства контекста

this.context

Каталог текущего файла.

module.exports = function(source) {
    console.log(this.context);

    return source;
};

this.resource

Полный запрос ресурса.

module.exports = function(source) {
    console.log(this.resource);

    return source;
};

Может содержать query-часть:

file.js?raw=true

this.resourceQuery

Содержит query-строку ресурса.

module.exports = function(source) {
    console.log(this.resourceQuery);

    return source;
};

this.resourceFragment

Содержит hash-фрагмент.

Пример:

file.svg#icon

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

const path = require('path');

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

    const options = this.getOptions();

    const logger = this.getLogger('banner-loader');

    const filename = path.basename(
        this.resourcePath
    );

    logger.info(`Обработка ${filename}`);

    const banner =
        `/* File: ${filename} */\n`;

    const result = banner + source;

    if (options.emitMeta) {
        this.emitFile(
            `${filename}.meta.json`,
            JSON.stringify({
                file: filename
            })
        );
    }

    callback(null, result, map);
};

Архитектурная роль LoaderContext

Контекст загрузчика является связующим слоем между:

  • ядром Webpack;
  • системой модулей;
  • графом зависимостей;
  • файловой системой;
  • механизмом инкрементальной сборки;
  • системой кэширования;
  • инфраструктурой source map;
  • цепочкой загрузчиков.

Именно через LoaderContext загрузчик становится полноценным участником процесса сборки, а не простой функцией преобразования текста.