Detailed description of the whole process of Web pack loader configuration

Keywords: Webpack TypeScript Attribute JSON

Preface

1. The main purpose is to sort out the process from configuration to loading slightly. In addition, the detailed explanation of course to add some source code to enhance the style (my rookie, there are mistakes, please kindly correct)
2. The files packaged by WebPack are transformed into a module, such as import'. / x x x / X. jpg'or require('. / xxx/x.js'). As for how to transform the actual situation, it should be handed over to the loader.
3. The typescript will be used below. To facilitate the explanation of which options are available and the value types of each option

Configuration grammar parsing

Module attribute

module.exports = {
    ...
    module: {
        noParse: /jquery/,
        rules: [
            {
                test: /\.js/,
                exclude: /node_modules/,
                use:[
                    {
                        loader: './loader1.js?num=1',
                        options: {myoptions:false},
                    },
                    "./loader2.js?num=2",
                ]
            },
            {
                test: /\.js/,
                include: /src/,
                loader: './loader1.js!./loader2.js',
            },
        ]
    }
}

The above is a presentation of common configuration writings. WebPack has written typescript declarations for its options. The declaration of the module's properties is visible in the WebPack/declaration:

export interface ModuleOptions {
    // Usually the following two
    noParse?: RegExp[] | RegExp | Function | string[] | string;
    rules?: RuleSetRules;
    
    // These... have been abandoned, will be deleted soon, do not need to see
    defaultRules?: RuleSetRules;
    exprContextCritical?: boolean;
    exprContextRecursive?: boolean;
    exprContextRegExp?: boolean | RegExp;
    exprContextRequest?: string;
    strictExportPresence?: boolean;
    strictThisContextOnImports?: boolean;
    unknownContextCritical?: boolean;
    unknownContextRecursive?: boolean;
    unknownContextRegExp?: boolean | RegExp;
    unknownContextRequest?: string;
    unsafeCache?: boolean | Function;
    wrappedContextCritical?: boolean;
    wrappedContextRecursive?: boolean;
    wrappedContextRegExp?: RegExp;
}

noParse is used to let WebPack s skip the transformation of these files, that is, they will not be processed by the loader (but will still be packaged and exported to the DIST directory)
rules core configuration, see below
module.rules attribute
The module.rules type is RuleSetRule []. Please continue with the WebPack / declaration to see its typescript. What attributes are there and the attribute types are clear at a glance.
Note that RuleSetConditions Recursive is declared in another file as interface RuleSetConditions Recursive extends Array < import (". / declarations / Web packOptions"). RuleSetCondition > {} is actually export type RuleSetConditions Recursive = RuleSetConditions []; it represents a RuleSetConditions array.
Significance of direct posting of Chinese documents: module.

Well, the above is basically to move the typescript statement, combined with the document can basically know what attributes, attributes of the type and meaning. The following is a supplementary explanation of some difficult parts of the document in conjunction with the source code.

text

Rule set
Normalization of Rules (Type Convergence)
It can be seen from the above that there are many possibilities for the attribute type of a rule object, so we should standardize it and reduce a lot of typeof of code at the bottom. This is normalized by RuleSet.js. The following is a rough form of a rule object after rule set processing:

// The normalized shape of rule object should be:
{
	resource: function(),
	resourceQuery: function(),
	compiler: function(),
	issuer: function(),
	use: [
		{
			loader: string,
			options: string | object, // Source code annotations can be historical legacies, and options can also be object types
			<any>: <any>
		} // This single element, called the use array, is called the loader object. Normalized, it usually has only loader and options attributes.
	],
	rules: [<rule>],
	oneOf: [<rule>],
	<any>: <any>,
}

The adverbial clause of rules: the English of oneOf is used for nesting, and it also contains normative rule objects.
The four functions here are WebPack to determine whether the contents of the file need to be handed over to the loader. If WebPack encounters import'. / a.js', then rule.resource('f:/a.js') ==== true will send the file to the loader specified in the rule to process, resourceQuery is the same.
The afferent parameter'f:/a.js'here is what the official website says.

Conditions already have two input values:
Resource: The absolute path to the request file. It has been parsed according to the resolve rule. issuer: The absolute path to the module file of the requested resource (the requested resource). It is the position at the time of import.

The first thing to do is to move all Rule.loader, Rule.options (Rule.query is obsolete, but not deleted) into objects of elements of the Rule.use array. This is mainly handled by the static normalizeRule(rule, refs, ident) function, the code is mainly dealing with various "abbreviations", the value is moved to the loader object, do some error processing, it is not very difficult to look at it, the next pick it inside the "conditional function" standardization to say.

