preface
Undertake above (Babel category and how to implement, interested students can poke it, webpack | loader two or three things | loader category), to continue our topic of Babel
Core: In Webpack, loader processing is based on a third-party library loader-Runner, whose module exports a function runLoaders, and Webpack passes the loader address and the callback after processing to it. It returns the result of loader processing to Webpack (the callback parameter).
Features: read the documents
In conclusion
features
- Loaders run in a Node environment, which means they can use all node apis
- Plugins can be used in conjunction with loaders
- Loaders have options to pass parameters
The target
What can you expect from reading this article
- How does Webpack handle loader
- How to implement interrupt iteration if asynchronous logic (such as request interface) exists in loader
- To implement a
babel-loader
Need to implement
-
Loader is actually a function. There will be a pitch method on loader. During execution, all pitch functions of Loader will be executed first, and then the operation of reading resources will be triggered
-
When the pitch function returns a value, the iteration of pitch is terminated and the reverse iteration of Loader is started
-
Loader supports asynchronous processing operations
-
Effect: You can perform asynchrony (such as setTimeout) in the Loader and then give the loader control to continue execution
-
Use: the context object (this) that the Loader or pitch executes has an async function that returns an innnerCallback. The loader iteration will not be executed until the user calls innerCallback
-
example
function loader(source) { let callback = this.async(); console.log(new Date()); setTimeout(() = >{ callback( null, source + "//async1")},3000)}Copy the code
-
The body of the
Stage 1: Implement the logic scenario where pitch has no return value and Loader has the same step
Look at the use of
webpack.config.js
module: {
rules: [{enforce: 'pre'.test: /\.js$/,
use: ["pre-loader"."pre-loader2"] {},test: /\.js$/,
use: ["normal-loader"."normal-loader2"] {},enforce: 'post'.test: /\.js$/,
use: ["post-loader"."post-loader2"]]}}Copy the code
Loader (every loader is like this)
function loader(source) {
console.log('pre-loader1');
return source+'//pre-loader1'
}
loader.pitch = function (){
console.log('pitch pre-loader1');
}
module.exports = loader
Copy the code
index.js
debugger;
let sum = (a,b) = > {
return a + b;
}
Copy the code
Post console print
pitch pre-loader1
pitch pre-loader1
pitch pre-loader2
pitch pre-loader2
pitch normal-loader1
pitch normal-loader1
pitch normal-loader2
Copy the code
As can be seen, the execution sequence is: Loader pitch- Loader itself
The core problem
There’s no core problem, iteration
solution
Define the entry function runLoaders that handles the loader
-
The arguments:
-
opts
Resource :path.join(__dirname, resource), // The absolute path to load resources loaders, / / loaders arrays is an absolute path readResource: fs. ReadFile. Bind (fs) / / read the file The default is readFileCopy the code
-
callback
(err,data)=>{ if(err){ console.log(err); return; } let { result: [].// index.js(entry file) file contents resourceBuffer: null.// Index.js buffer format. } = data; }Copy the code
-
-
Return :(data passed to callback)
{ result: [ 'debugger;\r\n' + 'let sum = (a,b) => {\r\n' + ' return a + b; \r\n' + '}//inline-loader2//inline-loader1//pre-loader2//pre-loader1' ], resourceBuffer: <Buffer 64 65 62 75 67 67 65 72 3b 0d 0a 6c 65 74 20 73 75 6d 20 3d 20 28 61 2c 62 29 20 3d 3e 20 7b 0d 0a 20 20 20 20 72 65 74 75 72 6e 20 61 20 2b 20 62 3b ... 3 more bytes> }Copy the code
implementation
Upload phase: 1. Integrate loader in inlineloader and config file. 2. Assembles a loaders array consisting of absolute paths to loader files
Function implementation stage:
-
Create an array of Loader objects from the loader address array
{ path: ' '.// loader absolute path query: ' '.// Query parameters fragment: ' './ / logo normal: ' './ / normal function pitch: ' './ / pitch function raw: ' '.// Is it a buffer data: ' '.// Custom objects. Each loader has a data custom object pitchExecuted: ' '.// The pitch function of the current loader has been executed and does not need to be executed normalExecuted: ' ' // The normal function of the current Loader has been executed} and there are listenersObject.defineProperty(obj,'request', { get(){ return obj.path + obj.query } } Copy the code
-
Consolidates loader’s context object as this when calling loader or pitch:
loaders // An array of loader objects context // The directory pointing to the resource to load (i.e. the absolute path to the parent folder of index.js) loaderIndex // The loader index currently being processed starts at 0 resourcePath // The resource address (i.e. the absolute path to index.js) resourceQuery // Query parameters async// is a method that sets loader execution from synchronous to asynchronous //======= the following is a definePropery form definition ==========// resource // Resource absolute address + query parameter + identifier (mostly empty) request // All loader requests are combined with resource absolute paths to! A concatenated string remainingRequest // All loader requests after the current loader are combined with the resource absolute path to! A concatenated string currentRequest // Loaders are currently processed and all loaders' requests are combined with resource absolute paths to! Concatenated string (plus itself compared to the remainingRequest) previousRequest // The absolute path of the loaded loader request to the resource! A concatenated string query // If the user has options, use options; otherwise, use the user's query. data // Data of the current loader Copy the code
-
Defines the function of the iteration pitch
/** * Iterates the patch function of loader *@param {*} Options WebPack The custom object has two parameters resourceBuffer stores the resource raw data readSource The function that reads the file *@param {*} LoaderContext Context object * of the loader@param {*} If an exception is thrown, an Err object will be passed to user CB, rather than reporting an error. This callback will execute */ during iteration of the loader function iteratePitchingLoaders(opts,loaderContext,callback) { // If patch is finished if(loaderContext.loaderIndex >= loaderContext.loaders.length){ // Then: reads the file and executes the loader function iteratively return processResource(opts,loaderContext,callback); } let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; if(currentLoaderObject.pitchExecuted){ loaderContext.loaderIndex++; } // Load the loader object, mainly three attributes of the assignment // Normal (loader itself) is retrieved by the require function // Pitch (the pitch function itself) is obtained from normal // raw (sets the data type of the raw data of the returned file, default false to true if set to buffer) loadLoader(currentLoaderObject); let pitchFunction = currentLoaderObject.pitch; currentLoaderObject.pitchExecuted = true; // If the current loader does not have a pitch function, proceed if(! pitchFunction){return iteratePitchingLoaders(opts,loaderContext,callback); } // Execute the pitch function and pass the parameters let result = fn.apply(context,[ loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data={} ]); / / recursion iteratePitchingLoaders(opts,loaderContext,callback) } Copy the code
LoaderIndex – loaderIndex- loaderIndex- loaderIndex- loaderIndex- loaderIndex-
Note, however, that this is specifically simplified because two scenarios are missing: asynchronous and pitch with a return value; They respectively represent: transfer the execution right of continuous iteration to Loader; transfer the pitch after iteration directly to the previous loader corresponding to iteration instead of the pitch after iteration; More on this later; Next, there are only two steps left in our scenario: reading the resource file and iterating the Loader
The second stage: read the resource file
Go to the processResource function
/** * After patch execution is complete, read the file first and iteratively execute loader function *@param {*} options
* @param {*} loaderContext
* @param {*} callback* /
function processResource(options,loaderContext,callback) {
loaderContext.loaderIndex--;
let resourcePath = loaderContext.resourcePath;
options.readSource(resourcePath,function(err,buffer) {
if(err){
return callback(err)
}
// resourceBuffer represents the original content of the resource
options.resourceBuffer = buffer;
iterateNormalLoaders(
options,
loaderContext,
[buffer],
callback
)
});
}
Copy the code
Stage 3: Iteration of Loader
The loader function and pitch process are similar, without further details, show the code
/** * Iterates over the loader function itself *@param {*} Options WebPack The custom object has two parameters resourceBuffer stores the resource raw data readSource The function that reads the file *@param {*} LoaderContext Context object * of the loader@param {*} Args An array * that wraps the resource's raw data@param {*} If an exception is thrown, an ERR object will be passed to the user cb instead of reporting an error */
function iterateNormalLoaders(options,loaderContext,args,callback) {
// When all loaders have been executed, a user-defined callback is executed and the read result is returned
if(loaderContext.loaderIndex < 0) {return callback(null,args);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
if(currentLoaderObject.normalExecuted){
loaderContext.loaderIndex = loaderContext.loaderIndex - 1;
return iterateNormalLoaders(options,loaderContext,args,callback);
}
let normalFn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
// Set the format of the returned data buffer or not
convertArgs(args,currentLoaderObject.raw);
runSyncOrAsync(normalFn,loaderContext,args,function(err){
if(err) return callback(err);
// Note that the first error needs to be removed
let args = Array.prototype.slice.call(arguments.1); iterateNormalLoaders(options,loaderContext,args,callback); })}Copy the code
The only thing to note is that the callback passed by the user is called at the end to return the read file resource; At this point, we go through the normal scenario loader execution.
Stage 4: Implement asynchrony
In order to achieve asynchronous processing, we can associate with THE CO library written by TJ God in KOA, applied to the recursive processing of promise, its essence is to control the next function, interested students can see this article KOA core analysis, not to see it also doesn’t matter, the principle and the implementation of this article is actually the same, mainly lies in two points
- Instead of calling the function directly, wrap it in a function to determine
- Set the switch, the default open, open the function recursive implementation of normal call, when the user invokes the async function will switch is set to false, then an iterative logic wrapped in another function, so that only the user invokes the internal function, iteration will continue to execute down, to realize the logic of “control back”
Without further ado, look at the code + comments to be clear
Changes in execution
/ / the original
// Execute the pitch function and pass the parameters
let result = fn.apply(context,[
loaderContext.remainingRequest,
loaderContext.previousRequest,
loaderContext.data={}
]);
/ / recursion
iteratePitchingLoaders(opts,loaderContext,callback)
/ / = = = = = = = = = = = = = = = =
To support asynchronous webpack, runSyncOrAsync is defined
runSyncOrAsync(
pitchFunction, // The pitch function to execute
loaderContext, // Context object
// The array of arguments to pass to pitchFunction
[
loaderContext.remainingRequest,
loaderContext.previousRequest,
loaderContext.data={}
],
function(err,args) {
// If args exists, the pitch has a return value
if(args){
loaderContext.loaderIndex--;
processResource(opts,loaderContext,callback)
}else{// Execute the pitch function of the next loader if no value is returned
iteratePitchingLoaders(opts,loaderContext,callback)
}
}
)
Copy the code
Package functionrunSyncOrAsync
/** * To support asynchron webpack defines the runSyncOrAsync function to mount on the context object. The return value of the async function is a function innerCallBack. When the innerCallBack is executed, the iteration of the pitch is executed downward. When a Loader function calls this.async() in pitch (the same applies to loader iterations), the iteration of pitch is interrupted until the user actively calls innerCallBack. The innerCallBack argument is passed to the callback *. Execute the pitch function to get the pitch return value * 2. Define the switch isSync to control synchronous asynchrony. When executed, the iteration of pitch continues * 2.2 is false (i.e. the user has called async) and the execution of the callback logic is handed to the innerCallback *@param {*} The pitch function * to be executed by fn@param {*} Context Context object *@param {*} Args the array of arguments to pass to pitchFunction *@param {*} Callback, which continues the iteration of pitch */
function runSyncOrAsync(fn,context,args,callback) {
let isSync = true; // Synchronize by default
let isDone = false; // Whether this function has been executed
Async: async: async: async: async: async: async: async: async: async: async: async: async: async: async: async: async
context.async = function(){
isSync = false;
return innerCallback;
}
// returns the call to the user before continuing
const innerCallback = context.callback = function(){
isDone = true;
isSync = false;
callback.apply(null.arguments); / / callback execution
}
/ / pitch
let result = fn.apply(context,args);
// Continue iterating if the synchronization switch is on otherwise interrupt
if(isSync){
isDone = true;
return callback.apply(null,result && [null,result]); }}Copy the code
Implementation pitch interrupts return if it has a return value
If there is a value, loaderIndex– is used, and the proceeResource function can be directly called to read the resources and iterate over the loader
loaderContext.loaderIndex--;
processResource(opts,loaderContext,callback)
Copy the code
Stage 4: Implement babel-loader
Use your own loader
The resolveLoader configuration exists in webpack.config.js, whose value is an array that defines the priority of the directory to find the Loader
resolveLoader:{
modules: ['node_modules',
path.join(__dirname,'./loaders')]},module: {
rules: [{test: /\.js$/,
use: [
{
loader: 'babel-loader'.options: {}}]},Copy the code
Start implementing babel-loader
Loader is a function, so we can export a function
// When the loader executes, this points to the loaderContext object, which has a callback method
function loader(source){... }module.exports = loader;
Copy the code
Internally, because of the Babel core library, it compiles
let babel = require('@babel/core');
// When the loader executes, this points to the loaderContext object, which has a callback method
function loader(source){
let options={
presets: ["@babel/preset-env"].// Config presets, which is a plug-in package, which contains plug-ins
sourceMap:true.// Generate the sourcemap file to debug the real source code
filename:this.resourcePath.split('/').pop() // The source file name can be displayed during code debugging
};
// New source-map file AST abstract syntax tree for es5 code after conversion
let {code,map,ast} = babel.transform(source,options);
// If Babel provides the AST abstract syntax tree, webpack will use the syntax tree provided by your loader
// You no longer need to convert code into a syntax tree yourself
/ / built in
// When this loader returns a value, it can return directly
// If you want to return multiple values callback();
return this.callback(null,code,map,ast);
}
module.exports = loader;
Copy the code
At the end
The loader should be able to address the issue of how to deal with it
Thanks for reading and hope to contribute to the community