数据字典请求与缓存方案

1. 背景

数据字典是项目中常用的数据维护的一种方式,它的好处在于用户可以自行配置数据类型,并且在不更新系统的方式下,下发到客户端。常用的实现方式为在后台管理系统中维护一个数据字典模块,服务端提供一套对数据字典的增删查改维护接口,客户端通过服务端提供的通用数据字典查询接口,提供数据字典码,便可以获取到数据列表,提供给用户下拉选择。

客户端常用的实现方式是

  1. 新建一个下拉选择框
  2. 新建一个下拉选择组件
  3. 创建数据接口请求,并将拿到的数据赋值给下拉选择组件
  4. 最终将选择好的数据赋值给下拉选择框,然后给表单提交

缺点:

  • 数据字典在项目中使用频繁,需要不停的创建下拉选择框和选择组件,有很多重复工作
  • 数据接口请求函数也要不停的创建,重复工作
  • 数据字典没有缓存,请求过的数据,还会重复请求,浪费性能

所以我们想优化系统中的数据字典处理,提高开发效率和性能,有了一下需求

2. 需求

  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

  1. 新建dict-code.ts文件,导出 useDictCodeStore pinia 对象
  2. state:设置 dictCodeMap 变量为Map对象 用于存放 数据字典数据 数据结构为 new Map<string,any[]>
  3. 编写功能函数:
    • 判断是否有缓存数据
    • 设置数据字典
    • 获取数据字典

实现代码如下:

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重新启动就会更新,缺点就是第一次进入会请求多次接口。

缓存机制是前端性能优化很好的地方,特别是在项目比较大,数据比较多的情况,除了数据字典,很多其他数据如用户固定信息,配置信息都可以放在缓存中,减少接口请求量,提高客户端性能,当然也需要注意数据刷新时机,避免出现客户端数据过期情况

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui