Подробное объяснение четвертого загрузчика вебпака серии 2

исходный код Webpack

Автор серии: Сяо Лэй

GitHub: github.com/CommanderXL

Предыдущая статьяВ основном речь идет о конфигурации загрузчика и механизма согласования. В этой статье в основном речь пойдет о механизме процесса использования загрузчика для обработки содержимого этого модуля после его создания. Во-первых, давайте в общих чертах посмотрим на весь процесс:

runLoader

В процессе сборки модуля в начале сначала будет создан объект loaderContext, который имеет взаимно однозначное соответствие с модулем, а все загрузчики, используемые модулем, будут совместно использовать объект loaderContext, являющийся контекстом при каждый загрузчик выполняет объект loaderContext, поэтому к нему можно получить доступ через this в написанном нами загрузчике.

// NormalModule.js

const { runLoaders } = require('loader-runner')

class NormalModule extends Module {
  ...
  createLoaderContext(resolver, options, compilation, fs) {
    const requestShortener = compilation.runtimeTemplate.requestShortener;
    // 初始化 loaderContext 对象,这些初始字段的具体内容解释在文档上有具体的解释(https://webpack.docschina.org/api/loaders/#this-data)
		const loaderContext = {
			version: 2,
			emitWarning: warning => {...},
			emitError: error => {...},
			exec: (code, filename) => {...},
			resolve(context, request, callback) {...},
			getResolve(options) {...},
			emitFile: (name, content, sourceMap) => {...},
			rootContext: options.context, // 项目的根路径
			webpack: true,
			sourceMap: !!this.useSourceMap,
			_module: this,
			_compilation: compilation,
			_compiler: compilation.compiler,
			fs: fs
		};

    // 触发 normalModuleLoader 的钩子函数,开发者可以利用这个钩子来对 loaderContext 进行拓展
		compilation.hooks.normalModuleLoader.call(loaderContext, this);
		if (options.loader) {
			Object.assign(loaderContext, options.loader);
		}

		return loaderContext;
  }

  doBuild(options, compilation, resolver, fs, callback) {
    // 创建 loaderContext 上下文
		const loaderContext = this.createLoaderContext(
			resolver,
			options,
			compilation,
			fs
    )
    
    runLoaders(
      {
        resource: this.resource, // 这个模块的路径
				loaders: this.loaders, // 模块所使用的 loaders
				context: loaderContext, // loaderContext 上下文
				readResource: fs.readFile.bind(fs) // 读取文件的 node api
      },
      (err, result) => {
        // do something
      }
    )
  }
  ...
}

Когда инициализация loaderContext завершена, вызывается метод runLoaders, и в это время начинается этап выполнения загрузчиков. Метод runLoaders создаетсяloader-runnerМетод, предоставляемый этим независимым пакетом NPM, а затем давайте посмотрим, как метод runLoaders работает внутри.

Во-первых, завершить дальнейшую обработку в соответствии с входящими параметрами и дополнительно расширить свойства объекта loaderContext:

exports.runLoaders = function runLoaders(options, callback) {
  // read options
	var resource = options.resource || ""; // 模块的路径
	var loaders = options.loaders || []; // 模块所需要使用的 loaders
	var loaderContext = options.context || {}; // 在 normalModule 里面创建的 loaderContext
	var readResource = options.readResource || readFile;

	var splittedResource = resource && splitQuery(resource);
	var resourcePath = splittedResource ? splittedResource[0] : undefined; // 模块实际路径
	var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 模块路径 query 参数
	var contextDirectory = resourcePath ? dirname(resourcePath) : null; // 模块的父路径

	// execution state
	var requestCacheable = true;
	var fileDependencies = [];
	var contextDependencies = [];

	// prepare loader objects
	loaders = loaders.map(createLoaderObject); // 处理 loaders 

  // 拓展 loaderContext 的属性
	loaderContext.context = contextDirectory;
	loaderContext.loaderIndex = 0; // 当前正在执行的 loader 索引
	loaderContext.loaders = loaders;
	loaderContext.resourcePath = resourcePath;
	loaderContext.resourceQuery = resourceQuery;
	loaderContext.async = null; // 异步 loader
  loaderContext.callback = null;

  ...

  // 需要被构建的模块路径,将 loaderContext.resource -> getter/setter
  // 例如 /abc/resource.js?rrr
  Object.defineProperty(loaderContext, "resource", {
		enumerable: true,
		get: function() {
			if(loaderContext.resourcePath === undefined)
				return undefined;
			return loaderContext.resourcePath + loaderContext.resourceQuery;
		},
		set: function(value) {
			var splittedResource = value && splitQuery(value);
			loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined;
			loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined;
		}
  });

  // 构建这个 module 所有的 loader 及这个模块的 resouce 所组成的 request 字符串
  // 例如:/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr
	Object.defineProperty(loaderContext, "request", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
  });
  // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,剩下还未被调用的 loader.pitch 所组成的 request 字符串
	Object.defineProperty(loaderContext, "remainingRequest", {
		enumerable: true,
		get: function() {
			if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource)
				return "";
			return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
  });
  // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,包含当前 loader.pitch 所组成的 request 字符串
	Object.defineProperty(loaderContext, "currentRequest", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
  });
  // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,包含已经被执行的 loader.pitch 所组成的 request 字符串
	Object.defineProperty(loaderContext, "previousRequest", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).join("!");
		}
  });
  // 获取当前正在执行的 loader 的query参数
  // 如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象
  // 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串
	Object.defineProperty(loaderContext, "query", {
		enumerable: true,
		get: function() {
			var entry = loaderContext.loaders[loaderContext.loaderIndex];
			return entry.options && typeof entry.options === "object" ? entry.options : entry.query;
		}
  });
  // 每个 loader 在 pitch 阶段和正常执行阶段都可以共享的 data 数据
	Object.defineProperty(loaderContext, "data", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders[loaderContext.loaderIndex].data;
		}
  });
  
  var processOptions = {
		resourceBuffer: null, // module 的内容 buffer
		readResource: readResource
  };
  // 开始执行每个 loader 上的 pitch 函数
	iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
    // do something...
  });
}

