前言
商城一直都是平台研发类的高频项目,也存在诸多含金量与难度非常大的功能点,比如购物车模块、支付模块、装修模块、商品模块、以及很多与业务相关的模块,主导此类复杂需求的开发与落地不仅可以升职加薪,也是面试中的展示肌肉的时刻,
所以今天,我和大家分享一个在正式项目中沉淀出来的Vue3版发布商品 - 构建sku的思路以及具体实现方案;附带源码与案例,点赞收藏不迷路,接下来进入正文~
源码:https://github.com/BlueDancers/vue3-sku-demo/blob/main/src/views/goods/add/index.vue
案例:https://bluedancers.github.io/vue3-sku-demo
什么是SKU
在开始正文之前,首先要做一次扫盲,那就是我们说的电商项目中的SKU,究竟是什么?
SKU的全称是Stock Keeping Units,我们可以理解为商家用于管理商品库存和销售的一种方式。
每个 SKU 对应着一个若干属性的组合,例如一个商品存在颜色、尺寸、款式等属性。商家可以根据商品的不同属性设置不同的 SKU,并对每个 SKU 进行价格、库存若干信息的管理。
举个例子,假设商家有一件衬衫商品,有红色、蓝色两种颜色、S、M、L 三种尺码可选。那么该商品便存在 6 个 SKU ,分别为:
- 红色 S 号
- 红色 M 号
- 红色 L 号
- 蓝色 S 号
- 蓝色 M 号
- 蓝色 L 号
在这里我们就要明确一下概念了,颜色、尺码都是我们的商品属性中的销售属性,而生成的 红色 S 号 红色 M 号 等等 就是我们的SKU。
再让我们在看看某宝的SKU选择弹窗
这个商品的销售属性:尺码(6个) 颜色分类(6个),那么通过销售属性,最终将会生成6*6=36个SKU,而用户选择任意尺码 + 颜色分类的搭配都可以匹配到具体的价格与库存等信息。
以上提到的功能点,就设计到电商后台的商品模块的SKU构建知识点,接下来让我们看看,如何使用Vue3构建SKU。
具体实现
构建销售属性
通过上面的案例,我们得知了SKU并非凭空捏造出来的,而是由销售属性动态生成。
那么根据某宝的SKU信息,我们反向推导一下,他的销售属性的数据结构可能是这样
1 2 3 4 5 6
| type skuAttrItemType = { title: string values: { attributeValue: string }[] }
|
如果,不考虑SKU的图片,我们的每个销售属性的数据结构都这样,存在一个销售属性的名称,以及若干个属性值。
我们继续观察某宝,发现无论如何修改尺码,白色的图片都是同一张,不会随着尺码的变化而变化,因此我们可以推断出,sku的图片是由着销售属性进行设置的;
另外还要注意一个细节,一个商品无论存在多少个销售属性,最终只能为其中一个销售属性设定图片,所以我们优化一下我们的销售属性数据结构,增加销售属性图片的字段。
1 2 3 4 5 6 7 8
| type skuAttrItemType = { title: string isAddImage: boolean values: { attributeValue: string thumbnailUrl?: string }[] }
|
于是,我们便可以得出我们案例的数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let skuAttrItemType = [ { title: '尺码', isAddImage: false, values: [ { attributeValue: 'S' }, { attributeValue: 'M' }, ], }, { title: '颜色分类', isAddImage: true, values: [ { attributeValue: '白色', thumbnailUrl: '...白.png' }, { attributeValue: '黑色', thumbnailUrl: '...黑.png' }, ], }, ]
|
这样的数据结构便满足了我们客户端渲染商品SKU,用户可以将不同的尺码与颜色分类进行搭配,但是我们目前仅能实现SKU的选择,还无法满足用户选择任意尺码 + 颜色分类的搭配后,立刻得知价格的场景。
这也是我们下一步需要解决的问题,就是基于销售属性构建商品SKU。
使用笛卡尔积算法实现商品SKU的构建
基于以上销售属性,我们的目标是构建一个如下的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| let sku = [ { attributeValue: 'S,白色', thumbnailUrl: '...白.png', }, attributeValue: 'S,黑色', thumbnailUrl: '...黑.png', }, { attributeValue: 'M,白色', thumbnailUrl: '...白.png', }, { attributeValue: 'M,黑色', thumbnailUrl: '...黑.png', }, ]
|
SKU的生成是存在明确的运算规则的,销售属性的属性名称的数量的乘积等于SKU的数量。
比如我们上线的销售属性是 两个尺码 两个颜色分类,则生成的SKU数量2 * 2 = 4个
假如我们存在三个尺码 三个颜色分类,则生成的SKU数量3 * 3 = 9个
假如我们再多一个销售属性 两个尺码 三个颜色 四个风格,则生成的SKU数量为 2 * 3 * 4 = 24个
我们程序如何实现以上逻辑呢?大部分小伙伴面对这样的诉求的第一反应应该都是递归,因为销售属性的数量是未知的,写死循环实现是不现实的,不过在SKU生成上,我们一般使用更加简单的笛卡尔积算法。
笛卡尔积:笛卡尔乘积是指在数学中,两个集合X和Y的笛卡尓积,又称直积,表示为X × Y,第一个对象是X的成员而第二个对象是Y的所有可能有序对的其中一个成员 。
接下来,我们就来实现这一部分的逻辑
sku的生成是实时的,销售属性的变化会引发sku的变化,所以我们需要监听销售属性的变化,这里我们通过watch进行实现
笛卡尔积本身不复杂,熟练了解reduce即可,如果有点忘记了,请去MDN复习一下~
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
| type skuAttrItemType = { title: string isAddImage: boolean 是否上传图片(限制最多只能打开一个) values: { attributeValue: string thumbnailUrl?: string }[] }
watch( () => skuAttributes.value, (value) => { if (value.length) { generateSku(deepClone(value)) } }, { deep: true } )
function generateSku(skuAttribute: skuAttrItemType[]) { let attrValue: any[] = [] skuAttribute.map((item) => attrValue.push(item.values)) let skus: any[] = [] skus = attrValue.reduce((col: any[], set) => { let res: any[] = [] col.forEach((c) => { set.forEach((s) => { let t = c.attributeValue + ',' + s.attributeValue res.push({ attributeValue: t, thumbnailUrl: c.thumbnailUrl || s.thumbnailUrl || '' }) }) }) return res }) stockKeepUnits.value = skus }
|
经过以上的代码,我们变化销售属性,就会得出以下的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| let sku = [ { attributeValue: 'S,白', thumbnailUrl: '...白.png', }, { attributeValue: 'S,黑', thumbnailUrl: '...黑.png', }, { attributeValue: 'M,白', thumbnailUrl: '...白.png', }, { attributeValue: 'M,黑', thumbnailUrl: '...黑.png', }, ]
|
这便实现了我们的sku算法,无论是多么复杂的销售属性,都可以通过该函数,输出符合预期的SKU。
这时候有同学要说了,哎,我SKU的价格,库存等等属性呢?
这还不简单?赋值之前再循环一遍,增加字段即可。
1 2 3 4 5 6 7 8 9
| skus.map((e: skuType) => { e.price = '' e.marketPrice = '' e.stock = '' e.specificationBarCode = '' return e })
stockKeepUnits.value = skus
|
最终我们得到了这样的结果
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
| let sku = [ { attributeValue: 'S,白', thumbnailUrl: '...白.png', price: '', marketPrice: '', stock: '', specificationBarCode: '', }, { attributeValue: 'S,黑', thumbnailUrl: '...黑.png', price: '', marketPrice: '', stock: '', specificationBarCode: '', }, { attributeValue: 'M,白', thumbnailUrl: '...白.png', price: '', marketPrice: '', stock: '', specificationBarCode: '', }, { attributeValue: 'M,黑', thumbnailUrl: '...黑.png', price: '', marketPrice: '', stock: '', specificationBarCode: '', }, ]
|
数据准备妥当后,接下来我们就可以渲染表格了,这都是前端基操,我就不做过多赘述,有兴趣的朋友,可以看看源码以及我精心为大家准备的案例。
实现SKU数据缓存
如果大家仔细想以上代码,会发现一个问题
假设,运营人员在发布商品的时候漏填了一个销售属性,尺码L,但是这时候运营已经填写好了SKU表格中的信息,如果这时候运营想增加字段,根据我们上面的代码,会触发skuAttributes.value的watch,进而运行generateSku,开始重新构建SKU,导致运营人员之前的数据全部被重置了。
而以上提到的场景是项目实际运营期间非常常见的场景,那么有没有办法,可以实现SKU的变动,不影响已经填写好的SKU呢。
实现起来其实也很简单,那就是,我们保存每一次生成的SKU的副本,然后在下一次SKU重新构建的时候,对比副本,再回填信息。
思路如下:
- 实时保存SKU的副本
- SKU重新构建的时候对比副本
- 销售规格一致的SKU回填副本数据
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
| let afterSku: skuType[] = []
watch( () => stockKeepUnits.value, (value) => { afterSku = deepClone(value) }, { deep: true } )
function generateSku(skuAttribute: skuAttrItemType[]) { skus.map((e: skuType) => { let old = afterSku.find((item) => item.attributeValue == e.attributeValue) e.id = old == null ? '' : old.id e.price = old == null ? '' : old.price e.marketPrice = old == null ? '' : old.marketPrice e.stock = old == null ? '' : old.stock e.specificationBarCode = old == null ? '' : old.specificationBarCode return e }) stockKeepUnits.value = skus }
|
当然也可以是其他回填规则,比如按照下标的方式回填,这就看具体业务的要求了,基于副本我们便完成了SKU的变动后数据的缓存功能。
最后
我们vue3版本的商城项目的SKU核心实现到此就全部结束了,如果你想了解全部代码,请点击这里,如果你想测试案例,请点击这里
作为一名Vue3开发者,你可能对这个专栏感兴趣
Pinia 源码分析专栏
Vue3 硬核源码解析系列
写给前端nginx教程