背景
最近在整理重构前辈们留下的项目,发现项目中有一处代码,我有点疑问。
这段代码应该是想把 vue 项目路由的参数 query 合并更新。不想删掉老的参数,同时增加或者替换新的参数值。
我的疑问其实是前辈们为啥要用 merge (import merge from 'webpack-merge') , 而不用 Object.assign() , 或者用对象的解构 query = { ...this.$route.query, search: this.input, current_page: 1}。
在我印象中,merge
是使用在 webpack 配置中的,用来合并配置参数之类的。
此时处在了似懂非懂之间,有点尴尬,索性就好好学一遍 webpack-merge,探个究竟!感兴趣的就跟着我一起往下看。
简介
为 Webpack 设计的合并
webpack-merge 提供了一个 merge
函数,它连接数组并合并对象以创建一个新对象。如果遇到函数,它将执行它们,通过算法运行结果,然后再次将返回值包装在函数中。
这种行为在配置 webpack 时特别有用,尽管它还有其他用途。每当你需要合并配置对象时,webpack-merge 就能派上用场。(联想自己可能的应用场景)
语法
默认语法
js
merge(...configuration | [...configuration])
merge
是API的核心,也是最重要的思想。通常这就是你所需要的,除非你想进一步定制。
js
const { merge } = require('webpack-merge');
// 默认 API
const output = merge(object1, object2, object3, ...);
// 可以直接传递对象数组。
// 这适用于所有可用的函数
const output = merge([object1, object2, object3]);
// 与右侧匹配的 Keys 优先
const output = merge(
{ fruit: "apple", color: "red" },
{ fruit: "strawberries" }
);
console.log(output);
// { color: "red", fruit: "strawberries"}
Tips:不支持 Promise
分析:
如果 merge 的两个对象中的键值对:
- 数据类型不一样,后面完全覆盖前面;
- 如果两者都是基础数据类型,后面覆盖前面。
- 如果两者都是数组,就会把两个数组进行合并
- 如果两者都是对象,键值对规则同上
js
// 数据类型不一样
let dog = {
color: {},
};
let cat = {
color: '#000',
};
console.log(merge(dog, cat)); // {color: '#000'}
// 两者都是基础数据类型
let dog = {
color: 'yellow',
};
let cat = {
color: '#000',
};
console.log(merge(dog, cat)); // {color: '#000'}
// 如果两者都是数组
let dog = {
habit: ['run', 'eat'],
};
let cat = {
habit: ['sleep', 'eat'],
};
console.log(merge(dog, cat)); // {habit: ['run', 'eat', 'sleep', 'eat']}
// 完整示例
let dog = {
color: 'yellow',
habit: ['run', 'eat'],
behavior: {
character: 'lively',
age: 7,
},
};
let cat = {
color: '#000',
habit: ['sleep', 'eat'],
behavior: {
character: 'cute',
age: 8,
},
};
console.log(merge(dog, cat));
// {
// color: '#000',
// habit: [ 'run', 'eat', 'sleep', 'eat' ],
// behavior: { character: 'cute', age: 8 }
// }
自定义语法
js
mergeWithCustomize({ customizeArray, customizeObject })(...configuration | [...configuration])
如果您需要更大的灵活性,可以按字段自定义 merge
行为,如下所示:
js
const { mergeWithCustomize } = require('webpack-merge');
const object1 = {
foo1: ['object1'],
foo2: ['object1'],
bar1: { object1: {} },
bar2: { object1: {} },
}
const object2 = {
foo1: ['object2'],
foo2: ['object2'],
bar1: { object2: {} },
bar2: { object2: {} },
}
const output = mergeWithCustomize(
{
customizeArray(a, b, key) {
if (key === 'extensions') {
return _.uniq([...a, ...b]);
}
// 回退到默认合并
return undefined;
},
customizeObject(a, b, key) {
if (key === 'module') {
// 自定义合并
return _.merge({}, a, b);
}
// 回退到默认合并
return undefined;
}
}
)(object1, object2, ...);
上述代码运行后,则将为 Array
类型的每个属性调用 customizeArray
,即:
js
customizeArray(["object1"], ["object2"], "foo1");
customizeArray(["object1"], ["object2"], "foo2");
和 customizeObject
将被调用为 Object
类型的每个属性,即:
js
customizeObject({ object1: {} }, { object2: {} }, bar1);
customizeObject({ object1: {} }, { object2: {} }, bar2);
上述这部分看懂了吗?如果没看懂,好好看一下,直到看懂为止。
customizeArray
和 customizeObject
为 mergeWithCustomize
提供了小策略。它们支持字段名的 append
、 prepend
、 replace
和通配符。
js
const { mergeWithCustomize, customizeArray, customizeObject } = require('webpack-merge');
const output = mergeWithCustomize({
customizeArray: customizeArray({
'entry.*': 'prepend'
}),
customizeObject: customizeObject({
entry: 'prepend'
})
})(object1, object2, object3, ...);
unique
js
`unique(<field>, <fields>, field => field)`
unique
是一种策略,用于强制配置中的唯一性。当你想确保只有一个插件时,它对插件最有用。
第一个 <field>
是用于查找重复项的配置属性。
第二个 <fields>
表示在每个副本上运行 field => field 函数时应该唯一的值。
当第一配置中的 <field>
的元素的顺序不同于第二配置中的顺序时,保留第二配置。
js
const { mergeWithCustomize, unique } = require("webpack-merge");
const output = mergeWithCustomize({
customizeArray: unique(
"plugins",
["HotModuleReplacementPlugin"],
(plugin) => plugin.constructor && plugin.constructor.name,
),
})(
{
plugins: [new webpack.HotModuleReplacementPlugin()],
},
{
plugins: [new webpack.HotModuleReplacementPlugin()],
},
);
// 输出现在只包含一个 HotModuleReplacementPlugin
// 并且它将是最后一个插件实例。
mergeWithRules
为了支持高级合并需求(即在加载器中合并), mergeWithRules
包含了额外的语法,允许您匹配字段并应用策略进行匹配。考虑下面的完整示例:
js
const a = {
module: {
rules: [
{
test: /\.css$/,
use: [{ loader: "style-loader" }, { loader: "sass-loader" }],
},
],
},
};
const b = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: "style-loader",
options: {
modules: true,
},
},
],
},
],
},
};
const result = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: "style-loader",
options: {
modules: true,
},
},
{ loader: "sass-loader" },
],
},
],
},
};
assert.deepStrictEqual(
mergeWithRules({
module: {
rules: {
test: "match",
use: {
loader: "match",
options: "replace",
},
},
},
})(a, b),
result,
);
它的工作方式是,您应该使用与您的配置结构匹配的 match
(或 CustomizeRule.Match
,如果您使用 TypeScript )来注释字段,然后使用特定的策略来定义应如何转换特定字段。如果匹配项不存在于规则之上,则它将自动应用该规则。
支持的注释:
match
(CustomizeRule.Match
)-可选的匹配器,根据相似性将合并行为限制在特定部分(想想DOM或jQuery选择器)append
(CustomizeRule.Append
)-双列直插式项目prepend
(CustomizeRule.Prepend
)-预先准备项目replace
(CustomizeRule.Replace
)-替换项目merge
(CustomizeRule.Merge
)-合并对象(浅合并)
支持使用 TypeScript
webpack-merge 支持开箱即用的 TypeScript。你应该像下面这样把 webpack 中的 Configuration
type传递给它:
js
import { Configuration } from "webpack";
import { merge } from "webpack-merge";
const config = merge<Configuration>({...}, {...});
源码分析
都到 webpack-merge 的家门口,不进去看看,心有不甘啊,走,感兴趣的一起去看看源码的大概实现。
因源码要占用的文章篇幅挺多,就不在此粘贴,自行打开,对照下边的解释辅助阅读源码即可。
index.ts
1、源码地址第一部分(300行左右):github.com/survivejs/w...
该模块提供了一套用于合并配置对象的工具函数,支持自定义合并规则,并且具有一些用于数组和对象合并的辅助函数。具体的如下:
导入的模版作用:
wildcard
: 从 "wildcard" 模块导入,该模块用于通配符匹配。mergeWith
: 从相对路径 "./merge-with" 导入,这是一个自定义的合并函数。joinArrays
: 从相对路径 "./join-arrays" 导入,是用于合并数组的函数。unique
: 从相对路径 "./unique" 导入,是用于获取唯一值的函数。CustomizeRule
,CustomizeRuleString
,ICustomizeOptions
,Key
: 从 "./types" 导入一些类型定义。isPlainObject
,isSameCondition
,isUndefined
: 从 "./utils" 导入一些辅助函数。
函数 merge
:
- 接受一个或多个配置对象,并将它们合并为一个新的配置对象。
- 使用了
mergeWithCustomize
函数。
函数 mergeWithCustomize
:
- 接受一个
options
对象,并返回一个函数,该函数用于合并配置对象。 - 使用了
mergeWith
函数,其中包含一些自定义的配置选项。(后边再说这个函数)
函数 customizeArray
:
- 接受一个规则对象,返回一个函数,该函数用于自定义数组的合并规则。
类型 Rules
:
- 用于描述合并规则的类型,规定了一些自定义规则的格式。
函数 mergeWithRules
:
- 接受一个
Rules
对象,返回一个用于合并配置对象的函数,该函数使用了mergeWithCustomize
。
函数 mergeWithRule
:
- 用于处理特定的合并规则,包括
Match
、Append
、Merge
、Prepend
、Replace
等。
函数 mergeIndividualRule
:
- 用于处理单个规则的合并,根据不同的规则类型进行不同的操作。
函数 customizeObject
:
- 接受一个规则对象,返回一个函数,该函数用于自定义对象的合并规则。
merge-with.ts
2、源码第二部分(26行代码):github.com/survivejs/w...
这段源代码主要实现了一个合并对象数组的功能,允许通过自定义函数 customizer
对每个键值进行合并操作。具体如下:
mergeWith
函数:
- 接受一个对象数组
objects
和一个自定义合并函数customizer
。 - 通过解构赋值获取数组的第一个元素
first
和剩余的元素rest
。 - 使用
forEach
遍历rest
数组,依次调用mergeTo
函数进行合并。 - 返回合并后的结果。
mergeTo
函数:
- 接受两个对象
a
和b
,以及一个自定义合并函数customizer
。 - 创建一个空对象
ret
,用于存储合并结果。 - 使用
Object.keys
获取a
和b
的所有键,并通过concat
合并为一个数组。 - 遍历合并后的键数组,对每个键调用
customizer
函数,获取合并后的值,并将结果存储在ret
对象中。 - 如果
customizer
返回undefined
,则使用a
对象中相应键的值。 - 返回合并后的对象
ret
。
join-arrays.ts
3、源码第三部分(60行代码):github.com/survivejs/w...
这个模块提供了一个可定制的数组合并函数,通过参数传递不同的定制规则,可以对数组的合并行为进行个性化配置。这样的设计使得合并逻辑更加灵活,并允许在特定场景下定制化处理。
导出 joinArrays
函数:
- 接受一个参数对象,包含
customizeArray
、customizeObject
、和key
。 - 返回一个函数
_joinArrays
,这是实际执行数组合并的函数。
函数 _joinArrays
:
- 接受两个数组
a
和b
,以及一个键k
。 - 如果提供了
key
参数,则创建新的键newKey
,否则使用原有的键k
。 - 如果
a
和b
都是函数,则返回一个新的函数,该函数调用_joinArrays
对两个函数的结果进行合并。 - 如果
a
和b
都是数组,且提供了customizeArray
函数,则调用customizeArray
并返回其结果,否则直接合并两个数组。 - 如果
b
是正则表达式,则直接返回b
。 - 如果
a
和b
都是普通对象,且提供了customizeObject
函数,则调用customizeObject
并返回其结果,否则使用mergeWith
合并两个对象,递归调用joinArrays
处理嵌套数组。 - 如果
b
是普通对象,则使用cloneDeep
对b
进行深度克隆。 - 如果
b
是数组,则创建一个新数组,包含b
中的所有元素。
unique.ts
4、源码第四部分(24行代码):github.com/survivejs/w...
这部分的目的是在合并数组时,根据指定的键保持唯一性。它通过使用 Set
来追踪已有的键值,对合并后的数组进行处理,确保指定的键值在合并后的数组中是唯一的。
函数 mergeUnique
:
- 接受三个参数:
key
: 一个字符串,表示用于判断唯一性的键。uniques
: 一个字符串数组,表示要保持唯一性的键的集合。getter
: 一个函数,接受对象a
并返回一个字符串,表示如何获取a
对象中的键值。
- 返回一个合并函数,这个函数用于实际执行合并,并保持指定键的唯一性。
合并函数:
- 接受两个数组
a
和b
,以及一个键k
。 - 判断
k
是否等于传入的key
。 - 如果等于,执行以下步骤:
- 将
uniques
转换为一个Set
,以便快速检查某个键是否在唯一性集合中。 - 将
a
和b
合并成一个数组。 - 对每个对象执行以下操作:
- 使用
getter
函数获取对象的键值。 - 创建一个新对象,包含
key
和value
两个属性,其中key
为获取的键值,value
为原对象。 - 如果
uniquesSet
中已经存在该键值,则将key
设置为键值,否则仍然使用原对象的键值。
- 使用
- 将上述处理后的对象数组转换为一个
Map
对象,以键值对的形式存储。 - 通过
Map
的值获取新的对象数组。
- 将
- 返回新的对象数组。
对比 Object.assign
Object.assign()
静态方法将一个或者多个源对象 中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。
语法
js
Object.assign(target, ...sources)
js
let dog = {
color: 'yellow',
habit: ['run', 'eat'],
behavior: {
character: 'lively',
age: 7,
},
};
let cat = {
color: '#000',
habit: ['sleep', 'eat'],
behavior: {
character: 'cute',
age: 8,
},
};
console.log(Object.assign(dog, cat));
// {
// color: '#000',
// habit: [ 'sleep', 'eat' ],
// behavior: { character: 'cute', age: 8 }
// }
Tips:小技巧:如果只是想将两个或多个对象的属性合并到一起,不改变原有对象的属性,可以用一个空的对象作为target对象。
js
Object.assign({}, target, source);
更多的使用技巧
更多的 Object.assign 使用方法规则,可移步到 MDN
算了,估计大家都懒得去移步。我还是把好东西拿过来,再次和大家一起学习一下吧,因为我看了,发现真的很好,有的都是平时没有注意到的东西,确实好,所以下面就 copy 过来,好好学一下。
js
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// Expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget === target);
// Expected output: true
// 注意这里
假如源对象是一个对象的引用,它仅仅会复制其引用值。
js
const obj1 = { a: 0, b: { c: 0 } };
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 0, b: { c: 0 } }
obj1.a = 1;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 0, b: { c: 0 } }
obj2.a = 2;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 2, b: { c: 0 } }
obj2.b.c = 3;
console.log(obj1); // { a: 1, b: { c: 3 } }
console.log(obj2); // { a: 2, b: { c: 3 } }
// 深拷贝
const obj3 = { a: 0, b: { c: 0 } };
const obj4 = JSON.parse(JSON.stringify(obj3));
obj3.a = 4;
obj3.b.c = 4;
console.log(obj4); // { a: 0, b: { c: 0 } }
js
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };
const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 },目标对象本身发生了变化
js
const o1 = { a: 1 };
const o2 = { [Symbol("foo")]: 2 };
const obj = Object.assign({}, o1, o2);
console.log(obj); // { a : 1, [Symbol("foo")]: 2 } (cf. bug 1207182 on Firefox)
Object.getOwnPropertySymbols(obj); // [Symbol(foo)]
js
const obj = Object.create(
// foo 在 obj 的原型链上
{ foo: 1 },
{
bar: {
value: 2, // bar 是不可枚举的属性
},
baz: {
value: 3,
enumerable: true, // baz 是可枚举的自有属性
},
},
);
const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
js
const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo");
const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 基本类型将被封装,null 和 undefined 将被忽略。
// 注意,只有字符串封装对象才拥有可枚举的自有属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
js
const target = Object.defineProperty({}, "foo", {
value: 1,
writable: false,
}); // target.foo 是一个只读属性
Object.assign(target, { bar: 2 }, { foo2: 3, foo: 3, foo3: 3 }, { baz: 4 });
// TypeError: "foo" is read-only
// 这个异常会在给 target.foo 赋值的时候抛出
console.log(target.bar); // 2,第一个源对象成功复制。
console.log(target.foo2); // 3,第二个源对象的第一个属性也成功复制。
console.log(target.foo); // 1,异常在这里被抛出
console.log(target.foo3); // undefined,属性赋值已经结束,foo3 不会被复制
console.log(target.baz); // undefined,第三个源对象也不会被复制
js
const obj = {
foo: 1,
get bar() {
return 2;
},
};
let copy = Object.assign({}, obj);
console.log(copy);
// { foo: 1, bar: 2 }
// copy.bar 的值是 obj.bar 的 getter 的返回值。
// 这是一个将完整描述符复制的赋值函数
function completeAssign(target, ...sources) {
sources.forEach((source) => {
const descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
// 默认情况下,Object.assign 也会复制可枚举的 Symbol 属性
Object.getOwnPropertySymbols(source).forEach((sym) => {
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
}
copy = completeAssign({}, obj);
console.log(copy);
// { foo:1, get bar() { return 2 } }
场景
webpack.config.js
js
const commonConfig = { ... };
const productionConfig = { ... };
const developmentConfig = { ... };
module.exports = (env, args) => {
switch(args.mode) {
case 'development':
return merge(commonConfig, developmentConfig);
case 'production':
return merge(commonConfig, productionConfig);
default:
throw new Error('No matching configuration was found!');
}
}
项目中的真实使用场景,大家也可以看看自己的 webpack 项目,是否有类似的。