Series author: Xiao Lei

GitHub: github.com/CommanderXL

The previous article mainly talked about loader configuration, matching related mechanisms. This article will mainly talk about the process mechanism of using Loader to process the content of a module after it is created. First of all, let’s take a general look at the whole process:

When a Module is first built, a loaderContext object is created. This object is one-to-one with the Module, and all loaders used by the Module share this loaderContext object. The context of each loader execution is the loaderContext object, so it can be accessed through this in the loader we wrote.

// NormalModule.js

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

class NormalModule extends Module {... createLoaderContext(resolver, options, compilation, fs) {const requestShortener = compilation.runtimeTemplate.requestShortener;
    / / initialization loaderContext object, the specific content of the initial field explain specific explanation on the document (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, // The root path of the project
			webpack: true.sourceMap:!!!!!this.useSourceMap,
			_module: this._compilation: compilation,
			_compiler: compilation.compiler,
			fs: fs
		};

    // Triggers the normalModuleLoader hook function, which developers can use to extend the loaderContext
		compilation.hooks.normalModuleLoader.call(loaderContext, this);
		if (options.loader) {
			Object.assign(loaderContext, options.loader);
		}

		return loaderContext;
  }

  doBuild(options, compilation, resolver, fs, callback) {
    // Create a loaderContext
		const loaderContext = this.createLoaderContext(
			resolver,
			options,
			compilation,
			fs
    )
    
    runLoaders(
      {
        resource: this.resource, // The path to this module
				loaders: this.loaders, // Loaders used by the module
				context: loaderContext, // loaderContext context
				readResource: fs.readFile.bind(fs) // Read the file node API
      },
      (err, result) => {
        // do something})}... }Copy the code

When the loaderContext is initialized, the runLoaders method is called, and the loaders execution phase is entered. The runLoaders method is provided by loader-Runner as a separate NPM package, so let’s take a look at how the runLoaders method works internally.

Further processing is done based on the parameters passed in, and the loaderContext attributes are further extended:

exports.runLoaders = function runLoaders(options, callback) {
  // read options
	var resource = options.resource || ""; // The path to the module
	var loaders = options.loaders || []; // Loaders required by the module
	var loaderContext = options.context || {}; // loaderContext created in normalModule
	var readResource = options.readResource || readFile;

	var splittedResource = resource && splitQuery(resource);
	var resourcePath = splittedResource ? splittedResource[0] : undefined; // The actual path of the module
	var resourceQuery = splittedResource ? splittedResource[1] : undefined; // Module path query parameter
	var contextDirectory = resourcePath ? dirname(resourcePath) : null; // The module's parent path

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

	// prepare loader objects
	loaders = loaders.map(createLoaderObject); / / deal with loaders

  // Extend the properties of loaderContext
	loaderContext.context = contextDirectory;
	loaderContext.loaderIndex = 0; // The current loader index being executed
	loaderContext.loaders = loaders;
	loaderContext.resourcePath = resourcePath;
	loaderContext.resourceQuery = resourceQuery;
	loaderContext.async = null; Asynchronous loader / /
  loaderContext.callback = null; .// The path to the module to be built, loaderContext.resource -> getter/setter
  / / such as/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; }});// Build the request string of all loaders for this module and the module's resouce
  // For example: / 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("!"); }});// One of the arguments passed in the stage of executing the pitch function provided by Loader, the remaining request string of loader.pitch that has not yet been called
	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("!"); }});// One of the arguments passed in the execution stage of the pitch function provided by loader, containing the request string composed of the current loader.pitch
	Object.defineProperty(loaderContext, "currentRequest", {
		enumerable: true.get: function() {
			return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!"); }});// One of the arguments passed during the execution of the pitch function provided by loader, containing the request string of the loader.pitch function that has been executed
	Object.defineProperty(loaderContext, "previousRequest", {
		enumerable: true.get: function() {
			return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).join("!"); }});// Get the query parameter of the currently executing loader
  // If the loader is configured with options, this.query points to the option object
  // If the loader is called with a query string instead of options, this.query is called with a? Leading string
	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; }});// Each loader can share data during pitch and normal execution
	Object.defineProperty(loaderContext, "data", {
		enumerable: true.get: function() {
			returnloaderContext.loaders[loaderContext.loaderIndex].data; }});var processOptions = {
		resourceBuffer: null.// The contents of the module buffer
		readResource: readResource
  };
  // Start executing pitch function on each loader
	iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
    // do something...
  });
}
Copy the code

To summarize, the parameters of the runLoaders method are initialized at the beginning of the method. In particular, some properties of the loaderContext are written into getter/setter functions so that parameters can be dynamically obtained at different loaders execution stages.

Next, we call the iteratePitchingLoaders method to execute the pitch function provided on each loader. Each loader can mount a pitch function. The pitch method provided by each loader is exactly the opposite of the actual execution order of the loader. This is also explained in detail in the Webpack documentation (please poke me).

