1. 背景
数据字典是项目中常用的数据维护的一种方式,它的好处在于用户可以自行配置数据类型,并且在不更新系统的方式下,下发到客户端。常用的实现方式为在后台管理系统中维护一个数据字典模块,服务端提供一套对数据字典的增删查改维护接口,客户端通过服务端提供的通用数据字典查询接口,提供数据字典码,便可以获取到数据列表,提供给用户下拉选择。
客户端常用的实现方式是
- 新建一个下拉选择框
- 新建一个下拉选择组件
- 创建数据接口请求,并将拿到的数据赋值给下拉选择组件
- 最终将选择好的数据赋值给下拉选择框,然后给表单提交
缺点:
- 数据字典在项目中使用频繁,需要不停的创建下拉选择框和选择组件,有很多重复工作
- 数据接口请求函数也要不停的创建,重复工作
- 数据字典没有缓存,请求过的数据,还会重复请求,浪费性能
所以我们想优化系统中的数据字典处理,提高开发效率和性能,有了一下需求
2. 需求
- 需要一个完整的数据字典请求组件,能够快速完成数据请求并选择
- 对已经选择过的数据字典做本地缓存,免得重复请求,浪费性能
3. 实现思路(uniAPP端)
封装组件
使用input框作为展示,select组件作为选择,组件内部在onMounted时会根据prop传入的options 和 dataCode 判读使用用户传入的列表还是根据数据字典dataCode请求后端数据字典接口。
组件Props设计如下
Prop | 类型 | 默认值 | 备注 |
---|---|---|---|
options | any[] | [] | 选项列表 |
label | string | null | '' | 双向绑定名称 |
id | string | null | '' | 双向绑定id |
labelName | string | name | label别名 |
idName | string | code | id的别名 |
disabled | boolean | false | 是否禁用 |
dictCode | string | '' | 字典名称 |
placeholder | string | 请选择 | 提示语 |
clear | boolean | false | 是否允许清空选择 |
excludes | any[] | [] | 排除的数组 |
emptyText | string | '' | 空数据的时候提示c |
cached | boolean | true | 是否缓存 |
组件抛出事件
Event | 备注 | 调用 |
---|---|---|
update:label | 双向绑定值 | emits("update:label", item[props.labelName]); |
update:id | 双向绑定id | emits("update:id", item[props.idName]); |
confirm | 确认选择事件 | emits("confirm", item); |
clear | 清空事件 | emits("clear"); |
代码如下:
vue
<!--
@description 单选下拉组件-可双向绑定label和id 原则上label 不重复
@use:
<SelectSingle
v-model:label="str"
v-model:id="id"
label-name="label"
id-name="id"
:disabled="false"
:options="options"
@confirm="comfirm"
></SelectSingle>
@author:pxt
-->
<template>
<view class="wd-w-[100%] wd-h-[100%] wd-bg-white">
<tm-input
type="textarea"
:auto-height="true"
class="wd-w-full"
:model-value="label"
align="right"
:transprent="true"
:height="80"
:input-padding="[0, 0]"
:suffix="disabled ? '' : 'tmicon-angle-right'"
disabled
:show-clear="clear"
:placeholder="placeholder"
@clear="handleClear"
@click="handleClick"
></tm-input>
</view>
<mk-picker
v-model:show="visible"
v-model="selectData"
:default-value="[currentIndex]"
:columns="localOptions"
:data-key="labelName"
@confirm="handleConfirm"
></mk-picker>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watch } from "vue";
import { reqDictList } from "@/api/common";
import mkPicker from "@/components/mk-picker/mk-picker.vue";
import { useDictCodeStore } from "@/store/dict-code";
const dictCodeStore = useDictCodeStore();
// 定义Props
interface Props {
options?: Array<Object>; //列表
label: any; //双向绑定名称
id: any; //双向绑定id
labelName?: string; //label别名
idName?: string; //id的别名
disabled?: boolean;
dictCode?: string;
placeholder?: string;
clear?: boolean;
excludes?: Array<string>;
emptyText?: string; //空数据时提示
cached?: boolean; //是否缓存
}
// 获取传参
const props = withDefaults(defineProps<Props>(), {
idName: "code",
labelName: "name",
disabled: false,
dictCode: "",
placeholder: "请选择",
clear: false,
emptyText: "",
excludes: () => [],
options: () => [],
cached: true,
});
// 当前选中的索引
const selectData = ref([]);
// 是否展示选择框
const visible = ref(false);
const localOptions = ref([]);
watch(
() => props.options,
(val) => {
if (props.dictCode) return;
if (val && val.length) {
localOptions.value = val;
}
if (val && !val.length) {
localOptions.value = [];
}
},
{
immediate: true,
}
);
watch(
() => props.dictCode,
(val) => {
if (val) {
getDictData();
}
},
{
immediate: true,
}
);
// 第一次显示 - 回显文字
let currentIndex = computed(() => {
let index = props.options.findIndex(
(el) => el[props.labelName] === props.label
);
return index >= 0 ? index : 0;
});
// 如果prop 为空 返回值也为空
watch(
() => props.id,
(newValue) => {
if (newValue === "" || newValue === null) {
emits("update:label", "");
}
if (props.dictCode && props.dictCode === "whether") {
emits(
"update:label",
props.id == "1" ? "是" : props.id == "0" ? "否" : ""
);
}
},
{
immediate: true,
}
);
// 处理input框点击事件
const handleClick = () => {
// 禁用是点击
if (props.disabled) return;
if (!localOptions.value.length) {
getDictData();
}
if (!props.options.length && !props.dictCode && props.emptyText) {
uni.showToast({
title: props.emptyText,
icon: "none",
mask: true,
});
return;
}
visible.value = true;
};
// getDictData();
async function getDictData() {
if (!props.dictCode) return;
if (props.dictCode === "whether") {
localOptions.value = [
{
name: "是",
code: "1",
},
{
name: "否",
code: "0",
},
];
} else {
let data = [];
if (props.cached && dictCodeStore.hasDictCode(props.dictCode)) {
data = dictCodeStore.getDictCode(props.dictCode);
} else {
let res = await reqDictList(props.dictCode);
data = res.data?.result || [];
if (props.cached) {
dictCodeStore.setDictCode(props.dictCode, data);
}
}
if (props.excludes.length && data.length) {
data = data.filter(
(item) => !props.excludes.includes(item[props.idName])
);
}
localOptions.value = data.length ? data : [];
}
}
// 创建emit
const emits = defineEmits(["confirm", "update:label", "update:id", "clear"]);
// 处理回调,抛出事件
const handleConfirm = (value) => {
value = value[0];
let item = localOptions.value[value];
// if (!props.options.length && !props.dictCode) return;
// 选择空数据 或者 两次选择一样不修改 优化性能
if (item[props.idName] === "#" || props.label === item[props.labelName])
return;
// 点击确认返回点击对象
emits("confirm", item);
// 更新label
emits("update:label", item[props.labelName]);
// 更新id
emits("update:id", item[props.idName]);
};
// 清空
const handleClear = () => {
emits("update:label", "");
emits("update:id", "");
emits("clear");
};
</script>
<style scoped></style>
封装Pinia
- 新建dict-code.ts文件,导出 useDictCodeStore pinia 对象
- state:设置 dictCodeMap 变量为Map对象 用于存放 数据字典数据 数据结构为 new Map<string,any[]>
- 编写功能函数:
- 判断是否有缓存数据
- 设置数据字典
- 获取数据字典
实现代码如下:
javascript
import { defineStore } from "pinia";
export const useDictCodeStore = defineStore("dictCode", {
state: () => {
return {
dictCodeMap: new Map<string, any[]>(),
};
},
getters: {
GET_DICTCODE: (state: any) => state.dictCodeMap,
},
actions: {
// 判断是否有缓存数据
hasDictCode(code: string) {
return (
this.dictCodeMap.has(code) && this.dictCodeMap.get(code).length > 0
);
},
// 设置缓存数据
setDictCode(code: string, options: any[]) {
this.dictCodeMap.set(code, options);
},
// 获取缓存数据
getDictCode(code: string) {
return this.dictCodeMap.get(code) || [];
},
},
});
实现效果
使用:
缓存前:
缓存后:
总结
因为没有好的更新缓存时机,所以存放在pinia中,每次APP重新启动就会更新,缺点就是第一次进入会请求多次接口。
缓存机制是前端性能优化很好的地方,特别是在项目比较大,数据比较多的情况,除了数据字典,很多其他数据如用户固定信息,配置信息都可以放在缓存中,减少接口请求量,提高客户端性能,当然也需要注意数据刷新时机,避免出现客户端数据过期情况