I’m trying to look at the source code in a more straightforward way, and the code in the following examples is simplified code, which the text description corresponds to, reducing complexity to simplicity. First the bones, then the fat.

1. What is webpack-loader

Webpack can only understand JavaScript and JSON files, and Loader enables WebPack to process other types of files and convert them into valid modules for use by applications and to be added to dependency diagrams.

2. Loader configuration and default configuration

How and where is WebPack Loaders configured and is there a default configuration?

  • Configure module.rules in 2.1 webpack.config.js
  • Before 2.2 in webpack. Js created in compiler object through WebpackOptionsDefaulter. Js initialization when the default configuration will merge (merge cli and webpack. Config. Js) after the increase in the configuration items The module. DefaultRules;
// ... webpack.js
if (Array.isArray(options)) {} 
else if (typeof options === "object") {
  options = new WebpackOptionsDefaulter().process(options); The process method comes from the WebpackOptionsDefaulter parent class OptionsDefaulter
  compiler = new Compiler(options.context);
  compiler.options = options;
} 

/ / WebpackOptionsDefaulter class from webpack/lib/WebpackOptionsDefaulter. Js is as follows:
class WebpackOptionsDefaulter extends OptionsDefaulter { 
  constructor () {
    // ...
    // Loader default configuration includes JS, json
    this.set("module.defaultRules"."make".options= >[{type: "javascript/auto".resolve: {}}, {test: /.mjs$/i, type: "javascript/esm".resolve: { mainFields: 'example'}}, {test: /.json$/i, type: "json" },
        { test: /.wasm$/i, type: "webassembly/experimental"}]);// ... other default cfg}}Copy the code

Consider: How do these configurations work with Loader?

3. Execute part of loader

3.1 Resolve of WebPack Loader

Webpack.config. js only needs to be configured with a name. How does Webpack find the Loader module by name

The webpack search for loader is to enter the compilation stage, and the compilation entry is in the Compiler run method. This happens after the Compiler instance (new Compiler()) is created, and the Compiler.run method is called to initialize the compilation object.

The initial compilation pair requires the creation of two special objects: NormalModuleFactory and ContextModuleFactory instances, of which NormalModuleFactory is the key to this problem. NormalModuleFactory generates instances of NormalModule for webPack source modules, and goes through the necessary procedures before creating an instance of NormalModule. This process involves using the Loader resolver to resolve the loader path according to the loader name and related configurations.

3.2 Module. rules in webpack.config.js

What process did the module.rules configuration in webpack.config.js go through to take effect?

Webpack combines the webpack.config.js configuration item (modules.rules) with the default configuration item (options.defaultrules) to create RuleSet instances, This process also occurs when creating an instance of NormalModuleFactory; Sample code:

class NormalModuleFactory extends Tapable {
   constructor(context, resolverFactory, options) {
        // ...
        this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules))
   }
}
Copy the code

RuleSet instances act as filters. By calling their exec methods and passing in information about the current request, RuleSet instances return loader data for that request. That is, the loader result set matching the module configuration in webpack.config.js.

The exec call occurs in a callback after resolving the inline-loader; Sample code:

asyncLib.parallel([/* Handles inline-loader, which is the path information */].(err, results) = > {
    const result = this.ruleSet.exec({
        resource: resourcePath,
        realResource: matchResource ! = =undefined
        ? resource.replace(/? . * /."")
        : resourcePath,
        resourceQuery,
        issuer: contextInfo.issuer,
        compiler: contextInfo.compiler
    });
})
Copy the code

3.3 Inline-loader and Normal Loader

In addition to webpack.config.js loader configuration can also use inline-loader, inline-loader execution or normal loader which has a higher priority?

Here is an example of an inline-loader:

/ / sample:
import Styles from 'style-loader! css-loader? modules! ./styles.css';