Rule.resource normalization
From the above we can see that this is a "conditional function", which is based on our configuration test, include, exclude, resource standardization and generated more than 180 lines of source code:

if (rule.test || rule.include || rule.exclude) {
    checkResourceSource("test + include + exclude");
    condition = {
        test: rule.test,
        include: rule.include,
        exclude: rule.exclude
    };
    try {
        newRule.resource = RuleSet.normalizeCondition(condition);
    } catch (error) {
        throw new Error(RuleSet.buildErrorMessage(condition, error));
    }
}

if (rule.resource) {
    checkResourceSource("resource");
    try {
        newRule.resource = RuleSet.normalizeCondition(rule.resource);
    } catch (error) {
        throw new Error(RuleSet.buildErrorMessage(rule.resource, error));
    }
}

In the Chinese document, Rule.resource.test is abbreviated from Rule.resource.test in English, which is actually a series of codes.
Check ResourceSource checks for duplicate configuration, which is mentioned in the document: If you provide a Rule.test option dialog, you can no longer provide Rule.

Final RuleSet.normalizeCondition generates a "conditional function" as follows:

static normalizeCondition(condition) {
    if (!condition) throw new Error("Expected condition but got falsy value");
    if (typeof condition === "string") {
        return str => str.indexOf(condition) === 0;
    }
    if (typeof condition === "function") {
        return condition;
    }
    if (condition instanceof RegExp) {
        return condition.test.bind(condition);
    }
    if (Array.isArray(condition)) {
        const items = condition.map(c => RuleSet.normalizeCondition(c));
        return orMatcher(items);
    }
    if (typeof condition !== "object") {
        throw Error(
            "Unexcepted " +
                typeof condition +
                " when condition was expected (" +
                condition +
                ")"
        );
    }

    const matchers = [];
    Object.keys(condition).forEach(key => {
        const value = condition[key];
        switch (key) {
            case "or":
            case "include":
            case "test":
                if (value) matchers.push(RuleSet.normalizeCondition(value));
                break;
            case "and":
                if (value) {
                    const items = value.map(c => RuleSet.normalizeCondition(c));
                    matchers.push(andMatcher(items));
                }
                break;
            case "not":
            case "exclude":
                if (value) {
                    const matcher = RuleSet.normalizeCondition(value);
                    matchers.push(notMatcher(matcher));
                }
                break;
            default:
                throw new Error("Unexcepted property " + key + " in condition");
        }
    });
    if (matchers.length === 0) {
        throw new Error("Excepted condition but got " + condition);
    }
    if (matchers.length === 1) {
        return matchers[0];
    }
    return andMatcher(matchers);
}

This series of code is mainly based on string, regular expressions, objects, functional types to generate different "conditional functions", it is not difficult.
notMatcher, orMatcher, and Matcher are three auxiliary functions. Look at the name, you can see that the implementation is very simple, no source code. What logic you don't understand? Just go in and run.
Normalization of Rule Use
Next, we will translate Rule.use to the form mentioned above in the specification classification, that is, let the loader retain only the object of the loader adverbial clause: options are two attributes (of course, it is not necessarily the only two attributes). The source code is as follows:

