Зачем нужен погрузчик
webpack — это сборщик статических модулей для современных приложений JavaScript. Внутри управление зависимостями между модулями осуществляется путем построения графа зависимостей, и создается один или несколько связанных статических ресурсов.
Но webpack может обрабатывать только модули JavaScript и Json. В дополнение к модулям JavaScript и Json приложение также включает модули кода, отличные от js, такие как медиа-ресурсы, такие как изображения, аудио и шрифты, а также файлы стилей, такие как less и sass. Поэтому нам нужна возможность разбирать модули ресурсов, отличные от js, на модули, которыми можно управлять с помощью webpack. Это то, что делает загрузчик.
Например, для файлов меньшего стиля, если файл index.less обрабатывается в файле конфигурации веб-пакета, он будет обрабатываться меньшим загрузчиком, css-загрузчиком и загрузчиком стиля, как показано в следующем коде:
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
}
};
Когда webpack разбирает модуль index.less, он сначала использует что-то похожее на fs.readFile для чтения файла и получения в файле исходного текста, полученный исходник нужно преобразовать в ast через js-парсер, но в это, я пойду к загрузчику, настроенному webpack, чтобы увидеть, есть ли загрузчик, который обрабатывает файл, и обнаружил, что есть три загрузчика ['style-loader', 'css-loader', 'less-loader'], которые обрабатываются по порядку, поэтому webpack передаст исходный код и процессор загрузчика библиотеке обработки загрузчика loader-runner, которая будет обрабатывать исходные файлы через уровни загрузчиков по определенным правилам и, наконец, получит модули, которые webpack может распознать; затем преобразовать его в ast для обработки Дальнейшая обработка, такая как анализ ast, собирает зависимости модуля до тех пор, пока не будет завершен анализ ссылки на зависимость.
На этом этапе вы должны знать, что исходный файл index.less будет обработан тремя загрузчиками по определенным правилам для получения js-модуля. Что сделали три загрузчика, чтобы можно было конвертировать файлы стилей в файлы js?
Во-первых, исходник будет обработан less-loader в качестве входного параметра, и less-loader может преобразовать меньше кода в код css через генератор меньшего парсинга. Конечно, преобразованный код css нельзя использовать напрямую, потому что в css будут зависимости импорта в других файлах css.
Код css, разобранный less-loader, передать в css-loader.В css-loader для разбора css будет использоваться парсер css, то есть postcss будет парсить css.Например, import будет парситься в виде require в js чтобы ссылаться на другие ресурсы стиля, в то же время он преобразует код css в строку и прокидывает его через module.exports, На данный момент файл css преобразован в модуль js, и веб-пакет может с этим справиться. Но это пока не работает, потому что на него не ссылаются как на тег стиля. Поэтому его нужно обрабатывать с помощью style-loader.
Передайте код js, проанализированный css-loader, в style-loader, обработайте требуемый путь с помощью функции преобразования пути в loader-utils, добавьте и создайте теги стиля и назначьте код, на который ссылается require, в innerHtml, чтобы вы получили раздел код js. Код содержит содержимое созданного тега стиля, добавленного style-loader. Содержимое тега обрабатывается css-loader для синтаксического анализа css в код js, а less-loader анализирует файл less в css. Затем модуль less преобразуется в js-модуль, и webpack будет управлять им унифицированным образом.
Это процесс обработки вебпаком меньшего количества файлов в файлы js, но это лишь малая часть.Если его действительно можно использовать, то предстоит еще долгий путь, но это не тема данной статьи. К этому моменту у вас должно быть общее представление о том, что делает загрузчик в webpack и зачем он нужен. Проще говоря,Загрузчик предназначен для обработки модулей (модулей, файлов) и может обрабатывать модули таким образом, чтобы веб-пакет мог их анализировать, а также может повторно обрабатывать проанализированные файлы..
Далее я в основном расскажу как настроить загрузчик в webpack, расскажу о принципе работы загрузчика с макроуровня, заодно реализую ключевой модуль loader-runner в загрузчике вместе. Наконец, попросите всех вручную написать загрузчик стилей, загрузчик css и менее загрузчик, упомянутые выше.
Как настроить загрузчик
Ниже приведена базовая конфигурация загрузчика в веб-пакете:
module.exports = {
resolveLoader: {
// 从根目录下那个文件中寻找 loader
modules: ['node_modules', path.join(__dirname, 'loaders')],
},
module: {
rules: [{
enforce: 'normal',
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: [
"@babel/preset-env"
]
}
}]
},
{
enforce: 'pre',
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
Для получения подробной информации см.Web pack.doc — это China.org/config, как у ATI…Подробная документация для правила. Одним из наиболее важных полей является обеспечение соблюдения. Загрузчик делится на: post (пост), normal (обычный) и pre (пре) типы.
Помимо установки Loader в конфигурационном файле, за счет обработки любого файла или модуля. Таким образом, вы можете цитировать Loader в тех местах, где упоминается каждый модуль, например:
import style from 'style-loader!css-loader?modules!less-loader!./index.less'
Перед адресом файла ./index.less можно добавить загрузчик многократного использования! Сегментацию, а потом после каждого загрузчика можно добавить? Что касается загрузчика опций. Этот подход заключается в добавлении загрузчика встроенного (inline) типа загрузчика. Но также можно добавить специальный префикс тега для обозначения конкретной модели, какой тип загрузчика будет использоваться, следующим образом:
символ | Переменная | имея в виду |
---|---|---|
-! | noPreAutoLoaders | Не используйте предварительный (pre) и нормальный (normal) загрузчики |
! | noAutoLoaders | Не используйте обычный загрузчик |
!! | noPrePostAutoLoaders | Не вставайте и обычный загрузчик, пока встроенный загрузчик |
Например, для следующего:
import style from '-!style-loader!css-loader?modules!less-loader!./index.less'
Для модуля ./index.less нельзя использовать предварительно загруженный обычный загрузчик, настроенный в файле конфигурации, и для обработки этого модуля можно использовать только постзагруженный и встроенный загрузчик.
Поэтому существует четыре типа загрузчиков для обработки модулей: post (пост), normal (обычный), inline (встроенный) и pre (фронт). Существует три типа тегов, которые могут указывать, какой тип загрузчика использует конкретный модуль.Далее давайте посмотрим, как это реализовано с точки зрения исходного кода.
как работают загрузчики
Предположим, что есть следующие файлы и правила:
const request = 'inline-loader1!inline-loader2!./src/index.js';
const rules = [
{
enforce: 'pre',
test: /\.js$/,
use: ['pre-loader1', 'pre-loader2'],
},
{
enforce: 'normal',
test: /\.js$/,
use: ['normal-loader1', 'normal-loader2'],
},
{
enforce: 'post',
test: /\.js$/,
use: ['post-loader1', 'post-loader2'],
}
];
Здесь запрос означает, что модуль ./src/index.js, и модуль обрабатывается двумя встроенными загрузчиками, async-loader1 и async-loader2. В файле конфигурации веб-пакета также есть правила, в том числе предварительный загрузчик pre-loader1, pre-loader2, обычный загрузчик normal-loader1, normal-loader2, конечно, если принудительное выполнение не назначено, это обычное значение по умолчанию. Также есть пост-загрузчик post-loader1, post-loader2.
Сначала нам нужно получить эти четыре загрузчика:
const preLoaders = [];
const normalLoaders = [];
const postLoaders = [];
const inlineLoaders = request.replace(/^-?!+/, "").replace(/!!+/g, '!').split('!');
for(let i = 0; i < rules.length; i++) {
let rule = rules[i];
if(rule.test.test(resource)) {
if(rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if(rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else { // normal
normalLoaders.push(...rule.use);
}
}
}
Чтобы получить встроенный загрузчик, вам нужно использовать указанный адрес с ! Раздельное получение, но перед этим вам нужно установить пустой префикс специального маркера -?!, а также он должен быть пустым для последовательных !, чтобы избежать пустых загрузчиков. Таким образом, вы можете получить ['async-loader1', 'async-loader2', './src/index.js'], вы можете получить встроенный загрузчик, и вы можете получить другие загрузчики, перебирая правила в цикле. Пока у нас есть четыре грузчика. Стоит отметить, что порядок загрузчиков в справочнике и в правилах изменился в пределах установленного порядка.
Далее нам нужно получить список загрузчиков в том порядке, в котором они выполняются. По умолчанию, то есть без специальных флагов, загрузчики будут генерироваться в следующем порядке:
loaders = [
...postLoaders,
...inlineLoaders,
...normalLoaders,
...preLoaders,
];
По умолчанию загрузчики генерируются в порядке после встроенного обычного pre и в порядке, определяемом каждым загрузчиком.
Также влияет на содержимое загрузчиков для ссылок со специальными тегами:
if(request.startsWith('!')) { // 不要 normal
loaders = [
...postLoaders,
...inlineLoaders,
...preLoaders,
];
} else if(request.startsWith('-!')) { // 不要 normal、pre
loaders = [
...postLoaders,
...inlineLoaders
];
} else if(request.startsWith('!!')) { // 不要 post、normal、pre
loaders = [
...inlineLoaders,
];
} else { // post、inline、normal、pre
loaders = [
...postLoaders,
...inlineLoaders,
...normalLoaders,
...preLoaders,
];
}
Для запроса эталонного адреса загрузчик нормального типа не требуется, только если он начинается с !, но порядок загрузчиков других типов сохраняется. Точно так же -!не нужен нормальный, прелоадер, !!не нужен пост, нормальный, прелоадер.
На данный момент был получен список обработки погрузчика для справочного запроса на файл. Далее загрузчик в списке погрузчика должен быть обработан погрузчиком-бегуну в соответствии с определенными правилами.
runLoaders({
resource: path.join(__dirname, resource),
loaders
}, (err, data) => {
console.log(data);
});
Полный код загрузчика для получения списка загрузчиков выглядит следующим образом:
const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');
const loadDir = path.resolve(__dirname,'loaders', 'runner');
const request = 'inline-loader1!inline-loader2!./src/index.js';
let preLoaders = [];
let normalLoaders = [];
let postLoaders = [];
let inlineLoaders = request.replace(/^-?!+/, "").replace(/!!+/g, '!').split('!');
const resource = inlineLoaders.pop();
const resolveLoader = loader => path.resolve(loadDir, loader);
const rules = [
{
enforce: 'pre',
test: /\.js$/,
use: ['pre-loader1', 'pre-loader2'],
},
{
enforce: 'normal',
test: /\.js$/,
use: ['normal-loader1', 'normal-loader2'],
},
{
enforce: 'post',
test: /\.js$/,
use: ['post-loader1', 'post-loader2'],
}
];
for(let i = 0; i < rules.length; i++) {
let rule = rules[i];
if(rule.test.test(resource)) {
if(rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if(rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
}
preLoaders = preLoaders.map(resolveLoader);
normalLoaders = normalLoaders.map(resolveLoader);
inlineLoaders = inlineLoaders.map(resolveLoader);
postLoaders = postLoaders.map(resolveLoader);
let loaders = [];
if(request.startsWith('!')) { // 不要 normal
loaders = [
...postLoaders,
...inlineLoaders,
...preLoaders,
];
} else if(request.startsWith('-!')) { // 不要 normal、pre
loaders = [
...postLoaders,
...inlineLoaders
];
} else if(request.startsWith('!!')) { // 不要 post、normal、pre
loaders = [
...inlineLoaders,
];
} else { // post、inline、normal、pre
loaders = [
...postLoaders,
...inlineLoaders,
...normalLoaders,
...preLoaders,
];
}
runLoaders({
resource: path.join(__dirname, resource),
loaders,
readResource:fs.readFile.bind(fs)
}, (err, data) => {
console.log(data);
});
Подводя итог, после того, как веб-пакет получает запрос ссылочного адреса файла модуля, один шаг должен быть обработан загрузчиком.Во-первых, четыре типа загрузчиков вынимаются и собираются в загрузчики списка выполнения загрузчика в соответствии с почтовым встроенным нормальным предварительным .тип загрузчика для фильтрации. Но порядок загрузчика остается. После получения лаодеров он будет передан загрузчику-раннеру для дальнейшей обработки исходного файла по определенным правилам. Далее я подробно расскажу о более важном загрузчике-раннере, сначала объясню основные принципы и концепции, а затем вместе реализую загрузчик-раннер.
основные правила loader-runner
Вы когда-нибудь задумывались над вопросом, почему загрузчики, настроенные в конфигурационном файле, обрабатывают исходные файлы справа налево, а не слева направо? Это связано с тем, что когда загрузчик-бегун обрабатывает каждый загрузчик, он сначала выполняет метод шага загрузчика слева направо, а затем выполняет свой собственный метод загрузчика, называемый нормальным. Как показано ниже:
В предыдущем разделе загрузчики были представлены в определенной последовательности загрузчиков, которые затем будут переданы laoder-runner для выполнения.Как показано в следующем коде post-loader1, загрузчик может добавить метод питча:
function loader(source){
console.log('post-loader1 normal');
return source+"【post-loader1】";
}
loader.pitch = function(){
console.log('post-pitch1 pitch');
}
module.exports = loader;
Метод загрузчика можно назвать обычным методом.Этот метод в основном принимает содержимое исходного файла в качестве параметра, затем возвращает исходный файл постобработки, загрузчик в примере находится в основном после исходной строки, затем добавляется [ Post-loader1] , затем передать его, в качестве входящих параметров выполняется Loader's Normal. В то же время, метод PITCH также может быть добавлен в метод Normal, который в основном работает для некоторой предварительной обработки или перехвата.
Начните объяснять эту картинку, весь процесс обработки аналогичен механизму всплытия событий DOM, начните вызывать загрузчик-раннер, он будет выполняться по порядку в лаодерах, сначала выполните метод питча в загрузчике, если метод не имеет возвращаемого значения , продолжить выполнение следующего шага Шаг начинает выполнять нормаль последнего загрузчика после завершения выполнения. Затем выполните нормальный метод загрузчика справа налево, а возвращаемое значение предыдущего загрузчика используется в качестве входного параметра следующего нормального. Однако, если есть возвращаемое значение шага загрузчика, как показано красной пунктирной линией посередине, то напрямую используйте возвращаемое значение в качестве входного параметра предыдущего нормального загрузчика и продолжайте выполнение, чтобы исходный код не анализировался. , например, в качестве кеша будет использована эта сцена.
Для загрузчика как нормальный, так и шаг могут писать синхронный код и асинхронный код.Для синхронного кода он может напрямую возвращать значение, которое может использоваться в качестве входного параметра следующего загрузчика. А вот асинхронный будет немного другим, конкретный код такой:
function loader(source) {
const callback = this.async();
setTimeout(() => {
callback(null, source + "【async-loader1】");
}, 3000);
}
loader.pitch = function () {
const callback = this.async();
console.log('async-loader1-pitch');
callback(null);
}
module.exports = loader;
Во-первых, вам нужно вызвать функцию this.async, чтобы объявить, что это асинхронный метод, и вернуть дескриптор обратного вызова для выполнения последующего процесса после асинхронного выполнения. обратный вызов обеспечивает ошибку и следующие нормальные входные параметры. В то же время шаг тот же, вы также можете использовать this.async, чтобы рассказать о синхронном методе и изменить его на асинхронный.
На этом этапе мы представили процесс выполнения загрузчика, а также синхронный и асинхронный загрузчик.Далее мы будем использовать перспективу исходного кода, чтобы лучше понять загрузчик-бегун и дизайн, основанный на шаблоне цепочки ответственности.
Реализуем загрузчик-раннер
В предыдущем разделе loader-runner также был представлен общий процесс, и механизм всплытия событий DOM, цепочка областей действия, цепочка прототипов, механизм событий реакции, луковая модель koa и т. д. аналогичны, и их ядро основано на структуре цепочки событий. образец модели ответственности. Подробнее о модели цепочки ответственности см.Woohoo. Yuque.com/docs/share/…Цепочка обязанностей для множества объектов может быть единой обработкой, этого избегают, потому что запрашивающая сторона может привести к множеству типов и обработке связывания принимающей стороны, чтобы гарантировать, что запрос может пройти через множество принимающих обработок в соответствии с правилами, необходимо иметь механизм для получения хода боковой цепи, и запросчик будет осуществляться в этом звене. Поэтому важно, чтобы цепочка ответственности: чтобы гарантировать, что одна запрошенная функция требует универсальности, сигнатуры функций и возвращаемые значения должны быть согласованными и иметь возможность информировать цепочку о начале следующей; два пункта по порядку чтобы гарантировать функциональность запроса открытое закрытое, для этого процесса требуется цепочка последовательностей.
Параметр options предоставляется в коде загрузчика, который начинает выполняться:
runLoaders({
resource: path.join(__dirname, resource),
loaders,
readResource:fs.readFile.bind(fs)
}, (err, data) => {
console.log(data);
});
runLoaders 第一个参数 options 值为:
{
resource: '/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/src/index.js',
loaders: [
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader2',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/inline-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/inline-loader2',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/normal-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/normal-loader2',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/pre-loader1',
'/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/pre-loader2'
]
}
Первые параметры параметра, предоставляемые методу runLoader, имеют ресурс пути с абсолютным адресом ресурса и список загрузчиков пути с абсолютным адресом загрузчика.
Частью метода runLoaders является создание контекста выполнения, а затем вызов метода iteratePitchingLoaders для запуска выполнения laoder.
exports.runLoaders = function (options, callback) {
createLoaderContext(options);
let processOptions = {
resourceBuffer: null, //最后我们会把loader执行的Buffer结果放在这里
readResource: options.readResource || readFile,
}
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
if (err) {
return callback(err, {});
}
callback(null, {
result,
resourceBuffer: processOptions.resourceBuffer
});
});
};
Далее давайте представим контекст выполнения загрузчика и объект загрузчика.
function parsePathQueryFragment(resource) { //resource =./src/index.js?name=wms#1
let result = /^([^?#]*)(\?[^#]*)?(#.*)?$/.exec(resource);
return {
path: result[1], //路径名 ./src/index.js
query: result[2], // ?name=wms
fragment: result[3] // #1
}
};
//loader绝对路径
// 比如:/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader1?{"presets":["/Users/anning/Desktop/webpack-demo/node_modules/@babel/preset-env"]}"
function createLoaderObject(loader) {
let obj = {
path: '', //当前loader的绝对路径
query: '', //当前loader的查询参数
fragment: '', //当前loader的片段
normal: null, //当前loader的normal函数,也就是loader本函数
pitch: null, //当前loader的pitch函数
raw: null, //是否是Buffer
data: {}, //自定义对象 每个loader都会有一个data自定义对象
pitchExecuted: false, //当前 loader的pitch函数已经执行过了,不需要再执行了
normalExecuted: false //当前loader的normal函数已经执行过了,不需要再执行
}
Object.defineProperty(obj, 'request', {
get() {
return obj.path + obj.query + obj.fragment;
},
set(value) {
let splittedRequest = parsePathQueryFragment(value);
obj.path = splittedRequest.path;
obj.query = splittedRequest.query;
obj.fragment = splittedRequest.fragment;
}
});
obj.request = loader;
return obj;
};
function loadLoader(loaderObject) {
let normal = require(loaderObject.path);
loaderObject.normal = normal;
loaderObject.pitch = normal.pitch;
loaderObject.raw = normal.raw;
};
function createLoaderContext(options) {
// 要加载的资源的绝对路径
const splittedResource = parsePathQueryFragment(options.resource || '');
// 准备loader对象数组
loaders = (options.loaders || []).map(createLoaderObject);
// loader执行时候的上下文对象 这个对象将会成为loader执行的时候的this指针
const loaderContext = {};
loaderContext.context = path.dirname(splittedResource.path); // 要加载的资源的所在目录
loaderContext.loaderIndex = 0; //当前处理的loader索引
loaderContext.loaders = loaders; // loader集合
loaderContext.resourcePath = splittedResource.path;//资源绝对路径
loaderContext.resourceQuery = splittedResource.query;// 资源路径中解析出的query
loaderContext.resourceFragment = splittedResource.fragment;// 资源路径中解析出的片段
loaderContext.async = null; //是一个方法,可以使loader的执行从同步改成异步
loaderContext.callback = null; //调用下一个loader
// 加载资源的完整路径
Object.defineProperty(loaderContext, 'resource', {
get() {
return loaderContext.resourcePath + loaderContext.resourceQuery + loaderContext.resourceFragment;
}
});
//request =loader1!loader2!loader3!./src/index.js
Object.defineProperty(loaderContext, 'request', {
get() {
return loaderContext.loaders.map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//剩下的loader从当前的下一个loader开始取,加上resource
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//当前loader从当前的loader开始取,加上resource
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//之前loader
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(l => l.request)
}
});
//当前loader的query, 如果配置中配置了options则使用,否则使用query中的
Object.defineProperty(loaderContext, 'query', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.options || loader.query;
}
});
//当前loader的data,可以在pitch normal函数中获取到
Object.defineProperty(loaderContext, 'data', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.data;
}
});
};
createLoaderContext — генерировать контекст для выполнения нормали или питча на основе входящих опций, то есть this. Помимо некоторых общих параметров, более важным является асинхронный обратный вызов. В представленном ранее асинхронном загрузчике this в this.async является loaderContext, а также есть глобальный объект loaderIndex.loaderIndex может использоваться для управления тем, какой загрузчик выполняется, и должен ли процесс быть следующим шагом или предыдущим шаг. То есть каждый загрузчик гарантирует единственную ответственность и контролирует, продолжать ли выполнение через this.callback.
createLoaderObject заключается в создании объекта загрузчика на основе абсолютного адреса каждого загрузчика, который включает в себя такие функции, как normal, pitch и так далее. Также есть данные для обмена данными между normal и pitch в одном и том же загрузчике, а также бит флага для завершения pitchExecuted и normalExecuted. Функция loadLoader состоит в том, чтобы загрузить загрузчик, module.exports — это нормально, а затем получить шаг и необработанные данные.
Давайте посмотрим, как это работает в деталях. Выполнение разделено на три части: одна часть — это метод iteratePitchingLoaders, который в основном используется для управления потоком между методами шага, а другой — iterateNormalLoaders для управления потоком нормальной функции в обычном режиме, а также runSyncOrAsync, который выполняет нормальная и тональная функции. Когда шаг достигает конца, метод processResource необходим для получения исходного файла.
Вот код iteratePitchingLoaders:
function iteratePitchingLoaders(options, loaderContext, callback) {
// 所有的 pitch 处理完毕,开始获取源代码
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
return processResource(options, loaderContext, callback);
}
//获取当前的loader
const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 已经处理过pitch了,需要处理下一个loader的pitch
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback)
}
// 加载laoder
loadLoader(currentLoaderObject);
let pitchFunction = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if (!pitchFunction) {
return iteratePitchingLoaders(options, loaderContext, callback);
}
runSyncOrAsync(
pitchFunction, //要执行的pitch函数
loaderContext, //上下文对象
//这是要传递给pitchFunction的参数数组
[loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data = {}],
//处理完成的回调
function (err, ...args) {
if (args.length > 0) { //如果args有值,说明这个pitch有返回值
loaderContext.loaderIndex--; //索引减1,开始回退了
iterateNormalLoaders(options, loaderContext, args, callback);
} else { //如果没有返回值,则执行下一个loader的pitch函数
iteratePitchingLoaders(options, loaderContext, callback)
}
}
);
};
В частности, используйте loadLoader, чтобы загрузить процессор загрузчика, получить функцию основного тона и установить для флага основного тона лаодера значение true, указывая, что функция начинает входить в стадию обработки. На этапе обработки, если полученная функция шага не существует, непосредственно повторно вызовите iteratePitchingLoaders, чтобы перейти к следующему выполнению шага, в противном случае он вызовет runSyncAsync для выполнения функции шага и пожалеет о параметрах, переданных в соответствии с обратным вызовом после выполнения. При наличии параметров loaderIndex уменьшится на единицу Start для выполнения обычной функции предыдущего загрузчика, в противном случае продолжите выполнение следующего питча, проинструктируйте все питчи обработать и начните вызывать метод processResource для получения исходного кода и выполнить нормально. Параметры, передаваемые для выполнения шага, — это оставшийся запрос, предыдущий запрос и данные, в основном
function processResource(options, loaderContext, callback) {
//重置loaderIndex 改为loader长度减1
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
let resourcePath = loaderContext.resourcePath;
//调用 fs.readFile方法读取资源内容
options.readResource(resourcePath, function (err, buffer) {
if (err) return callback(error);
options.resourceBuffer = buffer; //resourceBuffer放的是资源的原始内容
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
}
После того, как функция processResource получает исходный код ресурса, она начинает вызывать normal.В это время loaderIndex должен быть iteratePitchingLoaders плюс 1, а затем минус 1.
Затем введите функцию iterateNormalLoaders:
function iterateNormalLoaders(options, loaderContext, args, callback) {
//如果正常的normal loader全部执行完了
if (loaderContext.loaderIndex < 0) {
return callback(null, args);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
//如果说当这个normal已经执行过了,让索引减少1
if (currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback)
}
let normalFn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
runSyncOrAsync(normalFn, loaderContext, args, function (err) {
if (err) return callback(err);
let args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
Метод iterateNormalLoaders После получения функции нормали установите флаг нормали в значение true и начните вызывать runSyncOrAsync для выполнения кода.После завершения выполнения получаются параметры и далее продолжается выполнение следующей нормали.
function runSyncOrAsync(fn, context, args, callback) {
let isSync = true; //默认是同步
let isDone = false; //是否完成,是否执行过此函数了,默认是false
//调用context.async this.async 可以把同步把异步,表示这个loader里的代码是异步的
context.async = function () {
isSync = false; //改为异步
return innerCallback;
}
const innerCallback = context.callback = function () {
isDone = true; //表示当前函数已经完成
isSync = false; //改为异步
callback.apply(null, arguments); //执行 callback
}
//第一次fn=pitch1,执行pitch1
let result = fn.apply(context, args);
//在执行pitch2的时候,还没有执行到pitch1 这行代码
if (isSync) {
isDone = true;
return callback(null, result);
}
}
runSyncOrAsync Для основной и обычной функций основного загрузчика, если this.async не вызывается в функции, context.aysnc не будет выполняться, то есть, если isSync все еще true, метод будет выполняться напрямую, используя loaderContext как это для выполнения, а возвращаемое значение в качестве результата для направления вызова. В последующем процессе, если это асинхронная функция, вам нужно дождаться завершения асинхронного выполнения функции, а затем вызвать обратный вызов для продолжения последующего исполнение. То есть их выполнение является асинхронным последовательным, а не асинхронным параллельным выполнением.
Ниже приведен полный код laoder-runner:
const fs = require('fs');
const path = require('path');
const readFile = fs.readFile.bind(fs);
function parsePathQueryFragment(resource) { //resource =./src/index.js?name=wms#1
let result = /^([^?#]*)(\?[^#]*)?(#.*)?$/.exec(resource);
return {
path: result[1], //路径名 ./src/index.js
query: result[2], // ?name=wms
fragment: result[3] // #1
}
};
//loader绝对路径
// 比如:/Users/mt/Documents/my-lib-learn/webpack/5.webpack-loader/loaders/runner/post-loader1?{"presets":["/Users/anning/Desktop/webpack-demo/node_modules/@babel/preset-env"]}"
function createLoaderObject(loader) {
let obj = {
path: '', //当前loader的绝对路径
query: '', //当前loader的查询参数
fragment: '', //当前loader的片段
normal: null, //当前loader的normal函数,也就是loader本函数
pitch: null, //当前loader的pitch函数
raw: null, //是否是Buffer
data: {}, //自定义对象 每个loader都会有一个data自定义对象
pitchExecuted: false, //当前 loader的pitch函数已经执行过了,不需要再执行了
normalExecuted: false //当前loader的normal函数已经执行过了,不需要再执行
}
Object.defineProperty(obj, 'request', {
get() {
return obj.path + obj.query + obj.fragment;
},
set(value) {
let splittedRequest = parsePathQueryFragment(value);
obj.path = splittedRequest.path;
obj.query = splittedRequest.query;
obj.fragment = splittedRequest.fragment;
}
});
obj.request = loader;
return obj;
};
function loadLoader(loaderObject) {
let normal = require(loaderObject.path);
loaderObject.normal = normal;
loaderObject.pitch = normal.pitch;
loaderObject.raw = normal.raw;
};
function createLoaderContext(options) {
// 要加载的资源的绝对路径
const splittedResource = parsePathQueryFragment(options.resource || '');
// 准备loader对象数组
loaders = (options.loaders || []).map(createLoaderObject);
// loader执行时候的上下文对象 这个对象将会成为loader执行的时候的this指针
const loaderContext = {};
loaderContext.context = path.dirname(splittedResource.path); // 要加载的资源的所在目录
loaderContext.loaderIndex = 0; //当前处理的loader索引
loaderContext.loaders = loaders; // loader集合
loaderContext.resourcePath = splittedResource.path;//资源绝对路径
loaderContext.resourceQuery = splittedResource.query;// 资源路径中解析出的query
loaderContext.resourceFragment = splittedResource.fragment;// 资源路径中解析出的片段
loaderContext.async = null; //是一个方法,可以使loader的执行从同步改成异步
loaderContext.callback = null; //调用下一个loader
// 加载资源的完整路径
Object.defineProperty(loaderContext, 'resource', {
get() {
return loaderContext.resourcePath + loaderContext.resourceQuery + loaderContext.resourceFragment;
}
});
//request =loader1!loader2!loader3!./src/index.js
Object.defineProperty(loaderContext, 'request', {
get() {
return loaderContext.loaders.map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//剩下的loader从当前的下一个loader开始取,加上resource
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//当前loader从当前的loader开始取,加上resource
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(l => l.request).concat(loaderContext.resource).join('!')
}
});
//之前loader
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(l => l.request)
}
});
//当前loader的query, 如果配置中配置了options则使用,否则使用query中的
Object.defineProperty(loaderContext, 'query', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.options || loader.query;
}
});
//当前loader的data,可以在pitch normal函数中获取到
Object.defineProperty(loaderContext, 'data', {
get() {
let loader = loaderContext.loaders[loaderContext.loaderIndex];
return loader.data;
}
});
};
function convertArgs(args, raw) {
// 如果这个loader需要buffer, args[0]不是, 需要转成buffer
if (raw && !Buffer.isBuffer(args[0])) {
args[0] = Buffer.from(args[0], 'utf8');
} else if (!raw && Buffer.isBuffer(args[0])) {
args[0] = args[0].toString('utf8');
}
};
function runSyncOrAsync(fn, context, args, callback) {
let isSync = true; //默认是同步
let isDone = false; //是否完成,是否执行过此函数了,默认是false
//调用context.async this.async 可以把同步把异步,表示这个loader里的代码是异步的
context.async = function () {
isSync = false; //改为异步
return innerCallback;
}
const innerCallback = context.callback = function () {
isDone = true; //表示当前函数已经完成
isSync = false; //改为异步
callback.apply(null, arguments); //执行 callback
}
//第一次fn=pitch1,执行pitch1
let result = fn.apply(context, args);
//在执行pitch2的时候,还没有执行到pitch1 这行代码
if (isSync) {
isDone = true;
return callback(null, result);
}
}
function processResource(options, loaderContext, callback) {
//重置loaderIndex 改为loader长度减1
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
let resourcePath = loaderContext.resourcePath;
//调用 fs.readFile方法读取资源内容
options.readResource(resourcePath, function (err, buffer) {
if (err) return callback(error);
options.resourceBuffer = buffer; //resourceBuffer放的是资源的原始内容
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
}
function iterateNormalLoaders(options, loaderContext, args, callback) {
//如果正常的normal loader全部执行完了
if (loaderContext.loaderIndex < 0) {
return callback(null, args);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
//如果说当这个normal已经执行过了,让索引减少1
if (currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback)
}
let normalFn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
convertArgs(args, currentLoaderObject.raw);
runSyncOrAsync(normalFn, loaderContext, args, function (err) {
if (err) return callback(err);
let args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
function iteratePitchingLoaders(options, loaderContext, callback) {
// 所有的 pitch 处理完毕,开始获取源代码
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
return processResource(options, loaderContext, callback);
}
//获取当前的loader
const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 已经处理过pitch了,需要处理下一个loader的pitch
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback)
}
// 加载laoder
loadLoader(currentLoaderObject);
let pitchFunction = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if (!pitchFunction) {
return iteratePitchingLoaders(options, loaderContext, callback);
}
runSyncOrAsync(
pitchFunction, //要执行的pitch函数
loaderContext, //上下文对象
//这是要传递给pitchFunction的参数数组
[loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data = {}],
//处理完成的回调
function (err, ...args) {
if (args.length > 0) { //如果args有值,说明这个pitch有返回值
loaderContext.loaderIndex--; //索引减1,开始回退了
iterateNormalLoaders(options, loaderContext, args, callback);
} else { //如果没有返回值,则执行下一个loader的pitch函数
iteratePitchingLoaders(options, loaderContext, callback)
}
}
);
};
exports.runLoaders = function (options, callback) {
createLoaderContext(options);
let processOptions = {
resourceBuffer: null, //最后我们会把loader执行的Buffer结果放在这里
readResource: options.readResource || readFile,
}
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
if (err) {
return callback(err, {});
}
callback(null, {
result,
resourceBuffer: processOptions.resourceBuffer
});
});
};
Пока мы разобрались, что такое загрузчик, как его настроить в webpack и как работает принцип загрузчика, а с точки зрения исходного кода загрузчика-раннера у нас будет более глубокое понимание. Далее идет простая реализация css-loader и style-loader.
начать писать загрузчик
Вот несколько простых реализаций загрузчика:
css-loader
let postcss = require('postcss');
let loaderUtils = require('loader-utils');
let Tokenizer = require('css-selector-tokenizer');
function loader(cssString){
const cssPlugin = (options)=>{
return (cssRoot)=>{
//遍历语法树,找到所有的import语句
cssRoot.walkAtRules(/^import$/i,rule=>{
rule.remove();//删除 这个import
let imp = rule.params.slice(1,-1);
options.imports.push(imp);
});
cssRoot.walkDecls(decl=>{
let values = Tokenizer.parseValues(decl.value);
values.nodes.forEach(function(value){
value.nodes.forEach(item=>{
if(item.type === 'url'){
item.url = "`+require("+loaderUtils.stringifyRequest(this,item.url)+").default+`";
console.log('====item',item);
}
});
});
decl.value = Tokenizer.stringifyValues(values);
});
}
}
let callback = this.async();
let options = {imports:[]};//["./global.css"]
//源代码会经过流水线的一个个的插件
let pipeLine = postcss([cssPlugin(options)]);
pipeLine.process(cssString).then(result=>{
let importCSS = options.imports.map(url=>{
return "`+require("+loaderUtils.stringifyRequest(this,"!!css-loader2!"+url)+")+`";
}).join('\r\n');
let output = "module.exports = `"+importCSS+"\n"+result.css+"`";
output=output.replace(/\\"/g,'"');
callback(null,output);
});
}
module.exports = loader;
style-loader:
let loaderUtils = require('loader-utils');
function loader(source){
};
loader.pitch = function(remainingRequest,previousRequest,data) {
let script = `
let style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(this,"!!"+remainingRequest)});
document.head.appendChild(style);
`;
return script;
};
module.exports = loader;
file-loader
const path = require('path');
const { getOptions,interpolateName } = require('loader-utils');
function loader(content){
let options = getOptions(this)||{};
let filename = interpolateName(this, options.filename, {
content
});
this.emitFile(filename, content);
return `export default ${JSON.stringify(filename)}`;
}
//加载的二进制,处理的是 Buffer 类型数据
loader.raw = true;
module.exports = loader;