字典组件作为常用组件,好好封装一下很有必要。
使用字典的常见方式
- 字典数据作为 JSON 文件在前端项目中引入;
- 应用加载时,把所有字典的数据请求过来缓存在前端;
- 根据需要按需调用接口获取字典数据,然后渲染组件;
方式 1 非常不灵活,适用于不会变动的字典。要不后端数据一变化,前端还要跟着维护,这样容易出问题。
方式 2 缺点也比较明显:
- 把数据库所有字典数据请求过来,安全性不高(不想暴露的字典也被拿出来了),而且有的系统字典数据非常大。
- 不会实时更新,一般是在用户重新登录或者刷新页面后才能更新
在我的 mocha Vue3 Admin 中很早就封装了字典组件,但是感觉并不理想。
之前的做法
- 使用 useDict 钩子在渲染数据前通过接口调用字典API获取需要字典的数据(以此避免多个相同的字典组件多次请求接口)
- Modict 组件根据返回的数据渲染字典的文字和样式
以上方法最大的缺点是:虽然使用 useDict 减少了请求,但是每次切换页面都会重新调用API,增加网络请求。另外,因为每次都要先调用useDict,用起来还是稍嫌麻烦。
优点是可以实时读取字典,不会出现滞后。
新方案
所以趁着假期,继续优化我们的字典组件。
因为在实际场景中,大部分字典的值是几乎不会变动的。所以把字典数据缓存在前端没问题,对于需要实时更新也让它能够更新起来。
接下来,就是实现下面 4 个需求:
- 按需加载数据,只请求需要的字典数据
- 无需频繁更新的数据,保存在缓存中
- 需要实时更新的数据,可以保持及时更新
- 减少请求次数
需求 1 根据当前字典的 name 返回字典数据即可。
需求 2,我们先判断缓存中有没有当前字典的数据,如果有,直接用缓存的,反之,调用接口后再放入缓存中,这样可以极大减少网络请求。
在表格、列表这种,同一个字典标签被重复使用几十次,在第一次进入还没有缓存时,依然会向后端发送多次请求。需求 3:实时更新数据,这种情况不再依赖缓存,每个字典标签都会发出一个请求。 所以如何减少重复请求呢?当然少不了好用的 promise。
首先,我们使用pinia来管理字典,在 store 下新建 dict.ts,
主要的两个方法代码如下:
注:在此使用 piniaPluginPersistedstate 将 pinia 的 字典数据保存在缓存中,本项目使用SessionStorage。
可以在应用重新加载/重新登录时清除缓存,重新获取字典数据。
// ...
export const useDictStore = defineStore(
'dict',
() => {
const dicts = ref<DictMap>({})
const getDictData = async (dictType: string, refresh: boolean = false) => {
return new Promise((resolve, reject) => {
let data: any = dicts.value[dictType]
// 根据缓存中是否有数据 && refresh(==false),读取缓存中的数据
if (data && !refresh) {
try {
resolve(data)
} catch (e) {
reject(e)
}
}
// 否则从接口中获取数据
else {
const p = handleRepeatedRequest(dictType)
resolve(p)
}
})
}
// 处理重复的 Promise 请求
let promiseRecords = <PromiseRequestMap>{}
// 多次相同的请求只调用一次
const handleRepeatedRequest = (key: string) => {
if (!promiseRecords[key]) {
console.log('no repeated request')
promiseRecords[key] = systemApi
.getDicts(key)
.then((res: any) => {
// 存入缓存
dicts.value[key] = res
return res
})
.catch((e: Error) => {})
.finally(() => {
// 请求完毕,清理掉,否则会一直保留该promise
promiseRecords[key] = null
})
} else {
console.log('already has repeated request')
}
return promiseRecords[key]
}
// ...
字典数据获取的方法写好了,接下来就开始封装字典组件。
主要实现以下功能:
- 渲染为
ElTag
或者 纯文本 - 根据
DictName
获取字典数据渲染 - 可以使用自定义的字典数据渲染
- 设置
refresh
属性为true
,实时获取最新的字典数据
代码如下:
<template>
<span v-if="item">
<span v-if="text">{{ item.label }}</span>
<el-tag
:type="$attrs.type || item.type"
:class="item.class"
:effect="$attrs.effect || item.effect"
v-else
>{{ item.label }}</el-tag
>
</span>
</template>
<script lang="ts" setup>
import { ref, computed, watch, watchEffect } from 'vue'
import { useDictStore } from '~/store/dict'
import { DictItem } from '#/dict'
const useDict = useDictStore()
const props = defineProps({
value: {
type: String,
default: ''
},
// 根据 DictName 从接口/缓存获取字典数据集
dictName: {
type: String,
default: ''
},
// 使用自定义的字典数据集,不使用接口/缓存中
dictData: { type: Array },
text: { type: Boolean, default: false },
// 不使用缓存,实时获取获取
refresh: { type: Boolean, default: false }
})
const dictOptions = ref<DictItem[]>([])
watchEffect(async () => {
// 优先使用自定义的 DictData
if (props.dictData) {
dictOptions.value = props.dictData
} else {
dictOptions.value = (await useDict.getDictData(props.dictName, props.refresh)) as DictItem[]
}
})
const item = computed<DictItem | undefined>(() => {
const tempItem = dictOptions.value?.find((item) => item.value === props.value)
if (tempItem) return tempItem
else return undefined
})
</script>
OK,最后放在页面中实践一下,基本的字典标签功能都有了,可以根据自己的需要,继续完善。
<template>
<div>
<div>字典标签</div>
<div class="m-4">
<div class="mb-2">获取 clientType 的字典</div>
<div>ElTag:<MoDict :value="formData.type" dictName="clientType" refresh /></div>
<div>普通文本:<MoDict :value="formData.type" dictName="clientType" text /></div>
</div>
<el-divider></el-divider>
<div class="m-4">
<div class="mb-2">自定义字典数据</div>
<div class="mb-2"><el-button @click="changeColor">Change Color</el-button></div>
<MoDict :value="formData.color" :dictData="customDictData" />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
const formData = reactive({
type: 'highschool',
color: 'red'
})
const customDictData = [
{
label: '红色',
value: 'red',
class: 'text-red-500 bg-red-100 border-red-200'
},
{
label: '蓝色',
value: 'blue',
class: 'text-blue-500 bg-blue-100 border-blue-200'
},
{
label: '绿色',
value: 'green',
class: 'text-green-500 bg-green-100 border-green-200'
},
{
label: '粉色',
value: 'pink',
class: 'text-pink-500 bg-pink-100 border-pink-200'
}
]
const changeColor = () => {
formData.color = customDictData[Math.floor(Math.random() * 4)].value
}
</script>
字典标签组件有了,那么常用的selector、radio、checkbox也可以封装起来。这里就不再赘述,详见mocha-vue3-system 仓库代码。
项目地址
本项目GIT地址:github.com/lucidity99/…
如果有帮助,给个star ✨ 点个赞