使用策略模式展示页面状态
孙泽辉 Lv5

页面状态复杂时,我总是会用策略模式来提升代码可读性,并尽量减轻心智负担,最近我按照我的思路,抽象出来一个操作类,在项目里用着不错,分享出来。

思路

先讲一下思路吧,之前写的状态大概是:

1
2
3
4
5
6
7
8
const stateMap = {
'0': '活动未开始',
'1': '活动进行中',
'2': '活动已结束',
}
const typeFormServer = 1
const showText = stateMap[typeFromServer]
console.log(showText) // 活动进行中

此外,还有需要展示不同的dom样式的问题

1
2
3
4
5
6
7
8
9
10
11
12
const stateMap = {
'0': {
text: '活动未开始',
style: 'unstart'
}
// ...
}
const btnStyle = stateMap[0].style // unstart
/**
* 在 vue 中使用
* <Button :class="{[btnStyle]: true}"></Button>
*/

诚然上面解决了我的问题,但是项目中很多页面都有这种状态信息展示,管理起来异常繁琐,可能某天心情好 stateMap 直接用数组,下标当key了,又可能我只关心样式,将style当key,总之我需要定义一种规则,保证代码简单可阅读。

所以我写出一个 KeyMap 类统一管理这些状态。

写法上我希望是这种:
不再以某种(后端返回的type或前端style)当key,传入数组,KeyMap 内部为每个字段都编制成key,方便按照value/sign字段检索。

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
const keyMap = new KeyMap([
{
text: '待接单',
sign: 'Pending',
value: 1,
},
{
text: '进行中',
sign: 'Processing',
value: 2,
},
{
text: '已完成',
sign: 'Completed',
value: 3,
}
])

// 通过sign/value字段查询
keyMap.getValue({sign: 'Pending'}) // 1
keyMap.getText({value: 2}) // 进行中
keyMap.getSign({value: 2}) // Processing

// 传入 value 判断是否在 sign 中
keyMap.includes(['Pending', 'Processing'], 0) // true

keyMap.includes(['Pending', 'Processing'], 3) // true

实现

在constructor里,初始化所有属性

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
export class KeyMap {
private map: MapType[]
public sign!: any
constructor(map: MapType[]) {
this.map = map
this.initValue()
this.initSign()
}

// 通过 value 编制, 直接挂到KeyMap实例上
private initProperties() {
this.map.forEach((v) => {
this[v.value] = {
text: v.text,
sign: v.sign
}
})
}
// 通过 sign 编制, 保存到sign成员变量
private initSign() {
this.sign = this.map.reduce((acc, cur) => {
return Object.assign(acc, {
[cur.sign]: {
text: cur.text,
value: cur.value,
},
})
}, {})
}
}

下面是查询方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

class KeyMap {
// ...
public getText({ value, sign }: Partial<TextQuery>): string | number {
// 优先使用sign查询
if (sign)
return get(this, ['sign', sign, 'text'], '')
return get(this, [value ?? '', 'text'], '')
}
public getSign({ value }: Partial<TextQuery>): string | number {
return get(this, [value ?? '', 'sign'], '')
}
public getValue({ sign }: Partial<TextQuery>): string | number {
return get(this, ['sign', sign ?? '', 'value'], '')
}
// 判断是否包含
public includes(signs: (string | number)[], value: MaybeRef<string | number>) {
const allValue = signs.map(v => this.getValue({ sign: v }))
return allValue.includes(unref(value))
}
}

用起来自我感觉良好,分享一下我的使用经验

展示状态文本

1
2
3
4
5
6
7
const orderType = ref(0)
onMounted(async ()=> {
orderType.value = await loadType()
})
const orderStateText = computed(() => {
return keyMap.getText({ value: unref(orderType) })
}) // 进行中

v-if 根据状态区别展示元素

1
2
3
4
5
6
7
8
<template>
<div
v-if="orderType === keyMap.getValue({ sign: 'Reject' })"
class="reject-card">
<span class="card-title">拒绝原因</span>
<span class="value">时间与其他任务冲突,无法接单 </span>
</div>
</template>

某些按钮只能在Pending、Processing、Completed状态中展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div
v-if="keyMap.includes(['Pending', 'Processing', 'Completed'], orderType)" class="page-action">
<div class="btn-group">
<template v-if="orderType === keyMap.getValue({ sign: 'Pending' })">
<button class="btn cancel" @click.stop="cancelOrder">
取消点单
</button>
</template>
<template v-if="orderType === keyMap.getValue({ sign: 'Processing' })">
<button class="btn" @click.stop="ensureOrder">
确认完成
</button>
</template>
<template v-if="orderType === keyMap.getValue({ sign: 'Completed' })">
<button class="btn" @click="goEvaluate">
前往评价
</button>
</template>
</div>
</div>
</template>

另外,如果要展示状态的副标题或者其他信息,可以在每一条状态中多加一个rest,只需在init时将reset也保存,再添加一条getRest方法即可。

我这里为了简单,为了实现开闭原则也很简单,init时候将所有字段都存上,再添加一个createGetter方法,传好获取路径的回调函数,此时这个KeyMap便是对添加开放,无需修改了。

也就是说:

1
2
3
4
5
6
7
8
9
10
11
// KeyMap.ts
private initProperties() {
this.map.forEach((v) => {
const { value, ...other } = v
// 除了value字段都存上
this[value] = other
})
}
public createGetter(getter: (keyMap: KeyMap) => any) {
return value => getter(this, value)
}

而 createGetter 使用方法

1
2
3
4
5
// 自定义获取方法
const getPersonType = keyMap.createGetter((keyMap, value) => {
return get(keyMap, value, 'PersonType')
})
getPersonType(0) // 'Pending'
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Total words 85.5k