浅析webpack-merge源码

merge方法

webpack-merge 类库的 merge 方法基于 lodash 类库的 mergeWith 方法实现。

mergeWith 方法用于遍历数组项或对象属性,通过尾参 customizer 定制化数据处理函数,获得新的数组项或对象属性。customizer 函数的参数为 (objValue, srcValue, key, object, source, stack)。

webpack-merge 通过 join-arrays 模块导出的 joinArrays 函数将用户配置项 { customizeArray, customizeObject } 对象构建为 customizer 函数生成器。customizer 函数的处理逻辑为:当 objValue, srcValue 为函数时,该函数的返回值由 customizer 函数再次处理。当 objValue, srcValue 为数组时,通过 customizeArray 函数处理,参数为(objValue, srcValue, key);若 customizeArray 函数未定义,合并数组项。当 objValue, srcValue 为对象时,通过 customizeObject 函数处理,若无返回值或 customizeObject 函数未定义,构建新的key值,调用 joinArrays 函数将 { customizeArray, customizeObject } 配置项转化成内层数据处理函数,并对 objValue, srcValue 的子孙属性深度处理。除此而外,深拷贝 source 或者将 source 作为返回值。特别的,当 customizeArray, customizeObject 函数均未作配置时,customizer 函数作深拷贝处理。

export default function joinArrays({
  customizeArray,
  customizeObject,
  key
} = {}) {
  return function _joinArrays(a, b, k) {
    const newKey = key ? `${key}.${k}` : k;

    if (isFunction(a) && isFunction(b)) {
      return (...args) => _joinArrays(a(...args), b(...args), k);
    }
    if (isArray(a) && isArray(b)) {
      const customResult = customizeArray && customizeArray(a, b, newKey);

      return customResult || [...a, ...b];
    }

    if (isPlainObject(a) && isPlainObject(b)) {
      const customResult = customizeObject && customizeObject(a, b, newKey);

      // 第二层以同样的逻辑处理,如 'module.rules'等
      return customResult || mergeWith({}, a, b, joinArrays({
        customizeArray,
        customizeObject,
        key: newKey
      }));
    }

    if (isPlainObject(b)) {
      return cloneDeep(b);
    }

    return b;
  };
}

在此基础上,webpack-merge 实现了 merge 方法。该方法支持单参数,通过接受{ customizeArray, customizeObject } 对象,返回值用于对数据(格式可以是数组,或多参数)做处理;也支持多参数,数据处理模式即为深拷贝。


multiple 方法

multiple 方法在 merge 方法的基础上构建,首先通过 merge 方法将配置数据复合为一,然后通过 lodash.values 方法以数组形式返回复合对象各属性的值。

var path = require('path');
var baseConfig = {
    server: {
      target: 'node',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'lib.node.js'
      }
    },
    client: {
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'lib.js'
      }
    }
  };

// specialized configuration
var production = {
    client: {
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash].js'
      }
    }
  }

// output
[{
  target: 'node',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'lib.node.js'
  }
},{
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[hash].js'
  }
}]


unique 方法

unique 方法通过 lodash 类库的 differenceWith 方法实现。differenceWith 方法接受参数为 (array, values, comparator),通过遍历 array 数组项,并调用 comparator 函数将数组项和 values 数组比较,将返回否值的数组项构建成新的数组。unique 方法用于构建 customizeArray 配置函数,且其接受参数为 (key, uniques, getter),key 即属性名,uniques, getter 参数用于构建 differenceWith 方法中的参数 comparator。若getter返回值已存在 uniques 中,comparator 返回真值;否则返回否值,即该数组项拷贝到新数组中。

function mergeUnique(key, uniques, getter = a => a) {
  return (a, b, k) => (
    k === key && [
      ...a,
      ...differenceWith(
        b, a, item => uniques.indexOf(getter(item)) >= 0
      )
    ]
  );
}