...
static normalizeUse(use, ident) {
    if (typeof use === "function") {
        return data => RuleSet.normalizeUse(use(data), ident);
    }
    if (Array.isArray(use)) {
        return use
            .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`))
            .reduce((arr, items) => arr.concat(items), []);
    }
    return [RuleSet.normalizeUseItem(use, ident)];
}

static normalizeUseItemString(useItemString) {
    const idx = useItemString.indexOf("?");
    if (idx >= 0) {
        return {
            loader: useItemString.substr(0, idx),
            options: useItemString.substr(idx + 1)
        };
    }
    return {
        loader: useItemString,
        options: undefined
    };
}

static normalizeUseItem(item, ident) {
    if (typeof item === "string") {
        return RuleSet.normalizeUseItemString(item);
    }

    const newItem = {};

    if (item.options && item.query) {
        throw new Error("Provided options and query in use");
    }

    if (!item.loader) {
        throw new Error("No loader specified");
    }

    newItem.options = item.options || item.query;

    if (typeof newItem.options === "object" && newItem.options) {
        if (newItem.options.ident) {
            newItem.ident = newItem.options.ident;
        } else {
            newItem.ident = ident;
        }
    }

    const keys = Object.keys(item).filter(function(key) {
        return !["options", "query"].includes(key);
    });

    for (const key of keys) {
        newItem[key] = item[key];
    }

    return newItem;
}

These functions are relatively roundabout, but in general, they are not very difficult.
Here are a few more phenomena to summarize:

1.loader:'. / loader 1!. / loader 2'. If Rule.loader specifies more than two loaders, then Rule.options cannot be set, because it is not known which loader to pass this option to and report the error directly.
2.-loader can not be omitted, such as babel!./loader's English is illegal, because around line 440 of webpack/lib/NormalModuleFactory.js440, no longer support this writing, direct error reporting calls you to write as babel-loader
3. loader:'. / loader 1? Num1 = 1 & num2 = 2'will be processed as {loader:'. / loader', options: `num = 1 & num = 2'} for string segmentation, and finally processed as a normalized loader object.

This concludes the normalization of rule sets, and interested executives can continue to look around at source code methods and constructors.

Loader

Next, we discuss how various loaders can read the objects we configure.
Transfer and processing options of attributes in WebPack
First, a loader simply derives a function, such as the one used in the example above.

loader1.js: 
module.exports = function (content){
    console.log(this)
    console.log(content)
    return content
}

This in this function is bound to a loaderContext (loader context), the official api: loader API.

Add this loader1.js directly to the configuration file webpack.config.js, and he will print something when compiling.

Simply put, in a loader, we can access the Normalized Loader Object Options property through this.query. For example, {loader:'. / loader 1. js', options: `num1 = 1 & num = 2'}, then this.query =='? Num1 = 1 & num = 2'}.
The question is, where does this question mark come from if it is an object?
Web Pack implements the loader through the leader of the loader. This problem can be solved by loader-runner/lib/Loader Runner.js. There is a section in the create Loader Object function:
...

...
if (obj.options === null)
    obj.query = "";
else if (obj.options === undefined)
    obj.query = "";
else if (typeof obj.options === "string")
    obj.query = "?" + obj.options;
else if (obj.ident) {
    obj.query = "??" + obj.ident;
}
else if (typeof obj.options === "object" && obj.options.ident)
    obj.query = "??" + obj.options.ident;
else
    obj.query = "?" + JSON.stringify(obj.options);

In the runLoaders function:

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

In summary, this.query is the object when the selection exists and is an object; if the option is a string, then this.query is equal to a question mark + the string.

The Method of Reading Options for Most Loaders

const loaderUtils=require('loader-utils')
module.exports = function (content){
    console.log(loaderUtils.getOptions(this))
    return content
}

With the help of utils, then go to loaderUtils.getOptions and see:

const query = loaderContext.query;
if (typeof query === 'string' && query !== '') {
  return parseQuery(loaderContext.query);
}
if (!query || typeof query !== 'object') {
  return null;
}
return query;

Here we only copy the key code. It mainly makes some simple judgments. The core of the string is converted to parseQuery. Next, look at:

const JSON5 = require('json5');
function parseQuery(query) {
  if (query.substr(0, 1) !== '?') {
    throw new Error(
      "A valid query string passed to parseQuery should begin with '?'"
    );
  }
  query = query.substr(1);
  if (!query) {
    return {};
  }
  if (query.substr(0, 1) === '{' && query.substr(-1) === '}') {
    return JSON5.parse(query);
  }
  const queryArgs = query.split(/[,&]/g);
  const result = {};
  queryArgs.forEach((arg) => {
    const idx = arg.indexOf('=');
    if (idx >= 0) {
      let name = arg.substr(0, idx);
      let value = decodeURIComponent(arg.substr(idx + 1));

      if (specialValues.hasOwnProperty(value)) {
        value = specialValues[value];
      }
      if (name.substr(-2) === '[]') {
        name = decodeURIComponent(name.substr(0, name.length - 2));
        if (!Array.isArray(result[name])) {
          result[name] = [];
        }
        result[name].push(value);
      } else {
        name = decodeURIComponent(name);
        result[name] = value;
      }
    } else {
      if (arg.substr(0, 1) === '-') {
        result[decodeURIComponent(arg.substr(1))] = false;
      } else if (arg.substr(0, 1) === '+') {
        result[decodeURIComponent(arg.substr(1))] = true;
      } else {
        result[decodeURIComponent(arg)] = true;
      }
    }
  });
  return result;
}

It uses json5 library and its own set of parameters conversion.
In summary, as long as you can make sure that the loader you use gets the option object through loader utils, you can directly write the options as the following string (commonly used in inline loader, such as import 'loader1? A = 1 & B = 2!. / A.js'):

options: "{a: '1', b: '2'}" // Non-json, json5 format string, slightly different, please turn right Baidu

options: "list[]=1&list=2[]&a=1&b=2" // Common url parameter parts in http requests

More examples can be seen in WebPack / utils

Posted by macmonkey on Wed, 25 Sep 2019 02:08:04 -0700