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 ababel-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

  1. Instead of calling the function directly, wrap it in a function to determine
  2. 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