/ / webpack processing the inline - loader code: webpack/lib/NormalFactory js - > constructor - > this. Hooks. The resolver. Tap (handler
let elements = requestWithoutMatchResource
    .replace(/ ^ -? ! +/."")
    .replace(/!!!!! +/g."!")
    .split("!");
Copy the code

Two Loaders are obtained after processing: style-loader and CSS-loader. After obtaining the two loaders, use loader-resolver and module resolver to resolve the path of the two Loaders and module./style. CSS. AsyncLib. Parallel (resolve); / / asyncLib. Parallel (resolve); / / asyncLib.

  1. The first item is the path and module of the inline-loader obtained by parsing.
  2. The second item is the module path information. In this callback, the configured loader and inline-loader will be sorted. Example:
// webpack/lib/NormalFactory.js -> constructor -> this.hooks.resolver.tap( hanlder
asyncLib.parallel(
    [
      callback= > this.resolveRequestArray(contextInfo, context, elements, loaderResolver, callback), // Resolve the inline-loader path
      callback= > normalResolver.resolve(contextInfo, context, resource, {}, (err, resource, resourceResolveData) = > { // Resolve the module path
        callback(null, {resourceResolveData,resource })
      })
    ],
    (err, results) = > {
        // Inline-loader after resolve
        let loaders = results[0]})Copy the code

3.4 Enforce Adjusts the loader priority

Configure Enforce pre or post to define the loader type as pre-loader or post-loader (if no normal loader is configured), and then adjust the execution sequence of the loader. How is this implemented?

After getting the loader result set in webpack.config.js, webpack will divide loader into three groups according to whether pre/ POST is configured. UseLoaderPost useLoaders, useLoaderPre. Then resolve the path information of these three loader modules. After resolve is completed, sort all loaders. The execution sequence is: pre, normal, inline, post

  • The simplified code is as follows:
// webpack/lib/NormalModuleFactory.js -> constructor
this.hooks.resolver.tap("NormalModuleFactory".() = > (data, callback) = > {
    / /...
    asyncLib.parallel([/* Handles inline-loader, which is the path information */].(err, results) = > {
        let loaders = results[0]; // inline-loaders

        const result = this.ruleSet.exec({});// This is a configuration Loader result set combining webpack.config.js with the current request information
    
        // Start grouping
        const useLoadersPost = [];
        const useLoaders = [];
        const useLoadersPre = [];
        for (const r of result) {
            if (r.type === "use") {
                if (r.enforce === "post" && !noPrePostAutoLoaders) {
                    useLoadersPost.push(r.value);
                } else if (
                    r.enforce === "pre"&&! noPreAutoLoaders && ! noPrePostAutoLoaders ) { useLoadersPre.push(r.value); }else if (
                    !r.enforce &&
                    !noAutoLoaders &&
                    !noPrePostAutoLoaders
                ) {
                    useLoaders.push(r.value);
                }
            } 
        }
        // Start resolve then end group:
        asyncLib.parallel(
            [
                this.resolveRequestArray.bind(/* other args,*/ useLoadersPost,loaderResolver),
                this.resolveRequestArray.bind(/* other args,*/ useLoaders,loaderResolver),
                this.resolveRequestArray.bind(/* other args,*/ useLoadersPre, loaderResolver)
            ],
            (err, results) = > {
                if (matchResource === undefined) {
                    // results [useLoaderPost, useLoaders, useLoadersPre]
                    // loaders inline-loaders
                    // Final result: post-loader, inline-loader, normal-loader, and pre-loader
                    loaders = results[0].concat(loaders, results[1], results[2]);
                } 
                process.nextTick(() = > {
                    callback(null, {
                        context: context,
                        request: loaders.map(loaderToIdent).concat([resource]).join("!"), // The loader request has been merged
                        dependencies: data.dependencies,
                        userRequest,
                        rawRequest: request,
                        loaders, // All loaders sorted
                        resource,
                        matchResource,
                        resourceResolveData,
                        settings,
                        type,
                        parser: this.getParser(type, settings.parser),
                        generator: this.getGenerator(type, settings.generator), resolveOptions }); }); })})})Copy the code

3.5 run loaders

At the end of the previous section 3.4, the callback is called in process.nextTick, passing an object containing the consolidated loaders. So where is callback passed in? What happened in there? Callback is in NormalModuleFactory. Perform this constructor. Hooks. Factory. Tap (” NormalModuleFactory “, handler), Passed in when resolver is called in the factory function returned by its handler.

  • The simplified code is as follows:
// webpack/lib/NormalModuleFactory.js
class NormalModuleFactory extends Tapable {
    constructor(context, resolverFactory, options) {
        this.hooks.factory.tap("NormalModuleFactory".() = > (result, callback) = > {
            let resolver = this.hooks.resolver.call(null);
            resolver(result, (err, data) = > { 
                /* This function is resolver's callback and is used to receive consolidated loaders */
                this.hooks.afterResolve.callAsync(data, (err, result) = > {
                  createdModule = new NormalModule(result); // webpack/lib/NormalModule.js
                  return callback(null, createdModule); })})this.hooks.resolver.tap("NormalModuleFactory".() = > (data, callback) = >{})}}Copy the code

Ignore the subplots and in this callback, creates NormalModule instance, through the instance creation module, during this period will be NormalModule. Prototype. The build () method, This method does the work of run-loaders, parsing ast analysis dependencies, and so on. Our focus is still on run-loaders, and the build method will call the doBuild method. The doBuild method first creates the loaderContext object, which provides many capabilities for developing a loader, and then executes the Loader by calling the runLoaders method, which comes from a separate library: loader-runner

  • Simplified code:
// webpack/lib/NormalModule.js
class NormalModule extends Module {
    constructor({type, request, userRequest, rawRequest, loaders, resource, matchResource, parser, generator, ... }) {
      Loaders (rawRequest); // This constructor argument looks familiar from loaders (rawRequest)
    }
    build(options, compilation, resolver, fs, callback) {
      return this.doBuild(options, compilation, resolver, fs, err= >{})}doBuild(options, compilation, resolver, fs, callback) {
        const loaderContext = this.createLoaderContext(resolver, options, compilation,fs);
        
        runLoaders(
            {
                resource: this.resource,
                loaders: this.loaders,
                context: loaderContext,
                readResource: fs.readFile.bind(fs)
            },
            (err, result) = >{})}createLoaderContext(resolver, options, compilation, fs) {
        const requestShortener = compilation.runtimeTemplate.requestShortener;
        const getCurrentLoaderName = () = > {};
        const loaderContext = {
            version: 2.emitWarning: warning= > {},
            emitError: error= > {},
            getLogger: name= > {},
            exec: (code, filename) = > {},
            resolve(context, request, callback) {},
            getResolve(options) {},
            emitFile: (name, content, sourceMap, assetInfo) = > {},
            rootContext: options.context,
            webpack: true.sourceMap:!!!!!this.useSourceMap,
            mode: options.mode || "production"._module: this._compilation: compilation,
            _compiler: compilation.compiler,
            fs: fs
        };
        returnloaderContext; }}Copy the code

3.6 The inside story of loader-Runner

The runLoaders method exported in loader-Runner is used to execute the loader. The runLoaders method receives an options object and callback function containing the loaderContext and loaders collection. The runLoader method does the following:

  1. Default parameter processing for important parameters such as loaderContext;
  2. Extend the properties and capabilities of loaderContext, whereloaderIndexBe added at this time, the ability of extension for example addDependency/adContextDependency method;
  3. DefineProperty redefines the value (get) and set behavior of some loaderContext attributes for ease of use.
  4. Enter the loaderpitchStage;
// node_modules/loader-runner/lib/LoaderRunner.js

exports.runLoaders = function runLoaders(options, callback) {
   / / sample 1
   var loaderContext = options.context || {};
   loaders = loaders.map(createLoaderObject);
  
    / / sample 2
   loaderContext.loaderIndex = 0;
   loaderContext.addContextDependency = function addContextDependency(context) {
      contextDependencies.push(context);
   };
   
    // Example: 3
   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("!"); }});// Enter the piching phase
   iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {});
};
Copy the code

