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.