Подробное объяснение с несколькими изображениями, одноразовое понимание загрузчика Webpack

внешний интерфейс JavaScript Webpack
Подробное объяснение с несколькими изображениями, одноразовое понимание загрузчика Webpack

⚠️Эта статья является первой подписанной статьей сообщества 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Эта статья.

10. Справочные ресурсы