Краткий обзор здесь заключается в том, чтобы инициализировать соответствующие параметры в начале метода runLoaders, особенно переписав некоторые свойства в loaderContext для функций получения/установки, чтобы некоторые параметры можно было получить динамически на разных этапах выполнения загрузчика.

Затем вызовите метод iteratePitchingLoaders, чтобы выполнить функцию подачи, предусмотренную для каждого загрузчика. Каждый, кто написал загрузчик, должен знать, что каждый загрузчик может монтировать функцию питча, а метод питча, предоставляемый каждым загрузчиком, прямо противоположен фактическому порядку выполнения загрузчика. Содержимое этого блока также подробно описано в документации веб-пакета (пожалуйста, ткните меня).

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

function iteratePitchingLoaders() {
  // abort after last loader
	if(loaderContext.loaderIndex >= loaderContext.loaders.length)
		return processResource(options, loaderContext, callback);

  // 根据 loaderIndex 来获取当前需要执行的 loader
	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // iterate
  // 如果被执行过,那么直接跳过这个 loader 的 pitch 函数
	if(currentLoaderObject.pitchExecuted) {
		loaderContext.loaderIndex++;
		return iteratePitchingLoaders(options, loaderContext, callback);
	}

	// 加载 loader 模块
	// load loader module
	loadLoader(currentLoaderObject, function(err) {
		// do something ...
	});
}

Прежде чем каждый раз выполнять функцию питча, сначала получите загрузчик (currentLoaderObject), который необходимо выполнить в соответствии с loaderIndex, и вызовите функцию loadLoader для загрузки загрузчика.loadLoader внутренне совместим с определениями таких модулей, как SystemJS, ES. Module и CommonJs, и в конечном итоге будет передавать шаг, предоставленный загрузчиком.Методы и обычные методы назначаются для currentLoaderObject:

// loadLoader.js
module.exports = function (loader, callback) {
  ...
  var module = require(loader.path)
 
  ...
  loader.normal = module

  loader.pitch = module.pitch

  loader.raw = module.raw

  callback()
  ...
}

Когда загрузчик загружен, начните выполнение обратного вызова loadLoader:

loadLoader(currentLoaderObject, function(err) {
  var fn = currentLoaderObject.pitch; // 获取 pitch 函数
  currentLoaderObject.pitchExecuted = true;
  if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); // 如果这个 loader 没有提供 pitch 函数,那么直接跳过

  // 开始执行 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);
      // Determine whether to continue the pitching process based on
      // argument values (as opposed to argument presence) in order
      // to support synchronous and asynchronous usages.
      // 根据是否有参数返回来判断是否向下继续进行 pitch 函数的执行
      var hasArg = args.some(function(value) {
        return value !== undefined;
      });
      if(hasArg) {
        loaderContext.loaderIndex--;
        iterateNormalLoaders(options, loaderContext, args, callback);
      } else {
        iteratePitchingLoaders(options, loaderContext, callback);
      }
    }
  );
})

Здесь есть метод runSyncOrAsync, который будет описан позже, и запустить выполнение функции тона, когда функция тона будет выполнена, выполнить входящую функцию обратного вызова. Мы видим, что функция обратного вызова будет определять количество полученных параметров.Помимо первого параметра err, если есть другие параметры (эти параметры передаются в функцию обратного вызова после выполнения функции тона), то она напрямую попадет в Стадия выполнения обычного метода загрузчика напрямую пропускает последующую стадию выполнения загрузчика. Если функция основного тона не возвращает значение, то переходите к фазе выполнения функции основного тона следующего загрузчика. Вернемся внутрь метода iteratePitchingLoaders, когда выполняются все функции питча на загрузчике, то есть когда значение индекса loaderIndex >= длины массива загрузчика:

function iteratePitchingLoaders () {
  ...

  if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

  ...
}

