webpack-test-include-exclude

webpack test、include、exclude

介绍

  • webpack module rules中的test、include、exclude都是针对处理当前ruleloader做范围限制的,换句话说是做范围匹配
  • loader会针对依赖图中的所有module运行匹配逻辑,如果匹配了,则用当前loader进行处理
  • 三者可单独指定,也可同时指定
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // webpack.config.js

    module.exports={

    module:{

    rules:[
    {
    test:/\.css$/, // test、include、exclude三者可单独指定,也可同时指定
    // inculde:path.join(__dirname,'./src')
    // inculde:path.join(__dirname,'./static')
    loader:'css-loader'
    }
    ]
    }
    }

相关源码

  • testincludeexclude相关的匹配规则生成源码,在RuleSet.js
  • module.rules一般我们会传递一个数组,所以其会逐条序列化rule(normalizeRule)
  • 序列化rule时,会检查是否既设置了test、include、exclude又设置了resource字段,webpack会报错,因为不允许同时设置这两个字段;https://webpack.docschina.org/configuration/module/#rule-include
  • 如果未同时设置,则会利用test、include、exclude生成一个condition对象,然后使用normalizeCondition进行序列化,normalizeCondition函数总是返回一个条件函数
  • normalizeCondition中,会判断condition对象的类型
    • 如果是string,则返回一个条件函数,此条件函数内部利用indexOf判断module路径是否以condition开头来匹配
    • 如果是function,则将传入的function做为条件函数返回
    • 如果为正则表达式,则对其的test方法先绑定上下文,然后将test 方法做为条件函数返回
    • 如果是object,则遍历condition对象keys,调用normalizeCondition生成条件函数
      • 如果发现condition对象keyor、include、test,则直接生成一个条件函数push到预先定义好的matchers数组中
      • 如果keyand,则遍历key对应的value数组,生成一个由条件函数组成的数组items,并将items传入andMatcher函数,返回一个经过andMatcher包装的条件函数并将其pushmatchers中;
        • 经过andMathcer包装后的条件函数,在执行时,要求入参必须让items中的所有条件函数都返回true才通过(且的关系);相当于所有条件函数的返回值使用&&连接了
      • 如果keynot、exclude,则生成一个条件函数,传入notMatcher中,进而返回一个被notMathcer包装后的条件函数,再将包装后的条件函数`pushmatchers`中
        • 经过notMathcer包装后的条件函数,在执行时,要求入参必须让条件函数返回false 才通过(非的关系);
    • 经过上面逻辑后,mathers数组将包含所有的条件函数,最后使用andMatcher函数,将matchers中保存的条件函数使用&&连接起来
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      // RuleSet.js

      "use strict";
      module.exports = class RuleSet {
        // ...

      // 序列化rules
        static normalizeRules(rules, refs, ident) {

      // 数组,逐条序列化
          if (Array.isArray(rules)) {
            return rules.map((rule, idx) => {
              return RuleSet.normalizeRule(rule, refs, `${ident}-${idx}`);
            });
          } else if (rules) {
            return [RuleSet.normalizeRule(rules, refs, ident)];
          } else {
            return [];
          }
        }

      // 序列化rule
        static normalizeRule(rule, refs, ident) {
         // ...

          const newRule = {};
          let useSource;
          let resourceSource;
          let condition;

          // 传递了test或include或exclude
          if (rule.test || rule.include || rule.exclude) {
            // test、include、exclude和Rule.resource是互斥的,所以这里需要检查,如果发现都设置了,则报错提示
            checkResourceSource("test + include + exclude");

            condition = {
              test: rule.test,
              include: rule.include,
              exclude: rule.exclude
            };

            try {
              // 序列化condition
              newRule.resource = RuleSet.normalizeCondition(condition);
            } catch (error) {
              throw new Error(RuleSet.buildErrorMessage(condition, error));
            }
          }
          // ...
          return newRule;
        }
        // 序列化condition,总是返回一个条件函数
        static normalizeCondition(condition) {
          if (!condition) throw new Error("Expected condition but got falsy value");
          // 如果配置数据类型为 string,那么直接使用 indexOf 作为路径匹配规则
          if (typeof condition === "string") {
            return str => str.indexOf(condition) === 0;
          }
          // 如果为 function 函数,那么使用这个开发者自己定义的 function 作为路径匹配规则
          if (typeof condition === "function") {
            return condition;
          }
          // 如果为正则表达式,则绑定上下文后,使用正则来匹配
          if (condition instanceof RegExp) {
            return condition.test.bind(condition);
          }
          // 如果condition是数组,则生成orMatcher,orMathcer是一个高阶函数,会返回一个接收str的函数;其要求str满足items中的一项即可(或的关系)
          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 +
                ")"
            );
          // condition是一个对象,则遍历对象的keys
          const matchers = [];
          Object.keys(condition).forEach(key => {
            const value = condition[key];
            switch (key) {
              // 如果key是or、include、test,则根据value值生成一个条件函数
              case "or":
              case "include":
              case "test":
                if (value) matchers.push(RuleSet.normalizeCondition(value));
                break;
              // 如果key是and,则遍历value,生成多个条件函数,并用andMatcher返回一个函数,此函数要求传入的str必须满足items中的所有条件(且的关系)
              case "and":
                if (value) {
                  const items = value.map(c => RuleSet.normalizeCondition(c));
                  matchers.push(andMatcher(items));
                }
                break;
              // not或exclude,序列化value后,生成一个notMatcher
              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 === 1return matchers[0];
          // 将上面生成好的条件函数数组,用且的关系连接起来;符合这条rule的module,就会用对应的loader做转换
          return andMatcher(matchers);
        }
      };
      function notMatcher(matcher{
        return function(str{
          return !matcher(str);
        };
      }
      function orMatcher(items{
        return function(str{
          for (let i = 0; i < items.length; i++) {
            if (items[i](str)) return true;
          }
          return false;
        };
      }
      function andMatcher(items{
        return function(str{
          for (let i = 0; i < items.length; i++) {
            if (!items[i](str)) return false;
          }
          return true;
        };
      }

解释

单独指定一个字段

  • 单独指定test
    • 只要module符合test指定的正则,就可以使用对应loader转换
  • 单独指定include
    • 只会针对include中指定的模块,会使用对应loader转换,不管是否真的能转换
  • 单独指定exclude
    • 会对exclude以外的所有模块,使用对应loader转换,不管是否真的能转换

指定两个字段

  • test + include
    • 通过源码我们知道,在normalizeCondition时,遇到test、include都是拿对应字段的value直接生成一个条件函数,然后pushmatchers
    • 但要注意在normalizeCondition的最后,他们使用andMatcher连接了,所以module必须同时满足testinclude,才能被对应loader转换
    • 所有可以说,同时指定testinclude,二者是test && include的关系
  • test + exclude
    • 因为exclude返回的条件函数是通过notMatcher包装的,所以要求module的路径必须不在exclude指定范围内,才能被对应loader转换
    • 所以同时指定test + exclude他们的关系是test && (!exclude)
  • include + exclude
    • test + exclude类似
    • 同时指定时的关系是include && (!exclude)

同时指定三个字段

  • 同时指定三个字段,跟同时指定两个字段有一定的相同;testinclude&的关系,exclude!
  • 所以同时指定时三者关系是test && include && (!exclude)

结论

  • 起初,我以为testinclude之间是的关系取并集的操作,发现错了,原来他们取的是交集
  • testincludeexclude确定范围时的关系是test && include && (!exclude)
  • 只要指定了exclude,则其匹配的module必将不会被对应loader转换

最佳实践

  • 根据官网上的文档有以下最佳实践
  • 使用test + include,避免使用exclude
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     // 这里是匹配条件,每个选项都接收一个正则表达式或字符串
    // test 和 include 具有相同的作用,都是必须匹配选项
    // exclude 是必不匹配选项(优先于 test 和 include)
    // 最佳实践:
    // - 只在 test 和 文件名匹配 中使用正则表达式
    // - 在 include 和 exclude 中使用绝对路径数组
    // - 尽量避免 exclude,更倾向于使用 include
    test: /\.jsx?$/,
    include: [
    path.resolve(__dirname, "app")
    ],
    exclude: [
    path.resolve(__dirname, "app/demo-files")
    ],

相关项目

参考