Каждый загрузчик в Webpack выполняется не в изолированной функции, а
внутри специального контекста. Внутри функции загрузчика значение
this содержит набор методов, свойств и служебных утилит,
через которые происходит взаимодействие с системой сборки.
Контекст загрузчика предоставляет:
Без понимания контекста 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
Часто используется для:
Пример получения относительного пути:
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);
});
};
Webpack loader API исторически основан на callback-модели.
Некоторые версии Webpack умеют работать с Promise, однако официальным
и наиболее совместимым способом остаётся this.async().
this.callback()Позволяет вернуть результат вручную.
Сигнатура:
this.callback(error, content, sourceMap, meta);
| Аргумент | Назначение |
|---|---|
error |
ошибка |
content |
новый код |
sourceMap |
карта исходников |
meta |
произвольные метаданные |
module.exports = function(source, map) {
const result = source.replace(/DEBUG/g, '');
this.callback(null, result, map);
};
После вызова callback возвращать значение уже не
нужно.
Если предыдущий загрузчик передал source map, она приходит вторым аргументом.
module.exports = function(source, map) {
console.log(map);
return source;
};
Если загрузчик изменяет код, карту необходимо передавать дальше:
module.exports = function(source, map) {
const transformed = source.replace(/foo/g, 'bar');
this.callback(null, transformed, map);
};
module.exports = function(source) {
console.log(this.sourceMap);
return source;
};
this.cacheable()Webpack кэширует загрузчики автоматически.
Однако можно явно указать поведение.
module.exports = function(source) {
this.cacheable(false);
return source;
};
Это необходимо, если результат зависит от:
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 корректно обработает:
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.fsWebpack предоставляет собственную файловую систему.
В 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;
};
getLogger():
Стрелочные функции нельзя использовать как загрузчики.
Неправильно:
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 загрузчик становится
полноценным участником процесса сборки, а не простой функцией
преобразования текста.