// unique 方法使用
// 需要注意的是,若首个 plugins 中没有 HotModuleReplacementPlugin 构造函数
// 第二个 plugins 中的HotModuleReplacementPlugin 也不会拷贝到新数组中
const output = merge({
  customizeArray: merge.unique(
    'plugins',
    ['HotModuleReplacementPlugin'],
    plugin => plugin.constructor && plugin.constructor.name
  )
})({
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}, {
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
});


strategy 方法

strategy 方法在 merge 方法基础上实现,通过配置项 rules 构建 { customizeArray, customizeObject } 对象,最终生成 customizer 数据处理函数。rules 以对象形式配置,定义了数据处理策略,其中,'prepend' 为反向深拷贝,'replace' 替换,默认为 'append' 即正向深拷贝。与 merge 方法相同,rules 可以设置深度嵌套属性的处理规则,如 'module.rules': 'prepend'。

const mergeStrategy = (rules = {}) => merge({
  customizeArray: customizeArray(rules),
  customizeObject: customizeObject(rules)
});

function customizeArray(rules) {
  return (a, b, key) => {
    switch (rules[key]) {
      case 'prepend':
        return [...b, ...a];
      case 'replace':
        return b;
      default: // append
        return false;
    }
  };
}

function customizeObject(rules) {
  return (a, b, key) => {
    switch (rules[key]) {
      case 'prepend':
        return mergeWith({}, b, a, joinArrays());
      case 'replace':
        return b;
      default: // append
        return false;
    }
  };
}


smart 方法

smart 方法为处理 module.rules 配置项而设计。首先提取 module.rules 数组,交由 lodash 模块的 unionWith 方法处理。unionWith 方法接受参数为 (...array, compator),comparator(arrVal, othVal) 函数返回 false 时保留两个数组项,返回真值只保留 arrVal。特别的,comparator 执行过程中,可对引用对象 arrVal 进行再度插值操作,使其具有 othVal 的特性。为此,webpack-merge 类库构造了 uniteRules 函数对 module.rules 数组项进行处理。

const mergeSmart = merge({
  customizeArray: (a, b, key) => {
    if (isRule(key.split('.').slice(-1)[0])) {
      return unionWith(a, b, uniteRules.bind(null, {}, key));
    }

    return null;
  }
});

uniteRules 函数接受参数为 {rules, key, newRule, rule},其中 rules 用于配置 module.rules 合并策略,如 { rules.use: 'prepend' | 'append' | 'replace' },key 即 'rules',newRule, rule 为待合并的两个加载器配置项。uniteRules 函数的处理逻辑为:首先比较 newRule, rule 的 test, include, exclude, enforce, query 查询字符串,是否跳过合并;其次,将 newRule.loader 配置项赋给rule,作为返回值,因为无论 'prepend' | 'append' | 'replace' 策略,都将返回 newRule.loader;其次将 rule.loader 转换为 [{ loader }]数组,同时newRule.use, rule.use 也转换为 [{ loader }] 数组,该数组同名 loader 将会被合并。