function processResource(options, loaderContext, callback) {
	// set loader index to last loader
	loaderContext.loaderIndex = loaderContext.loaders.length - 1;

	var resourcePath = loaderContext.resourcePath;
	if(resourcePath) {
		loaderContext.addDependency(resourcePath); // 添加依赖
		options.readResource(resourcePath, function(err, buffer) {
			if(err) return callback(err);
			options.resourceBuffer = buffer;
			iterateNormalLoaders(options, loaderContext, [buffer], callback);
		});
	} else {
		iterateNormalLoaders(options, loaderContext, [null], callback);
	}
}

Внутри метода processResouce вызовите API узла readResouce, чтобы прочитать текстовое содержимое соответствующего пути модуля, вызовите метод iterateNormalLoaders и запустите этап выполнения обычного метода загрузчика.

function iterateNormalLoaders () {
  if(loaderContext.loaderIndex < 0)
		return callback(null, args);

	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

	// iterate
	if(currentLoaderObject.normalExecuted) {
		loaderContext.loaderIndex--;
		return iterateNormalLoaders(options, loaderContext, args, callback);
	}

	var fn = currentLoaderObject.normal;
	currentLoaderObject.normalExecuted = true;
	if(!fn) {
		return iterateNormalLoaders(options, loaderContext, args, callback);
	}

  // buffer 和 utf8 string 之间的转化
	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);
	});
}

Внутри метода iterateNormalLoaders метод normal для каждого загрузчика выполняется в порядке справа налево (прямо противоположном порядку выполнения метода питча). Выполнение загрузчика, будь то метод питча или обычный метод, может быть синхронным или асинхронным. Здесь упоминается обычный метод.Как правило, если загрузчик, который вы пишете, требует большого количества вычислений, вы можете сделать свой загрузчик асинхронным и вызывать его в методе загрузчика.this.asyncметод, который возвращает асинхронную функцию обратного вызова. Когда фактическое содержимое внутри вашего загрузчика выполняется, вы можете вызвать этот асинхронный обратный вызов, чтобы начать выполнение следующего загрузчика.

module.exports = function (content) {
  const callback = this.async()
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    callback(null, result);
  });
}

В дополнение к вызову this.async для асинхронизации загрузчика, другим способом является возврат обещания в вашем загрузчике, и только когда обещание будет разрешено, будет вызван следующий загрузчик (конкретный механизм реализации см. ниже):

module.exports = function (content) {
  return new Promise(resolve => {
    someAsyncOpertion(content, function(err, result) {
      if (err) resolve(err)
      resolve(null, result)
    })
  })
}

Еще один момент, на который следует обратить внимание, это то, что в процессе передачи данных между вышестоящим и нижестоящим загрузчиками, если нижестоящий загрузчик получает один параметр, то после завершения выполнения предыдущего загрузчика, если он синхронный, его можно вернуть напрямую:

module.exports = function (content) {
  // do something
  return content
}

Если он асинхронный, вызовите асинхронный обратный вызов напрямую и передайте его дальше (см. асинхронность загрузчика выше). Если нижестоящий загрузчик получает более одного параметра после завершения выполнения предыдущего загрузчика, если он синхронный, то необходимо вызвать функцию обратного вызова, предоставляемую loaderContext:

module.exports = function (content) {
  // do something
  this.callback(null, content, argA, argB)
}

Если он асинхронный, продолжайте вызывать асинхронную функцию обратного вызова и передайте ее дальше (см. асинхронность загрузчика выше). Конкретный механизм выполнения включает в себя не упомянутый выше метод runSyncOrAsync, который предоставляет интерфейс для вызовов восходящего и нисходящего загрузчика:

function runSyncOrAsync(fn, context, args, callback) {
	var isSync = true; // 是否为同步
	var isDone = false;
	var isError = false; // internal error
	var reportedError = false;
	// 给 loaderContext 上下文赋值 async 函数,用以将 loader 异步化,并返回异步回调
	context.async = function async() {
		if(isDone) {
			if(reportedError) return; // ignore
			throw new Error("async(): The callback was already called.");
		}
		isSync = false; // 同步标志位置为 false
		return innerCallback;
  };
  // callback 的形式可以向下一个 loader 多个参数
	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;
		}
	};
	try {
		// 开始执行 loader
		var result = (function LOADER_EXECUTION() {
			return fn.apply(context, args);
    }());
    // 如果为同步的执行
		if(isSync) {
      isDone = true;
      // 如果 loader 执行后没有返回值,执行 callback 开始下一个 loader 执行
			if(result === undefined)
        return callback();
      // loader 返回值为一个 promise 实例,待这个实例被resolve或者reject后执行下一个 loader。这也是 loader 异步化的一种方式
			if(result && typeof result === "object" && typeof result.then === "function") {
				return result.catch(callback).then(function(r) {
					callback(null, r);
				});
      }
      // 如果 loader 执行后有返回值,执行 callback 开始下一个 loader 执行
			return callback(null, result);
		}
	} catch(e) {
		// do something
	}
}

Выше приведен анализ исходного кода процесса выполнения загрузчика в процессе построения модуля. Возможно, вы обычно используете процесс веб-пакета, чтобы понять соответствующие правила и стратегии выполнения загрузчика, и с этим анализом внутреннего механизма вы должны быть более впечатлены использованием загрузчика веб-пакета.