logo头像
Snippet 博客主题

纯前端实现sku商品特性选择

前言

  • 最近在掘金上看到有好几篇文章在讲电商sku算法问题,体验了下公司mall端对于sku商品的处理。发现每次选择一个特性值都会去请求接口,某个特性值可选和不可选是根据后端下发中的detailStatus字段决定的,如果一个商品特性特别多的情况下,页面还是明显的卡顿,效果如下图。同时也体验了淘宝页面,发现淘宝这部分功能是由前端处理,并非每次都要调用接口。

image

解决方案

测试数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 特性列表
const specList = [
{ title: "颜色", list: ["红色", "紫色"] },
{ title: "套餐", list: ["套餐一", "套餐二"] },
{ title: "内存", list: ["64G", "128G", "256G"] },
];
/**
* sku 列表
* @param {*} price: sku价格
* @param {*} count: sku库存
*/
const specCombinationList = [
{ id: "1", specs: ["紫色", "套餐一", "64G"], price: 100, count: 10 },
{ id: "2", specs: ["紫色", "套餐一", "128G"], price: 100, count: 10 },
{ id: "3", specs: ["紫色", "套餐二", "128G"], price: 100, count: 10 },
{ id: "4", specs: ["红色", "套餐二", "256G"], price: 100, count: 10 }
]

方法一:通过矩形(网上看到的一个关注度较高的方案,有bug)

  • 我们将所有的特性值枚举出来然后形成一个矩阵,交集中为1的表示2个特性值有关联,0表示2个特性值无关联,0和1是根据sku列表转换而来。
    image

  • 如果我们选择了红色,那么纵向为1的点位表示可选,0表示不可选,所以我们很容易就知道,只有套餐二和256G可选
    image

  • 当我们选择紫色和套餐二2个特性值的时候,就是2个长方形取交集(只有都是1的情况下才表示可选),我们可以很清晰的看出只有128G可以选。以此类推当我们选择3个特性值的时候实际就是3个纵向长方形取交集,并且只能取都满足1的点。看到这里我们就知道大致的原理了。

image

  • 代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取所有的特性值
const getFeatureValueIndex = () => {
return specList.reduce((total, item) => {
const { list } = item;
if(Array.isArray(list)){
list.forEach(i => {
total.push(i);
});
}
return total;
}, []);
};
// ["红色", "紫色", "套餐一", "套餐二", "64G", "128G", "256G"]
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
// 结果如下图所示
/**
* 获取每个特性值的索引
*/
const getFeatureValueIndex = () => {
return specList.reduce((total, item) => {
const { list } = item;
if(Array.isArray(list)){
list.forEach(i => {
total.push(i);
});
}
return total;
}, []);
};
/**
* 获取被选中的节点
* @param {*} arr
*/
const getChoosePoint = (arr) => {
if(!Array.isArray(arr) || arr.length <= 1){
return [];
}
let tempArr = [...arr];
return arr.reduce((total, item) => {
tempArr.shift();
if(tempArr.length >= 1){
tempArr.forEach(i => {
total = total.concat([`${item},${i}`, `${i},${item}`]);
});
}
return total;
}, []);
};
/**
* 每个被选中的组合 排列组合 获取这个组合哪些点被选中了
* @param {*} arr 每一个被选中的组合
*/
const mapIndex = (arr) => {
const list = getFeatureValueIndex();
const keys = arr.map(it => {
return list.findIndex(i => i === it);
});
return getChoosePoint(keys);
};
/**
* 二维数组中被选中的节点
*/
const getChoosedPoint = () => {
let arr = [];
specCombinationList.forEach(it => {
const { specs } = it;
arr = arr.concat(mapIndex(specs));
});
return arr;
};
// 获取矩阵(即二维数组)
const getTwoDimensionalArr = () => {
const list = getFeatureValueIndex();
let arr = [];
list.forEach((item, index) => {
arr.push([...list].fill(0));
});
const choosedPoint = getChoosedPoint();
const filterchoosedPoint = [...new Set(choosedPoint)];
filterchoosedPoint.forEach(it => {
const [fIndex, sIndex] = it.split(',');
arr[Number(fIndex)][Number(sIndex)] = 1;
});
return arr;
};

