preface

1. Sort out the process from configuration to Loader. In addition, of course, to add a little source code to improve the style (I rookie, wrong also please friendly correction)

2. Files that are packaged by WebPack are converted to a module, such as import ‘./ XXX /x.jpg’ or require(‘./ XXX /x.js’). As for the actual conversion, shall it be handled by Loader

3. The following uses typescript(quit warning?) To clarify what options are available and the value types of each option

Configuring Syntax Parsing

The module properties

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',},]}}Copy the code

The above is to show the common configuration writing. Webpack writes typescript declarations for each of its options. The declaration for this module property is visible in webpack/declarations:

export interface ModuleOptions {
    // In general, the following twonoParse? :RegExp[] | RegExp | Function | string[] | string; rules? : RuleSetRules;/ / these... Deprecated, about to be deleted, don't lookdefaultRules? : 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;
}
Copy the code

NoParse is used to let WebPack skip the conversion of these files, i.e. they are not processed by the Loader (but are still packaged and exported to the dist directory).

Rules core configuration, see below

The module. The rules of attributes

The module.rules type is RuleSetRule[]. Go to webpack/declarations to check typescript for properties and property types.

Note RuleSetConditionsRecursive statement this thing in another file, Is the interface RuleSetConditionsRecursive extends Array < import (“. / declarations/WebpackOptions “). RuleSetCondition > {}, Is actually the export type RuleSetConditionsRecursive = RuleSetCondition []; Represents an array of RuleSetCondition

Meaning directly paste Chinese document: module.

Ok, so this is basically moving typescript declarations, and you’ll basically know what properties there are, what types of properties they are, and what their meanings are. The following combined with the source code to document some difficult to understand the place to add explanation.

The body of the

RuleSet

Normalization of Rule (type convergence)

It can be seen from the above that a rule object has a variety of possible attribute types, so it should be normalized to reduce a large number of typeof judgments in the underlying code. This is normalized by ruleset.js. Here is what a rule object might look like after RuleSet processing:

{resource: function(), resourceQuery: function(), compiler: function(), issuer: function(), use: [{loader: string, the options: string | object, / / the source code comments may be left over from history, the options can also be used for the object type < any > : <any>: <any>, <any>: <any>, <any>: <any>,}Copy the code

Rules and oneOf are used for nesting, and there are also canonical rule objects inside.

These four functions are used by WebPack to determine if the contents of a file need to be handed over to the Loader. Rule. Resource (‘f:/a.js’)===true if webpack meets import ‘./a.js’, rule. Resource (‘f:/a.js’)===true

The parameter ‘f:/a.js’ passed in here is what the official website says

There are two input values for a condition: Resource: The absolute path of the request file. It has been resolved according to the resolve rule. Issuer: indicates the absolute path of the module file requested the resource. Is the location of the import.

The first thing to do is to move the rule-. Loader, rule-. options, and rule-. query(deprecated, but not deleted) into the object of the rule-. This is mainly handled by static normalizeRule(rule, refs, ident) function, the code is mainly to deal with a variety of “shorthand”, the value moved to the Loader object, do some error processing, difficult not to look at it can, below pick it inside the “conditional function” normalization to say.

Rule. The resource standardization

This is a “conditional function” generated by normalizing test, include, exclude, and Resource in our configuration. Source more than 180 lines:

. 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)); }}Copy the code

Rule. Test is short for Rule. Resource. Test.

CheckResourceSource is used to check for duplicate configurations. This means that if you provide a Rule. Test option, you cannot provide rule-.resource


The RuleSet. NormalizeCondition generated a “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 (typeofcondition ! = ="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);
}
Copy the code

This string of code is basically a string, RegExp, object, function type to generate different “conditional functions”, not difficult.

NotMatcher, orMatcher, andMatcher these three auxiliary functions, see the name, the implementation is very simple, do not paste the source code. Is there any logic that you don’t understand? Just plug it in and run

Rule. The use of standardization

We will then specify the Rule. Use in the form mentioned above, so that the Loader object has only the loader and options properties (but not necessarily only these properties). 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;
}
Copy the code

These functions are a little convoluting, but generally not very difficult.

Here are some more summary of the phenomenon:

  1. loader: './loader1! ./loader2', if theRule.loaderIf more than two loaders are specified, then this cannot be setRule.optionsLoader to which loader the options should be passed
  2. -loaderDo not omit, as inbabel! ./loaderIt’s illegal because inwebpack/lib/NormalModuleFactory.jsLine 440 or so, it is no longer supported to write this waybabel-loader
  3. loader: './loader1? num1=1&num2=2'Will be processed into{loader: './loader', options: 'num=1&num=2'}In order to?The string is segmented and processed into a normalized Loader object

RuleSet normalization ends here, and interested onlookers can continue to observe the source code’s exec methods and constructors

loader

The next step is to discuss how the various loaders read our configured objects.

Options properties are passed and processed in WebPack

Loader1.js: loader1.js: loader1.js: loader1.js

module.exports = function (content){
    console.log(this)
    console.log(content)
    return content
}
Copy the code

This function is bound to a loaderContext, the official API: the Loader API.

Simply add the loader1.js file to webpack.config.js and it will print something out at compile time.

In simple terms, we can use this. Query to query the options property of the normalized Loader object. For example, {loader: ‘./loader1.js’, options: ‘num1=1&num=2’}, then this.query === ‘? Num1 = 1 & num = 2 ‘.

The question is, where did the question mark come from? What if it’s an object? Webpack through the loader – runner to perform the loader, the problem can go to a loader – runner/lib/LoaderRunner js, have so a in createLoaderObject 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);
Copy the code

And this one inside 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; }});Copy the code

In summary, if options exists and is an object, then this.query is the object; If options is a string, then this.query equals a question mark + the string

The way most Loaders read options

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

Read with loader-utils. So let’s go to loaderUtils.getOptions and see:

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

Only the key code is copied here, it is mainly to do some simple judgment, the core transformation of the string on parseQuery, here we go:

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) = = =The '-') {
        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;
}
Copy the code

Uses the JSON5 library and its own set of parameters for conversion.

In summary, as long as you are sure that you are using a Loader that uses loader-utils to retrieve options objects, you can write options as a string (often used by inline Loaders, such as import ‘loader1? a=1&b=2! . / a. s’) :

options: "{a: '1', b: '2'}"// This is not json, it is json5 format string, please turn right to Baidu options:"list[]=1&list=2[]&a=1&b=2"// Common URL parameters in HTTP requestsCopy the code

More examples can be viewed in webpack/ Loader-utils

The last

🙃 comfortable, no longer afraid of all kinds of wonderful writing encountered when looking up information