⚠️Эта статья является первой подписанной статьей сообщества Nuggets, и её перепечатка без разрешения запрещена.
WebpackЭто инструмент модульной упаковки, который широко используется в большинстве проектов в области переднего плана. использоватьWebpackМы можем упаковать не только файлы JS, но и другие типы файлов ресурсов, такие как изображения, CSS, шрифты и т. д. Функция, которая поддерживает упаковку файлов, отличных от JS, основана наLoaderмеханизм достижения. Итак, чтобы хорошо изучить Webpack, нам нужно освоитьLoaderмеханизм. В этой статье брат Абао подробно расскажет вам о Webpack.LoaderМеханизм, прочитав эту статью, вы будете знать следующее:
- В чем суть Loader?
- Что такое обычные погрузчики и питч-погрузчики?
- Какова роль Pitching Loader?
- Как загружается загрузчик?
- Как работает загрузчик?
- Каков порядок выполнения нескольких загрузчиков?
- Как реализован механизм автоматического выключателя Pitching Loader?
- Как работает функция обычного загрузчика?
- на объекте Загрузчик
raw
Что делают свойства? - Функция погрузчика
this.callback
а такжеthis.async
Откуда взялся метод? - Как обрабатывается окончательный результат возврата Loader?
1. В чем суть Loader?
Как видно из рисунка выше, Loader — это, по сути, модуль JavaScript, который экспортирует функции. Экспортируемая функция, которую можно использовать для преобразования контента, поддерживает следующие 3 параметра:
/**
* @param {string|Buffer} content 源文件的内容
* @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
* @param {any} [meta] meta 数据,可以是任何内容
*/
function webpackLoader(content, map, meta) {
// 你的webpack loader代码
}
module.exports = webpackLoader;
Поняв сигнатуру экспортируемой функции, мы можем определить простуюsimpleLoader
:
function simpleLoader(content, map, meta) {
console.log("我是 SimpleLoader");
return content;
}
module.exports = simpleLoader;
НадsimpleLoader
Он не выполняет никакой обработки входного содержимого, а только выводит соответствующую информацию при выполнении загрузчика. Webpack позволяет пользователям настраивать несколько разных загрузчиков для определенных файлов ресурсов, например, при обработке.css
файл, который мы использовалиstyle-loader
а такжеcss-loader
, конкретная конфигурация выглядит следующим образом:
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};
Преимущество такого дизайна Webpack заключается в том, что каждый загрузчик несет единственную ответственность. В то же время это также удобно для объединения и расширения более поздних загрузчиков. Например, если вы хотите, чтобы Webpack мог обрабатывать файлы Scss, вам просто нужно установитьsass-loader
, а затем при настройке правил обработки файла Scss задайте объект правилаuse
собственность['style-loader', 'css-loader', 'sass-loader']
Вот и все.
2. Что такое обычный погрузчик и питч-погрузчик?
2.1 Normal Loader
Загрузчик — это, по сути, модуль JavaScript, который экспортирует функции, а функции, экспортируемые этим модулем (если модули ES6, функции экспортируются по умолчанию), называются обычными загрузчиками.Следует отметить, что обычный загрузчик, который мы здесь представляем, отличается от загрузчика, определенного в категории загрузчика Webpack.. В Webpack загрузчики можно разделить на 4 категории: pre, post, normal и inline. Среди них пре и пост загрузчик, может проходитьrule
объектenforce
свойства указать:
// webpack.config.js
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.txt$/i,
use: ["a-loader"],
enforce: "post", // post loader
},
{
test: /\.txt$/i,
use: ["b-loader"], // normal loader
},
{
test: /\.txt$/i,
use: ["c-loader"],
enforce: "pre", // pre loader
},
],
},
};
Разобравшись с концепцией Normal Loader, давайте начнем писать Normal Loader. Сначала создадим новый каталог:
$ mkdir webpack-loader-demo
Затем перейдите в этот каталог и используйтеnpm init -y
команда для выполнения операций инициализации. После успешного выполнения команды в текущем каталоге будет создан файл.package.json
документ:
{
"name": "webpack-loader-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Совет: Среда разработки, используемая локально: Node v12.16.2, Npm 6.14.4;
Затем мы используем следующую команду для установкиwebpack
а такжеwebpack-cli
Пакет зависимости:
$ npm i webpack webpack-cli -D
После установки зависимостей проекта мы добавляем соответствующие каталоги и файлы в соответствии со следующей структурой каталогов:
├── dist # 打包输出目录
│ └── index.html
├── loaders # loaders文件夹
│ ├── a-loader.js
│ ├── b-loader.js
│ └── c-loader.js
├── node_modules
├── package-lock.json
├── package.json
├── src # 源码目录
│ ├── data.txt # 数据文件
│ └── index.js # 入口文件
└── webpack.config.js # webpack配置文件
dist/index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webpack Loader 示例</title>
</head>
<body>
<h3>Webpack Loader 示例</h3>
<p id="message"></p>
<script src="./bundle.js"></script>
</body>
</html>
src/index.js
import Data from "./data.txt"
const msgElement = document.querySelector("#message");
msgElement.innerText = Data;
src/data.txt
大家好,我是阿宝哥
loaders/a-loader.js
function aLoader(content, map, meta) {
console.log("开始执行aLoader Normal Loader");
content += "aLoader]";
return `module.exports = '${content}'`;
}
module.exports = aLoader;
существуетaLoader
функция, мы будемcontent
содержимое для изменения, а затем вернутьсяmodule.exports = '${content}'
нить. Так зачем братьcontent
назначить наmodule.exports
характеристики? Мы не будем здесь объяснять конкретные причины, а разберем эту проблему позже.
loaders/b-loader.js
function bLoader(content, map, meta) {
console.log("开始执行bLoader Normal Loader");
return content + "bLoader->";
}
module.exports = bLoader;
loaders/c-loader.js
function cLoader(content, map, meta) {
console.log("开始执行cLoader Normal Loader");
return content + "[cLoader->";
}
module.exports = cLoader;
существуетloadersВ каталоге мы определяем выше 3Normal Loader. Реализация этих загрузчиков относительно проста, только когда загрузчик выполняетcontent
Добавьте в параметр соответствующую информацию о текущем загрузчике. Чтобы Webpack распозналloadersДля пользовательского загрузчика в каталоге нам также нужно установить его в файле конфигурации Webpack.resolveLoader
свойства, конкретная конфигурация выглядит следующим образом:
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
mode: "development",
module: {
rules: [
{
test: /\.txt$/i,
use: ["a-loader", "b-loader", "c-loader"],
},
],
},
resolveLoader: {
modules: [
path.resolve(__dirname, "node_modules"),
path.resolve(__dirname, "loaders"),
],
},
};
Когда обновление каталога будет завершено, вwebpack-loader-demoЗапуск в корневом каталоге проектаnpx webpack
Команда может начать упаковку. Ниже приводится операция Baogenpx webpack
После команды вывод консоли:
开始执行cLoader Normal Loader
开始执行bLoader Normal Loader
开始执行aLoader Normal Loader
asset bundle.js 4.55 KiB [emitted] (name: main)
runtime modules 937 bytes 4 modules
cacheable modules 187 bytes
./src/index.js 114 bytes [built] [code generated]
./src/data.txt 73 bytes [built] [code generated]
webpack 5.45.1 compiled successfully in 99 ms
Наблюдая за приведенными выше результатами вывода, мы можем знать, что порядок выполнения Normal Loader справа налево. Так же, когда упаковка готова, открываем ее в браузереdist/index.htmlфайла, на странице вы увидите следующую информацию:
Webpack Loader 示例
大家好,我是阿宝哥[cLoader->bLoader->aLoader]
Из выводимой информации на странице"Всем привет, меня зовут Баогэ [cLoader->bLoader->aLoader]"Видно, что загрузчик обрабатывает данные в виде конвейеров в процессе выполнения Конкретный процесс обработки показан на следующем рисунке:
Теперь, когда вы знаете, что такое обычный загрузчик и порядок выполнения обычного загрузчика, давайте представим еще один загрузчик —Pitching Loader.
2.2 Pitching Loader
При разработке загрузчика мы можем добавить функцию к экспортируемой функцииpitch
Свойство, значение которого также является функцией. Функция называетсяPitching Loader, который поддерживает 3 параметра:
/**
* @remainingRequest 剩余请求
* @precedingRequest 前置请求
* @data 数据对象
*/
function (remainingRequest, precedingRequest, data) {
// some code
};
вdata
Параметры, которые можно использовать для передачи данных. то естьpitch
функционировать, чтобыdata
добавить данные к объекту, а затемnormal
через функциюthis.data
способ чтения добавленных данных. а такжеremainingRequest
а такжеprecedingRequest
Какие именно параметры? Здесь мы сначала обновляемa-loader.js
документ:
function aLoader(content, map, meta) {
// 省略部分代码
}
aLoader.pitch = function (remainingRequest, precedingRequest, data) {
console.log("开始执行aLoader Pitching Loader");
console.log(remainingRequest, precedingRequest, data)
};
module.exports = aLoader;
В приведенном выше коде мы добавляем функцию загрузчика вpitch
свойство и установите его значение в объект функции. В теле функции мы выводим аргументы, которые получила функция. Далее обновляем таким же образомb-loader.js
а такжеc-loader.js
документ:
b-loader.js
function bLoader(content, map, meta) {
// 省略部分代码
}
bLoader.pitch = function (remainingRequest, precedingRequest, data) {
console.log("开始执行bLoader Pitching Loader");
console.log(remainingRequest, precedingRequest, data);
};
module.exports = bLoader;
c-loader.js
function cLoader(content, map, meta) {
// 省略部分代码
}
cLoader.pitch = function (remainingRequest, precedingRequest, data) {
console.log("开始执行cLoader Pitching Loader");
console.log(remainingRequest, precedingRequest, data);
};
module.exports = cLoader;
Когда все файлы обновлены, мыwebpack-loader-demoКорневой каталог проекта снова выполняетсяnpx webpack
После команды будет выведена соответствующая информация. Здесь мы беремb-loader.js
изpitch
Возьмите вывод функции в качестве примера для анализаremainingRequest
а такжеprecedingRequest
Вывод параметров:
/Users/fer/webpack-loader-demo/loaders/c-loader.js!/Users/fer/webpack-loader-demo/src/data.txt #剩余请求
/Users/fer/webpack-loader-demo/loaders/a-loader.js #前置请求
{} #空的数据对象
В дополнение к приведенной выше выходной информации мы также можем ясно видетьPitching Loaderа такжеNormal LoaderЗаказ исполнения:
开始执行aLoader Pitching Loader
...
开始执行bLoader Pitching Loader
...
开始执行cLoader Pitching Loader
...
开始执行cLoader Normal Loader
开始执行bLoader Normal Loader
开始执行aLoader Normal Loader
Очевидно, для нашего примераPitching LoaderПорядок выполнения такойслева направо,а такжеNormal LoaderПорядок выполнения такойсправа налево. Конкретный процесс выполнения показан на следующем рисунке:
Совет: внутри Webpack используетсяloader-runnerЭта библиотека для запуска настроенных загрузчиков.
Увидев здесь некоторых друзей, могут возникнуть вопросы,Pitching LoaderПомимо возможности опережать время, что еще он делает? Фактически, когдаPitching Loaderвернуть неundefined
Когда значение установлено, будет достигнут эффект предохранителя. Здесь мы обновляемbLoader.pitch
метод, пусть он вернется"bLoader Pitching Loader->"
Нить:
bLoader.pitch = function (remainingRequest, precedingRequest, data) {
console.log("开始执行bLoader Pitching Loader");
return "bLoader Pitching Loader->";
};
при обновленииbLoader.pitch
метод, мы выполняем сноваnpx webpack
После команды консоль выводит следующее:
开始执行aLoader Pitching Loader
开始执行bLoader Pitching Loader
开始执行aLoader Normal Loader
asset bundle.js 4.53 KiB [compared for emit] (name: main)
runtime modules 937 bytes 4 modules
...
Из приведенных выше результатов вывода видно, что когдаbLoader.pitch
метод возвращает неundefined
Значение, пропустите остальную часть погрузчика. Конкретный процесс реализации, как показано ниже:
Совет: внутри Webpack используетсяloader-runnerЭта библиотека для запуска настроенных загрузчиков.
После этого снова открываем в браузереdist/index.htmlдокумент. В этот момент на странице вы увидите следующую информацию:
Webpack Loader 示例
bLoader Pitching Loader->aLoader]
После ознакомления с соответствующими знаниями об обычном загрузчике и питчевом загрузчике давайте проанализируем, как запускается загрузчик.
3. Как работает загрузчик?
Чтобы узнать, как запускается Loader, мы можем использовать инструмент отладки точек останова, чтобы узнать текущую запись Loader. Здесь мы используем знакомоеVisual Studio CodeНапример, чтобы представить, как настроить среду отладки точки останова:
Когда вы выполните вышеуказанные шаги, в рамках текущего проекта (webpack-loader-demo) он будет создан автоматически..vscodeкаталог и автоматически создатьlaunch.jsonдокумент. Затем мы копируем следующее и заменяем его напрямуюlaunch.jsonоригинальный контент в формате .
{
"version": "0.2.0",
"configurations": [{
"type": "node",
"request": "launch",
"name": "Webpack Debug",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "debug"],
"port": 5858
}]
}
Используя приведенную выше информацию о конфигурации, мы создалиWebpack DebugОтладка задач. Когда задача работает, она будет выполнена в текущем рабочем каталогеnpm run debug
Заказ. Итак, тогда нам нужноpackage.jsonфайл добавленdebugКоманда, конкретное содержание выглядит следующим образом:
// package.json
{
"scripts": {
"debug": "node --inspect=5858 ./node_modules/.bin/webpack"
},
}
После вышеперечисленных приготовлений можноa-loaderизpitch
Добавьте точку останова в функцию. Соответствующий стек вызовов выглядит следующим образом:
Мы можем увидеть вызов, наблюдая за приведенной выше информацией о стеке вызовов.runLoaders
Метод, который получен изloader-runnerмодуль. Итак, чтобы выяснить, как запускается Loader, нам нужно проанализироватьrunLoaders
метод. Приступим к анализу используемых в проектеloader-runnerмодуль, его версия4.2.0. вrunLoaders
метод определен вlib/LoaderRunner.js
В файле:
// loader-runner/lib/LoaderRunner.js
exports.runLoaders = function runLoaders(options, callback) {
// read options
var resource = options.resource || "";
var loaders = options.loaders || [];
var loaderContext = options.context || {}; // Loader上下文对象
var processResource = options.processResource ||
((readResource, context, resource, callback) => {
context.addDependency(resource);
readResource(resource, callback);
}).bind(null, options.readResource || readFile);
// prepare loader objects
loaders = loaders.map(createLoaderObject);
loaderContext.context = contextDirectory;
loaderContext.loaderIndex = 0;
loaderContext.loaders = loaders;
// 省略大部分代码
var processOptions = {
resourceBuffer: null,
processResource: processResource
};
// 迭代PitchingLoaders
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
// ...
});
};
Как видно из приведенного выше кода, вrunLoaders
функция, она начнется сoptions
Получить на объект конфигурацииloaders
информацию, а затем позвонитеcreateLoaderObject
Функция создает объект Loader и при вызове метода возвращаетnormal
,pitch
,raw
а такжеdata
объект со свойствами. В настоящее время большинство значений свойств этого объектаnull
, в последующем потоке обработки соответствующее значение атрибута будет заполнено.
// loader-runner/lib/LoaderRunner.js
function createLoaderObject(loader) {
var obj = {
path: null,
query: null,
fragment: null,
options: null,
ident: null,
normal: null,
pitch: null,
raw: null,
data: null,
pitchExecuted: false,
normalExecuted: false
};
// 省略部分代码
obj.request = loader;
if(Object.preventExtensions) {
Object.preventExtensions(obj);
}
return obj;
}
После создания объекта Loader и инициализации объекта loaderContext он вызываетсяiteratePitchingLoaders
Функция начинает итерацию Pitching Loader. Чтобы дать вам общее представление о последующем потоке обработки, прежде чем рассматривать конкретный код, давайте рассмотрим предыдущую операцию.txt loadersСтек вызовов:
СоответствующийrunLoaders
функциональныйoptions
Структура объекта выглядит следующим образом:
Основываясь на приведенном выше стеке вызовов и связанном с ним исходном коде, брат Абао также нарисовал соответствующую блок-схему:
Прочитав приведенную выше блок-схему и диаграмму стека вызовов, давайте проанализируем основной код связанных функций на блок-схеме. Здесь мы сначала анализируемiteratePitchingLoaders
:
// loader-runner/lib/LoaderRunner.js
function iteratePitchingLoaders(options, loaderContext, callback) {
// abort after last loader
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
// 在processResource函数内,会调用iterateNormalLoaders函数
// 开始执行normal loader
return processResource(options, loaderContext, callback);
// 首次执行时,loaderContext.loaderIndex的值为0
var currentLoaderObject =
loaderContext.loaders[loaderContext.loaderIndex];
// 如果当前loader对象的pitch函数已经被执行过了,则执行下一个loader的pitch函数
if(currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 加载loader模块
loadLoader(currentLoaderObject, function(err) {
if(err) {
loaderContext.cacheable(false);
return callback(err);
}
// 获取当前loader对象上的pitch函数
var fn = currentLoaderObject.pitch;
// 标识loader对象已经被iteratePitchingLoaders函数处理过
currentLoaderObject.pitchExecuted = true;
if(!fn) return iteratePitchingLoaders(options, loaderContext,
callback);
// 开始执行pitch函数
runSyncOrAsync(fn,loaderContext, ...);
// 省略部分代码
});
}
существуетiteratePitchingLoaders
Внутри функции она начнет обработку с самого левого объекта-загрузчика, а затем вызоветloadLoader
Функция начинает загрузку модуля загрузчика. существуетloadLoader
Внутренние функции, основанные наloader
Тип, используйте другой метод загрузки. Для нашего текущего проекта он пройдетrequire(loader.path)
способ загрузки модуля загрузчика. Конкретный код выглядит следующим образом:
// loader-runner/lib/loadLoader.js
module.exports = function loadLoader(loader, callback) {
if(loader.type === "module") {
try {
if(url === undefined) url = require("url");
var loaderUrl = url.pathToFileURL(loader.path);
var modulePromise = eval("import(" +
JSON.stringify(loaderUrl.toString()) + ")");
modulePromise.then(function(module) {
handleResult(loader, module, callback);
}, callback);
return;
} catch(e) {
callback(e);
}
} else {
try {
var module = require(loader.path);
} catch(e) {
// 省略相关代码
}
// 处理已加载的模块
return handleResult(loader, module, callback);
}
};
Независимо от того, какой метод загрузки используется, после успешной загрузкиloader
После модуля он вызоветhandleResult
функция для обработки загруженных модулей. Функция этой функции состоит в том, чтобы получить экспортированную функцию в модуле иpitch
а такжеraw
Значение атрибута и присваивает соответствующийloader
Соответствующие свойства объекта:
// loader-runner/lib/loadLoader.js
function handleResult(loader, module, callback) {
if(typeof module !== "function" && typeof module !== "object") {
return callback(new LoaderLoadingError(
"Module '" + loader.path + "' is not a loader (export function or es6 module)"
));
}
loader.normal = typeof module === "function" ? module : module.default;
loader.pitch = module.pitch;
loader.raw = module.raw;
if(typeof loader.normal !== "function" && typeof loader.pitch !== "function") {
return callback(new LoaderLoadingError(
"Module '" + loader.path + "' is not a loader (must have normal or pitch function)"
));
}
callback();
}
После обработки загруженногоloader
После модуля он будет продолжать вызывать входящиеcallback
Перезвоните. В этой функции обратного вызова текущийloader
попасть на объектpitch
функция, затем вызовитеrunSyncOrAsync
функция для выполненияpitch
функция. Для нашего проекта начнется выполнениеaLoader.pitch
функция.
Увидев здесь друзей, вы уже должны знать, как загружается модуль загрузчика и как запускается функция тона, определенная в модуле загрузчика. Из-за ограниченного места брат Абао не будет подробно рассказывать об этом.loader-runnerдругие функции модуля. Далее мы продолжим анализ с несколькими вопросамиloader-runnerФункционал, предоставляемый модулем.
4. Как реализован механизм взрывателя Pitching Loader?
// loader-runner/lib/LoaderRunner.js
function iteratePitchingLoaders(options, loaderContext, callback) {
// 省略部分代码
loadLoader(currentLoaderObject, function(err) {
var fn = currentLoaderObject.pitch;
// 标识当前loader已经被处理过
currentLoaderObject.pitchExecuted = true;
// 若当前loader对象上未定义pitch函数,则处理下一个loader对象
if(!fn) return iteratePitchingLoaders(options, loaderContext,
callback);
// 执行loader模块中定义的pitch函数
runSyncOrAsync(
fn, loaderContext, [loaderContext.remainingRequest,
loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
var hasArg = args.some(function(value) {
return value !== undefined;
});
if(hasArg) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
В приведенном выше кодеrunSyncOrAsync
Внутри функции обратного вызова функции она будет основываться на текущейloader
объектpitch
Является ли возвращаемое значение функцииundefined
для выполнения различной логики обработки. еслиpitch
Функция вернула не-undefined
значение, произойдет перегорание. То есть пропустить последующий процесс выполнения и начать выполнение предыдущегоloader
на объектеnormal loaderфункция. Конкретная реализация также очень проста, т.loaderIndex
уменьшите значение 1, затем вызовитеiterateNormalLoaders
функцию для реализации. и еслиpitch
возврат функцииundefined
, затем продолжайте звонитьiteratePitchingLoaders
функция для обработки следующего необработанногоloader
объект.
5. Как работает функция обычного загрузчика?
// loader-runner/lib/LoaderRunner.js
function iterateNormalLoaders(options, loaderContext, args, callback) {
if(loaderContext.loaderIndex < 0)
return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// normal loader的执行顺序是从右到左
if(currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
// 获取当前loader对象上的normal函数
var fn = currentLoaderObject.normal;
// 标识loader对象已经被iterateNormalLoaders函数处理过
currentLoaderObject.normalExecuted = true;
if(!fn) { // 当前loader对象未定义normal函数,则继续处理前一个loader对象
return iterateNormalLoaders(options, loaderContext, args, callback);
}
convertArgs(args, currentLoaderObject.raw);
runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
Как видно из приведенного выше кода, вloader-runnerВнутри модуля он будет вызыватьсяiterateNormalLoaders
функция для выполнения загруженногоloader
на объектеnormal loaderфункция. а такжеiteratePitchingLoaders
функция, вiterateNormalLoaders
Внутренняя функция также называетсяrunSyncOrAsync
функция для выполненияfn
функция. Но звонюnormal loaderПеред функцией он сначала вызоветconvertArgs
Функция обрабатывает аргументы.
convertArgs
функция будет основываться наraw
атрибут для обработки args[0] (содержимое файла), конкретная реализация этой функции выглядит следующим образом:
// loader-runner/lib/LoaderRunner.js
function convertArgs(args, raw) {
if(!raw && Buffer.isBuffer(args[0]))
args[0] = utf8BufferToString(args[0]);
else if(raw && typeof args[0] === "string")
args[0] = Buffer.from(args[0], "utf-8");
}
// 把buffer对象转换为utf-8格式的字符串
function utf8BufferToString(buf) {
var str = buf.toString("utf-8");
if(str.charCodeAt(0) === 0xFEFF) {
return str.substr(1);
} else {
return str;
}
}
верить после прочтенияconvertArgs
После соответствующего кода функции у вас естьraw
Более глубокое понимание роли атрибутов.
6. Откуда берутся методы this.callback и this.async в теле функции Loader?
Загрузчик можно разделить на синхронный загрузчик и асинхронный загрузчик, для синхронного загрузчика мы можем передатьreturn
заявление илиthis.callback
способ вернуть преобразованный результат синхронно. просто по сравнению сreturn
утверждение,this.callback
Этот метод является более гибким, поскольку позволяет передавать несколько параметров.
sync-loader.js
module.exports = function(source) {
return source + "-simple";
};
sync-loader-with-multiple-results.js
module.exports = function (source, map, meta) {
this.callback(null, source + "-simple", map, meta);
return; // 当调用 callback() 函数时,总是返回 undefined
};
должен быть в курсеthis.callback
Метод поддерживает 4 параметра, и конкретная функция каждого параметра выглядит следующим образом:
this.callback(
err: Error | null, // 错误信息
content: string | Buffer, // content信息
sourceMap?: SourceMap, // sourceMap
meta?: any // 会被 webpack 忽略,可以是任何东西
);
А для асинхронных загрузчиков нам нужно вызватьthis.async
способ получитьcallback
функция:
async-loader.js
module.exports = function(source) {
var callback = this.async();
setTimeout(function() {
callback(null, source + "-async-simple");
}, 50);
};
Тогда в приведенном выше примереthis.callback
а такжеthis.async
Откуда взялся метод? С этим вопросом мы исходим изloader-runnerВ исходном коде модуля посмотрите.
this.async
// loader-runner/lib/LoaderRunner.js
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true; // 默认是同步类型
var isDone = false; // 是否已完成
var isError = false; // internal error
var reportedError = false;
context.async = function async() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
}
Мы уже представилиrunSyncOrAsync
Роль функции, которая используется для выполнения настроек, установленных в модуле LoaderNormal LoaderилиPitching Loaderфункция. существуетrunSyncOrAsync
внутри функции, которая в итоге пройдетfn.apply(context, args)
способ вызова функции Loader. пройдешьapply
Метод устанавливает контекст выполнения функции Loader.
Кроме того, из приведенного выше кода видно, что при вызовеthis.async
После метода он сначала установитisSync
ценностьfalse
, затем вернутьсяinnerCallback
функция. На самом деле функция иthis.callback
Все указывают на одну и ту же функцию.
this.callback
// loader-runner/lib/LoaderRunner.js
function runSyncOrAsync(fn, context, args, callback) {
// 省略部分代码
var innerCallback = context.callback = function() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("callback(): The callback was already called.");
}
isDone = true;
isSync = false;
try {
callback.apply(null, arguments);
} catch(e) {
isError = true;
throw e;
}
};
}
Если в функции Loader передаетсяreturn
оператор для возврата результата обработки, затемisSync
значение по-прежнемуtrue
, будет выполнена следующая соответствующая логика обработки:
// loader-runner/lib/LoaderRunner.js
function runSyncOrAsync(fn, context, args, callback) {
// 省略部分代码
try {
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
if(isSync) { // 使用return语句返回处理结果
isDone = true;
if(result === undefined)
return callback();
if(result && typeof result === "object"
&& typeof result.then === "function") {
return result.then(function(r) {
callback(null, r);
}, callback);
}
return callback(null, result);
}
} catch(e) {
// 省略异常处理代码
}
}
Наблюдая за приведенным выше кодом, мы можем знать, что в функции загрузчика мы можем использоватьreturn
оператор возвращает напрямуюPromise
объект, например, таким образом:
module.exports = function(source) {
return Promise.resolve(source + "-promise-simple");
};
Теперь, когда мы знаем, как загрузчик возвращает данные, как обрабатывается окончательный результат, возвращаемый загрузчиком? Кратко представим его ниже.
7. Как обрабатывается окончательный результат возврата Loader?
// webpack/lib/NormalModule.js(Webpack 版本:5.45.1)
build(options, compilation, resolver, fs, callback) {
// 省略部分代码
return this.doBuild(options, compilation, resolver, fs, err => {
// if we have an error mark module as failed and exit
if (err) {
this.markModuleAsErrored(err);
this._initBuildHash(compilation);
return callback();
}
// 省略部分代码
let result;
try {
result = this.parser.parse(this._ast || this._source.source(), {
current: this,
module: this,
compilation: compilation,
options: options
});
} catch (e) {
handleParseError(e);
return;
}
handleParseResult(result);
});
}
Как видно из приведенного выше кода, вthis.doBuild
В функции обратного вызова метода используйтеJavascriptParser
Синтаксический анализатор анализирует возвращенное содержимое, и нижний слойacornЭта сторонняя библиотека реализует синтаксический анализ кода JavaScript. И результат после разбора будет продолжать называтьhandleParseResult
функцию для дальнейшей обработки. Брат Абао не будет представлять его здесь Заинтересованные партнеры могут самостоятельно ознакомиться с соответствующим исходным кодом.
8. Зачем назначать содержимое свойству module.exports?
Наконец, давайте ответим на вопрос, оставленный ранее - вa-loader.jsмодуль, зачем ставитьcontent
назначить наmodule.exports
характеристики? Чтобы ответить на этот вопрос, мы создадимbundle.jsОтвет на этот вопрос находится в файле (с удаленными комментариями):
__webpack_modules__
var __webpack_modules__ = ({
"./src/data.txt": ((module)=>{
eval("module.exports = '大家好,我是阿宝哥[cLoader->bLoader->aLoader]'\n\n//#
sourceURL=webpack://webpack-loader-demo/./src/data.txt?");
}),
"./src/index.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var
_data_txt__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./data.txt */ \"./src/data.txt\");...
);
})
});
__webpack_require__
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
в сгенерированномbundle.jsфайл,./src/index.js
Внутри соответствующей функции он будет вызываться вызовом__webpack_require__
функция для импорта./src/data.txt
содержимое в пути. пока в__webpack_require__
Внутри функции он будет предпочтительно получен из объекта кеша.moduleId
Соответствующий модуль, если модуль уже существует, он вернется к объекту модуляexports
Стоимость имущества. Если кэшированный объект не существуетmoduleId
Соответствующий модуль создаст содержащийexports
атрибутmodule
объект, то будет основываться наmoduleId
от__webpack_modules__
В объекте получить соответствующую функцию и вызвать ее с соответствующими параметрами и, наконец, вернутьmodule.exports
ценность . так вa-loader.jsФайл,content
назначить наmodule.exports
Назначение атрибутов — экспортировать соответствующий контент.
9. Резюме
Эта статья знакомит с сущностью Webpack Loader, определением и использованием Normal Loader и Pitching Loader, а также с тем, как запускается Loader.Я надеюсь, что после прочтения этой статьи у вас будет более глубокое понимание механизма Webpack Loader. В статье Брат Абао только представилloader-runnerмодуль, по сутиloader-utils(библиотека инструментов загрузчика) иschema-utils(Библиотека проверки параметров загрузчика) Эти два модуля также тесно связаны с загрузчиком. Вероятно, вы будете использовать их при написании загрузчиков. Если вам интересно, как написать Loader, вы можете прочитатьwriting-a-loaderв этом документе или самородкахНаучите вас, как свернуть загрузчик WebpackЭта статья.