image

  • 通过以上代码我们获取了一个二维数组,当我选中红色也就是第一列(因为红色的索引为0),第一列中为1的点位表示可选,这些点可以通过映射找到其代表的特性值名称。当我们选择紫色和套餐二的时候就表示索引为1和3的2列,我们只要取这2列的交集就能获取到对应的特性值。

  • 看似完美的解决了问题,但是这种处理方式只考虑到某个特性值的关联关系,没有考虑到2个及2个以上特性值的组合关联关系。所以当我们采用以下数据的时候会存在bug,目前作者还没有找到解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 特性列表
const specList = [
{ title: "颜色", list: ["白色", "红色"] },
{ title: "内存", list: ["16G", "32G"] },
{ title: "容量", list: ["150ml", "300ml", "450ml"] }
];
/**
* sku 列表
* @param {*} price: sku价格
* @param {*} count: sku库存
*/
const specCombinationList = [
{ id: "1", specs: ["红色", "16G", "450ml"], price: 100, count: 10 },
{ id: "2", specs: ["红色", "32G", "150ml"], price: 120, count: 20 },
{ id: "3", specs: ["红色", "32G", "300ml"], price: 130, count: 30 },
{ id: "4", specs: ["红色", "32G", "450ml"], price: 110, count: 50 },
{ id: "5", specs: ["白色", "16G", "150ml"], price: 90, count: 60 },
{ id: "6", specs: ["白色", "16G", "300ml"], price: 90, count: 70 },
{ id: "7", specs: ["白色", "16G", "450ml"], price: 170, count: 80 },
{ id: "8", specs: ["白色", "32G", "150ml"], price: 80, count: 120 },
{ id: "9", specs: ["白色", "32G", "300ml"], price: 70, count: 150 },
{ id: "10", specs: ["白色", "32G", "450ml"], price: 30, count: 200 }
];

方法二:通过过滤的方式筛选节点

  • 方案一的bug是因为没有考虑到2个及2个以上特性值组合由此产生的关联,所以我们是不是可以把选中的节点作为过滤条件取筛选原sku列表,如果某个sku都满足了我们选中的特性值,那么这个sku就是默认可以被选择的。

  • 当我们选择某个特性值的时候,我们对specCombinationList进行循环,获取里面每一项中的specs中是否包含该特性值,然后将满足条件的specs进行concat,然后再进行new Set,这样就能获取到可选的特性值了,

  • 当我们选择红色的时候,我们过滤得到id为1,2,3,4这几个sku的specs中包含红色,那我们就将这些specs合并,同时去重,获取到16G、32G、150ml、300ml、450ml这几个特性值可以选择。

  • 当我们选择红色、32G的时候我们发现id为2,3,4这几个sku包含了红色且包含了32G,通过合并和去重之后获取到150ml,300ml这2个节点可选。同时我们还知道了一个价格区间以及剩余库存
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
// 具体代码如下
const [choosedValue, setChoosedValue] = useState<ChoosedValue[]>([]); // 当前被选中的特性值
// 可选的特性值列表
const filterCombinationList = useMemo(() => {
return specCombinationList?.reduce((total, item) => {
const { specs, price, count } = item;
const bingo = Array.isArray(choosedValue) ? choosedValue.every(i => specs.includes(i.value)) : false;
// 如果过滤条件符合
if(bingo){
total = {
featureValueList: [...total.featureValueList, ...specs],
priceList: [...total.priceList, price],
totalCount: total.totalCount + count
};
}
return total;
}, { featureValueList: [] as string[], priceList: [] as number[], totalCount: 0 });
}, [choosedValue]);
// 切换特性值选中/不选中
const onToggleChoose = (title: string, featureValue: string) => {
const index = choosedValue?.findIndex((it: { value: string; }) => it.value === featureValue);
if(index < 0){
setChoosedValue([...choosedValue, { title, value: featureValue }]);
} else {
choosedValue.splice(index, 1);
setChoosedValue([...choosedValue]);
}
};

总结

  • 前端现在都是由数据推动页面展示,所以对于数据结构转换等要求比之前要高很多,以前可以更多的需要对dom的api有更多了解,现在需要对数据结构以及算法要有更多的了解。

参考链接