3.7 loader. The pitch & normal & loaderContext. LoaderIndex

It is well known that the execution order of loader is exactly the opposite of the order in which we define loader. That is, if the definition order is A -> B -> C, the execution order is C -> B -> A. This is the sequence of the normal execution stages of the loader. Each loader run phase consists of 2 parts, pitching and normal; The pitching phase executes the pitch method defined on the Loader (pitch is optional).

Before the loader executes, there is a loadLoader method call that loads the loader modules and assigns the loader itself to loader.normal and the pitch function to loader.pitch. After the separation, will perform the loader. The pitch, if not return result of pitch, loading a loader is handed to go, this process can maintain loaderContext loaderIndex accumulation, so this process is a sequence, Until the loaderIndex > = loaderContext. Loaders. Length out of recursion to perform normal loader; Normal: loaderIndex– loaderIndex– loaderIndex <= 0 loaderIndex <= 0 loaderIndex <= 0 loaderIndex <= 0 loaderIndex <= 0 loaderIndex <= 0 loaderIndex <= 0

Of course, if you return something in one of the pitch functions, it will pass over the rest of the loader and go straight to the normal execution of the already parsed loader

  • An example loader with a pitch
// some-loader.js
module.exports = function (cnt) { return someSyncOps(cnt, this.data.value )}

