业务场景介绍
目前有以下页面:
- 普通商品详情页面
- 新人专享商品详情页面
- 秒杀商品详情页面
- 团购商品详情页面
- 满减商品详情页面
- 积分商品详情页面
- 直播商品详情页面
- 内购商品详情页面
这些页面都有共同的业务逻辑,比如展示商品信息,添加购物车,立即购买,选择规格属性,分享,等等。
存在问题
这些页面都有共同的商品数据字段,但又会存在各自不同的数据字段,但又因为不同的接口分别由不同的后端同学实现,返回的字段也不一致,比如同一个商品名称字段,可能A后端返回的数据字段是prodName,B后端返回的数据字段是productName,C后端返回的数据字段是name。 又比如说团购商品返回的数据结构是这样的:
{
startTime: '',// 团购开始时间
endTime: '', // 团购结束时间
prodName: '' // 商品名称
...
}
满减商品:
{
startTime: '',// 活动开始时间
endTime: '', // 活动结束时间
productVO: { // 商品详细数据
prodName: ''
}
...
}
又比如说: 积分商品的数据则由两个接口分别返回 A接口返回积分商品的SKU的配置信息和积分活动信息 B接口返回商品的其他信息
传统解决方案
分别建立各自的页面模块,各自实现各自的业务逻辑
缺点:
产生大量的冗余代码,而且后期维护成本非常高
中间层策略模式解决方案
什么是中间层
传统的前端中间层指的是Node层,Node层可以向server层获取数据,再通过对数据的计算整合转换成符合前端UI要求的数据格式。 另外Node层还可以做代理转发、接口聚合、数据缓存、接口限流、日志操作等等
我们现在没有Node层,但可以实现一个Model层的service模块,用来专门处理数据,统一数据格式之后交给View层,让View层更加专注于UI,即高效关注点分离,提高维护性。
什么是策略模式
策略模式的核心思想和 if else 如出一辙,根据不同的key动态的找到不同的业务逻辑。
优点:
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法 。
具体实现
流程图
定义一个商品详细数据转换服务层类中间层
/**
* 商品详细数据转换服务层
*/
class GoodsDetailService {
constructor(goodsDetailPolicy, goodsType, priceKey) {
this.goodsDetailPolicy = goodsDetailPolicy
this.goodsType = goodsType
this.priceKey = priceKey
}
/**
* 开始
* @param {Object} params api 请求参数
*/
start(params) {
this.setNavigationBarTitle()
return new Promise((resolve, reject) => {
this.goodsDetailPolicy.detailApi(params).then(r => {
this.goodsInfo = this.goodsDetailPolicy.initData ? this.goodsDetailPolicy.initData(r) : r.data
this.transform(this.goodsInfo)
resolve(this.goodsInfo)
}).catch(err => reject(err))
})
}
/**
* 设置标题
*/
setNavigationBarTitle() {
uni.setNavigationBarTitle({
title: this.goodsDetailPolicy.navigationBarTitle || '商品详情'
})
}
/**
* 数据转换
* @param {Object} goodsInfo 产品详细
*/
transform(goodsInfo) {
this.goodsDetailPolicy.transform(goodsInfo)
// 如果不是单属性,就设置多属性的SKU数据
if (!this.setIsSingleSpecification(goodsInfo)) {
goodsInfo['productAttr'] = this.normalizeSkus(goodsInfo.skus)
}
// 获取最小价格的SKU商品
let minHeap = util.minHeap(goodsInfo.skus, this.priceKey)
minHeap['price'] = minHeap[this.priceKey]
// 如果存在限量,那么库存就是限量
minHeap.actualStocks = minHeap.limits ? minHeap.limits : minHeap.actualStocks
goodsInfo['minHeap'] = minHeap
goodsInfo['price'] = minHeap.price
goodsInfo['actualStocks'] = minHeap.actualStocks
goodsInfo['marketPrice'] = minHeap.marketPrice
}
/**
* 设置否单规格
* @param {Object} goodsInfo 产品详细
*/
setIsSingleSpecification(goodsInfo) {
goodsInfo['isSingleSpecification'] = goodsInfo.skus.length === 1 && !goodsInfo.skus[0].properties
return goodsInfo.isSingleSpecification
}
/**
* SKU数据结构初始化
* @param {Object} skus
*/
normalizeSkus(skus) {
let temp = []
// 先把所有的属性拼装成一个一维数组,再通过normalizeSkus进行去重拼装成一个二维数组 MVK独有,主要可以兼容秒杀和直播商品
skus.forEach(o => {
const properties = JSON.parse(o.properties)
Object.keys(properties).forEach(v => {
temp.push({
attrName: v,
attrVal: properties[v]
})
})
})
// SKU数据结构初始化
return this.normalizeSkusToTwoDimensionalArray(temp)
}
/**
* 商品SKU数据结构构建
* @param {Array} data
*/
normalizeSkusToTwoDimensionalArray(data) {
let temp = []
data.forEach(o => {
const attrName = o.attrName
const skuVal = o.attrVal
if (temp.length > 0) {
if (temp.some(val => val.attrName === attrName)) {
temp.forEach((v, j) => {
if (v.attrName === attrName && v.attrValues.every(sv => sv.val !== o
.attrVal)) {
temp[j].attrValues.push({
val: skuVal,
isSelect: false
})
}
})
} else {
temp.push({
attrName,
attrId: attrName,
attrValues: [{
val: skuVal,
isSelect: false
}]
})
}
} else {
temp.push({
attrName,
attrid: attrName,
attrValues: [{
val: skuVal,
isSelect: false
}]
})
}
})
return temp
}
}
每个模块都需要进行共同的操作,比如设置标题,设置否单规格,SKU数据结构初始化,商品SKU数据结构构建,所以这些操作都放到共同的Service模块进行操作,另外各自不同的数据转换则放到各自的策略里进行转换操作。
定义转换策略
实现各自不同的策略转换操作,也可以说是实现不同的算法计算。
/**
* 转换策略
*/
const goodsDetailPolicy = {
[goodsTypes.GROUP_BUYING]: {
detailApi: fetchGroupBuyingGoodsDetail,
navigationBarTitle: '团购商品详情',
transform: goodsInfo => {
// 轮播图
goodsInfo['sliderImage'] = JSON.parse(goodsInfo.imgs)
goodsInfo['prodName'] = goodsInfo.productName
goodsInfo['skus'] = goodsInfo.skuList.map(o => {
o['prodName'] = goodsInfo.productName
// 团购限制一个
o['limits'] = 1
return o
})
}
},
[goodsTypes.FLASH_SALE]: {
detailApi: fetchFlashSaleGoodsDetail,
navigationBarTitle: '秒杀商品详情',
transform: goodsInfo => {
// 轮播图
goodsInfo['sliderImage'] = goodsInfo.imgs.split(',')
goodsInfo['prodName'] = goodsInfo.title
goodsInfo['skus'] = goodsInfo.seckillSkuVO
}
},
...
}
调用实现
const service = new GoodsDetailService(goodsDetailPolicy[this.goodsType], this.goodsType, this.priceKey)
service.start(params).then(r => {
this.goodsInfo = r
}).catch(err => console.log(err))
通过goodsDetailPolicy[this.goodsType]实现调用不同的转换策略,this.goodsType可以为商品类型:普通,秒杀,直播,满减,积分等。
具体案例实现解析
下面以积分商品详情的实现进行解析,因为积分商品详情几乎包含了各种情况。 首先积分商品详情需要通过两个接口返回相关的数据 api-A返回积分配置的信息,比如活动开始时间,积分配置的SKU信息 api-B返回商品的所有信息 那么我们需要的信息应该是api-A中有的数据,就取api-A中的信息,api-A没有的就取api-B中的信息 清楚需求之后,我们开始实现
定义一个转换策略
{
[goodsTypes.POINTS_MALLS]: {
detailApi: fetchPointsMallsGoodsDetail,
navigationBarTitle: '积分商品详情',
initData: data => {
},
transform: goodsInfo => {
}
},
}
虽然积分商品详情数据需要两个api提供,但在转换策略里面我们还是使用一个api作为入口
然后再api层进行两个api接口并发的请求处理,注意api层不作数据处理,让api层只专注api请求的工作
/**
* 获取积分商品详情
*
*/
export function fetchPointsMallsGoodsDetail(data) {
const pointsGoodsDetailApi = request.get(`/smsPointsMalls/${data.pointsMallsId}`, {})
const plainGoodsDetailApi = request.get('/smsFullMinus/product', { ids: data.ids })
return Promise.all([
pointsGoodsDetailApi,
plainGoodsDetailApi
])
}
请求回来的数据先在策略里进行初始化处理
[goodsTypes.POINTS_MALLS]: {
detailApi: fetchPointsMallsGoodsDetail,
navigationBarTitle: '积分商品详情',
initData: data => {
const pointsGoodsDetail = data[0].data
const plainGoodsDetail = data[1].data[0]
const pointSkus = JSON.parse(pointsGoodsDetail.sku)
// 设置最小积分和最小价格
let integral = ''
let price = ''
pointSkus.forEach(o => {
if (o.integral > integral && o.integral) integral = o.integral
if (o.price > price && o.price) price = o.price
})
const skus = pointSkus.map(o => {
o['pic'] = o.img
// 每人可以兑换数量
o['actualStocks'] = pointsGoodsDetail.personExchange
// 限购
o['limits'] = pointsGoodsDetail.personExchange
o['prodName'] = plainGoodsDetail.prodName
return o
})
// return data[0]
return {
...plainGoodsDetail,
pointsGoodsDetail,
skus,
integral,
price
}
},
transform: goodsInfo => {console.log('goodsInfo', goodsInfo)
// 轮播图
goodsInfo['sliderImage'] = JSON.parse(goodsInfo.imgs)
// goodsInfo['skus'] = JSON.parse(goodsInfo.skus)
}
}
初始化处理好数据之后,再进行各自的转换和公共层的数据转换。
总结:何时使用策略模式
根据阿里开发规约-编程规约-控制语句-第六条 :超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现。
相信大家都见过这种代码:
if (conditionA) {
逻辑1
} else if (conditionB) {
逻辑2
} else if (conditionC) {
逻辑3
} else {
逻辑4
}
这种代码虽然写起来简单,但违反了开闭原则(对扩展开放,对修改关闭):如果此时需要添加(删除)某个逻辑,那么不可避免的要修改原来的代码。
尤其是当 if-else 块中的代码量比较大时,后续代码的扩展和维护就会逐渐变得非常困难且容易出错,使用卫语句也同样避免不了以上两个问题。
根据阿里开发规约:
- if-else 不超过 2 层,块中代码 1~5 行,直接写到块中,否则封装为方法
- if-else 超过 2 层,但块中的代码不超过 3 行,尽量使用卫语句
- if-else 超过 2 层,且块中代码超过 3 行,尽量使用策略模式
总结来自阿里巴巴淘系技术《设计模式最佳套路—— 愉快地使用策略模式》本文讲述的是后端策略模式的实现,但核心思想同样适用于前端。