function uniteRules(rules, key, newRule, rule) {
  if (String(rule.test) !== String(newRule.test)
      || ((newRule.enforce || rule.enforce) && rule.enforce !== newRule.enforce)
      || (newRule.include && !isSameValue(rule.include, newRule.include))
      || (newRule.exclude && !isSameValue(rule.exclude, newRule.exclude))) {
    return false;
  } else if (!rule.test && !rule.include && !rule.exclude
      && (rule.loader && rule.loader.split('?')[0]) !== (newRule.loader && newRule.loader.split('?')[0])) {
    // Don't merge the rule if there isn't any identifying fields and the loaders don't match
    return false;
  } else if ((rule.include || rule.exclude) && (!newRule.include && !newRule.exclude)) {
    // Don't merge child without include/exclude to parent that has either
    return false;
  }

  // newRule.loader should always override
  if (newRule.loader) {
    const optionsKey = newRule.options ? 'options' : newRule.query && 'query';

    delete rule.use;
    delete rule.loaders;
    rule.loader = newRule.loader;

    if (optionsKey) {
      rule[optionsKey] = newRule[optionsKey];
    }
  } else if ((rule.use || rule.loaders || rule.loader) && (newRule.use || newRule.loaders)) {
    const expandEntry = loader => (
      typeof loader === 'string' ? { loader } : loader
    );
    // this is only here to avoid breaking existing tests
    const unwrapEntry = entry => (
      !entry.options && !entry.query ? entry.loader : entry
    );

    let entries;

    // 将 { loader } 转化成 [{ loader, option?, query? }]数组
    if (rule.loader) {
      const optionsKey = rule.options ? 'options' : rule.query && 'query';
      entries = [{ loader: rule.loader }];

      if (optionsKey) {
        entries[0][optionsKey] = rule[optionsKey];
      }

      delete rule.loader;

      if (optionsKey) {
        delete rule[optionsKey];
      }
    } else {
      // rule.use, rule.loaders 数组项若为字符串,转化为 { loader } 对象
      entries = [].concat(rule.use || rule.loaders).map(expandEntry);
    }
    const newEntries = [].concat(newRule.use || newRule.loaders).map(expandEntry);

    const loadersKey = rule.use || newRule.use ? 'use' : 'loaders';
    const resolvedKey = `${key}.${loadersKey}`;

    switch (rules[resolvedKey]) {
      case 'prepend':
        rule[loadersKey] = [
          ...differenceWith(newEntries, entries, uniteEntries),
          ...entries
        ].map(unwrapEntry);
        break;
      case 'replace':
        rule[loadersKey] = newRule.use || newRule.loaders;
        break;
      default:
        rule[loadersKey] = unionWith(
          // Remove existing entries so that we can respect the order of the new
          // entries
          differenceWith(entries, newEntries, isEqual),
          newEntries,
          uniteEntries
        ).map(unwrapEntry);
    }
  }

  if (newRule.include) {
    rule.include = newRule.include;
  }

  if (newRule.exclude) {
    rule.exclude = newRule.exclude;
  }

  return true;
}

function uniteEntries(newEntry, entry) {
  const loaderNameRe = /^([^?]+)/ig;

  const [loaderName] = entry.loader.match(loaderNameRe);
  const [newLoaderName] = newEntry.loader.match(loaderNameRe);

  if (loaderName !== newLoaderName) {
    return false;
  }

  // Replace query values with newer ones
  mergeWith(entry, newEntry);
  return true;
}


smartStrategy 方法

smartStrategy 方法不同于 smart 方法只处理 module.rules 配置项,smartStrategy 方法对整个 webpack 配置进行处理。其中,对 module.rules 配置项,smartStrategy 方法处理逻辑雷同 smart 方法,其他配置项则同 strategy 方法。针对 module.rules 配置项,合并规则 rules 须配置为 { 'module.rules': 'prepend' | 'replace' | 'append', 'module.rules.use': 'prepend' | 'replace' | 'append' } 形式。'module.rules' 属性设定 module.rules 合并策略,'module.rules.use' 属性设定 rule.use 合并策略。

const mergeSmartStrategy = (rules = {}) => merge({
  customizeArray: (a, b, key) => {
    const topKey = key.split('.').slice(-1)[0];

    if (isRule(topKey)) {
      switch (rules[key]) {
        case 'prepend':
          return [
            ...differenceWith(b, a, (newRule, seenRule) => (
              uniteRules(rules, key, newRule, seenRule, 'prepend'))
            ),
            ...a
          ];
        case 'replace':
          return b;
        default: // append
          return unionWith(a, b, uniteRules.bind(null, rules, key));
      }
    }

    return customizeArray(rules)(a, b, key);
  },
  customizeObject: customizeObject(rules)
});


备注:webpack-merge类库主要应用于webpack配置,对于roadhog等类库需要注入babelPlugins、babelPresets配置的,稍显鞭长莫及。


## 参考文档


[lodash官方文档](lodash.com/docs)


[webpack-merge仓库](github.com/survivejs/we)

编辑于 2018-02-03