// Define a pitch
module.exports.pitch = function (remainRequest, precedingRequest, data) {
  data.value = 42
}
Copy the code
  • Example of simplified code for pitch/normal:
// node_modules/loader-runner/lib/LoaderRunner.js

exports.runLoaders = function runLoaders(options, callback) {
    // Enter the piching phase
   iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {});
};

function iteratePitchingLoaders(options, loaderContext, callback) {
   // Terminate the recursion after the last loader is loaded and enter the normal phase
   if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);

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

   // pitchExecuted to true after the previous loader pitch, maintain loaderIndex++, recursively next
   if(currentLoaderObject.pitchExecuted) {
      loaderContext.loaderIndex++;
      return iteratePitchingLoaders(options, loaderContext, callback);
   }

   // Load the Loader module (grouping)
   loadLoader(currentLoaderObject, function(err) {
      var fn = currentLoaderObject.pitch; 
      currentLoaderObject.pitchExecuted = true;
        // If there is no pitch directly recurse next:
      if(! fn)return iteratePitchingLoaders(options, loaderContext, callback);

        // The smart guys have noticed that there is no procedure called. What about the call process? In runSyncOrAsync
      runSyncOrAsync(
         fn,
         loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
         function(err) {
            var args = Array.prototype.slice.call(arguments.1);
            if(args.length > 0) {
                    // Args is the result returned when the pitch is called. If the pitch is not empty, the loader terminates and enters the normal phase
               loaderContext.loaderIndex--;
                    // See function iterateNormalLoaders below
               iterateNormalLoaders(options, loaderContext, args, callback);
            } else {
                    // After successfully executing one pitch, recurse to the next pitchiteratePitchingLoaders(options, loaderContext, callback); }}); }); }function iterateNormalLoaders(options, loaderContext, args, callback) {
    // loaderIndex < 0
    // The run loader can be terminated by setting loaderIndex to a negative value
   if(loaderContext.loaderIndex < 0) return callback(null, args);

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

   LoaderIndex - / / maintenance
   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);

   runSyncOrAsync(fn, loaderContext, args, function(err) {
        // A loader that executes successfully executes the callback recursively executes the next one
      iterateNormalLoaders(options, loaderContext, args, callback);
   });
}

// node_modules/loader-runner/lib/loadLoader.js
module.exports = function loadLoader(loader, callback) {
    var module = require(loader.path);
    loader.normal = typeof module= = ="function" ? module : module.default;
    loader.pitch = module.pitch;
}
Copy the code

3.8 Sequence Control of Asynchronous Loaders

The order of synchronous loaders is guaranteed by the order of execution. How does Webpack ensure that loaders are still executed in the expected order when using asynchronous loaders?

The runSyncOrAsync method is used to run loader.normal or loader.pitch, which handles the asynchronous management process. The core implementation implements asynchronous serial by maintaining the isSync identifier with the extended loaderContext.async() method. LoaderContext is bound to this in the loader function. Loadercontext. async is the same as this.async in the asynchronous loader.

The async method returns a callback, which is called when the asynchronous logic in the async loader is complete. This callback is intended to tell Webpack that the async loader has run out of time.

  • Simplify the code
// node_modules/loader-runner/lib/LoaderRunner.js
function runSyncOrAsync(fn, context, args, callback) {
    var isSync = true;
    context.async = function async() {
        isSync = false;
        return innerCallback;
    };
    
    var innerCallback = context.callback = function() {
      isSync = false;
      callback.apply(null.arguments);
    };
    
    var result = (function LOADER_EXECUTION() {
        // fn is the loader function and applies apply to bind loaderContext to this
        returnfn.apply(context, args); } ());if(isSync) {
      isDone = true;
      if(result && typeof result === "object" && typeof result.then === "function") {
        return result.then(function(r) {callback(null, r); }, callback); }return callback(null, result); }}Copy the code