These pitch functions are not used to actually deal with the content of the Module. They are mainly used to intercept the module request, so as to meet some customized processing needs in the loader processing process. See the next document for more on pitch in action

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

  // Obtain the current loader according to loaderIndex
	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // iterate
  // If executed, skip the loader pitch function
	if(currentLoaderObject.pitchExecuted) {
		loaderContext.loaderIndex++;
		return iteratePitchingLoaders(options, loaderContext, callback);
	}

	// Load the Loader module
	// load loader module
	loadLoader(currentLoaderObject, function(err) {
		// do something ...
	});
}
Copy the code

Before each execution of pitch function, obtain the current loader (currentLoaderObject) according to loaderIndex and call loadLoader function to load this loader. CurrentLoaderObject assigns the pitch method and the normal method to the currentLoaderObject:

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

  loader.pitch = module.pitch

  loader.raw = module.raw

  callback()
  ...
}
Copy the code

When loadLoader finishes loading, the loadLoader callback is executed:

loadLoader(currentLoaderObject, function(err) {
  var fn = currentLoaderObject.pitch; // Get the pitch function
  currentLoaderObject.pitchExecuted = true;
  if(! fn)return iteratePitchingLoaders(options, loaderContext, callback); // If the loader does not provide a pitch function, skip it

  // Start executing 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.
      // Determine whether to proceed with pitch function execution according to whether any arguments are returned
      var hasArg = args.some(function(value) {
        returnvalue ! = =undefined;
      });
      if(hasArg) {
        loaderContext.loaderIndex--;
        iterateNormalLoaders(options, loaderContext, args, callback);
      } else{ iteratePitchingLoaders(options, loaderContext, callback); }}); })Copy the code

There’s a runSyncOrAsync method, which I’ll cover later, that starts executing pitch, and when pitch is done, executes the callback passed in. If there are any other parameters besides the first err parameter (which is passed to the callback function after the pitch function is executed), then the loader normal method will be executed directly. The loader execution phase will be skipped. If the pitch function returns no value, the next loader’s pitch function is executed. IteratePitchingLoaders (); loaderIndex (); loaderIndex (); iteratePitchingLoaders ();

function iteratePitchingLoaders () {... if(loaderContext.loaderIndex >= loaderContext.loaders.length)returnprocessResource(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); // Add dependencies
		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); }}Copy the code

Call the Node API readResouce inside the processResouce method to read the text content of the corresponding path to the Module, and call the iterateNormalLoaders method to start the execution phase of the Loader Normal method.

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);
	}

  // Convert between buffer and 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);
	});
}
Copy the code

Inside the iterateNormalLoaders method, the normal methods on each loader are executed in right-to-left order (as opposed to the pitch method). Loader execution of either pitch or normal methods can be synchronous or asynchronous. Async: async: async: async: async: async: async: async: async: async: async: async: async: async: async When you have finished executing the actual contents of the loader, you can call this asynchronous callback to proceed to the next loader.

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

Instead of calling this.async to asynchronize the loader, return a promise in your loader. If the promise is resolved, the next loader will be called:

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

In the process of data transfer between the upstream and downstream loaders, if the downstream loader receives a single parameter, it can return the parameter directly after the execution of the previous loader.

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

If asynchronous, call the asynchronous callback directly and pass it along (see Loader asynchronization above). If the downstream loader receives more than one parameter, the callback function provided by loaderContext needs to be called after the previous loader execution.

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

If it is asynchronous, continue to call the asynchronous callback function and pass it along (see Loader asynchronization above). The implementation mechanism involves the runSyncOrAsync method, which provides an interface for both upstream and downstream Loader calls, not mentioned above:

function runSyncOrAsync(fn, context, args, callback) {
	var isSync = true; // Whether to synchronize
	var isDone = false;
	var isError = false; // internal error
	var reportedError = false;
	// Assign async to the loaderContext to asynchronize the loader and return an asynchronous callback
	context.async = function async() {
		if(isDone) {
			if(reportedError) return; // ignore
			throw new Error("async(): The callback was already called.");
		}
		isSync = false; // The sync flag position is false
		return innerCallback;
  };
  // Callback can take the form of multiple arguments to the next 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;
			throwe; }};try {
		// Start executing loader
		var result = (function LOADER_EXECUTION() {
			returnfn.apply(context, args); } ());// If it is synchronous execution
		if(isSync) {
      isDone = true;
      // If no value is returned after loader execution, callback is executed to start the next loader execution
			if(result === undefined)
        return callback();
      // Loader returns a promise instance. The next loader is resolved or reject. This is also a way for loader to be asynchronous
			if(result && typeof result === "object" && typeof result.then === "function") {
				return result.catch(callback).then(function(r) {
					callback(null, r);
				});
      }
      // If the loader execution returns a value, the callback is executed to start the next loader execution
			return callback(null, result); }}catch(e) {
		// do something}}Copy the code

The above is the source code analysis of the loader execution process in the module construction process. If you are familiar with the loader execution rules and policies in the Webpack process, you will be more impressed with the use of WebPack Loader.