vue3项目实现数据字典、下拉数据缓存最佳方案,解决同一下拉数据并发多次调用接口

前言

在前端项目中经常会需要使用下拉选择框请求一些数据字典或者其他下拉接口数据,就比如在列表页搜索项中请求数据字典下拉数据,或者在一些表单编辑页请求下拉接口数据赋值给下拉选择框。这些下拉选择框通常是复用的,甚至一个页面调用多次一样的下拉选择框。为了减轻服务器负担,提高性能,节省带宽,同时提高开发效率,决定将这一类型数据进行优化。

背景

公司项目大量列表页面和编辑页面用到数据字典下拉框和其他远程接口的下拉框(如下图),作为前端项目负责人,希望将下拉框进行封装和数据缓存。

思路

采用pinia做数据缓存,封装统一调用数据请求的方法,封装下拉组件。

use-data-cache-store.ts提供getDictionaryByKey获取数据字典数据和getDropDownData获取下拉数据方法。getDictionaryByKey提供数据字典key查询参数,后端统一数据字典查询接口;getDropDownData提供要缓存的唯一key值和自定义请求。
当调用getDictionaryByKey方法时根据key来判断pinia是否缓存,如果有缓存则直接返回,如果没有则判断dictionaryPromise是否有正在请求的promise,如果有正在请求的promise则返回promise等待promise返回数据,如果既没有缓存也没有正在请求的promise则发起请求并赋值给dictionaryPromise,请求完毕后返回数据并缓存数据

缓存下拉数据,解决了同一个页面多次调用同个下拉框时会多次调用同一接口问题。

具体代码参考

/src/stores/use-data-cache-store.ts

ts 复制代码
import { defineStore } from 'pinia'
import type { Dictionary } from '~/types/conditional-search'
import { conditionalSearchApi } from '~/api/conditional-search'
import { ElMessage } from 'element-plus'

import { type Dict } from '~/types/dict'
import { type Option } from '~/types/option'

interface DataCacheStore {
  dictionary: Record<string, Option[]>
  dictionaryPromise: Record<string, Promise<any> | null>
  dropDownData: Record<string, Option[]>
  dropDownPromise: Record<string, Promise<any> | null>
}

export type FormatOptions<T> = (data: T[]) => Option[]

export const useDataCache = defineStore('dataCache', {
  state: (): DataCacheStore => {
    return {
      dictionary: {},
      dictionaryPromise: {},
      dropDownData: {},
      dropDownPromise: {}
    }
  },
  actions: {
    /**
     * @example: 获取数据字典数据
     * @param {Dictionary} key
     * @param {boolean} fetchCache 是否读取缓存
     * @return {*}
     */
    async getDictionaryByKey (key?: string, fetchCache: boolean = true) {
      if (!key) throw new Error('请传数据字典参数')

      if (!fetchCache) {
        Reflect.deleteProperty(this.dictionary, key)
      }

      if (fetchCache && this.dictionary[key]) {
        return this.dictionary[key]
      }

      if (this.dictionaryPromise[key]) {
        return await this.dictionaryPromise[key]
      }

      try {
        const fetchPromise = conditionalSearchApi
          .getDictData({
            type: key
          })
          .then((res) => {
            const { code, data, msg } = res
            if (code === 200) {
              this.dictionary[key] = data.map((item: Dict) => {
                return {
                  label: item.dictLabel,
                  value: item.dictValue
                }
              })
              return this.dictionary[key]
            } else {
              ElMessage.error(msg || '获取字典数据失败')
            }
          })
          .finally(() => {
            Reflect.deleteProperty(this.dictionaryPromise, key)
          })

        this.dictionaryPromise[key] = fetchPromise

        const result = await fetchPromise
        return result
      } catch (e) {
        Reflect.deleteProperty(this.dictionaryPromise, key)
      }
    },
    /**
     * @example: 更新数据字典数据
     * @param {Dictionary} key
     * @param {Option[]} data
     * @param {boolean} isCreate 不存在时是否创建数据
     * @return {*}
     */
    updateDictionary (key: Dictionary, data: Option[], isCreate: boolean = false) {
      if (!key) throw new Error('请传数据字典key')
      if (!isCreate && !this.dictionary[key]) return
      this.dictionary[key] = data
    },
    /**
     * @example: 获取下拉数据
     * @param {string} key
     * @param {(params?: any) => Promise<any>} getDropDownDataApi
     * @param {boolean} fetchCache 是否读取缓存
     * @param {FormatOptions<T>} formatOptions 返回数据处理函数
     * @return {*}
     */
    async getDropDownData<T>(
      key: string,
      getDropDownDataApi: (params?: any) => Promise<any>,
      fetchCache: boolean = true,
      formatOptions?: FormatOptions<T>
    ) {
      if (!fetchCache) {
        Reflect.deleteProperty(this.dropDownData, key)
      }

      if (fetchCache && this.dropDownData[key]) {
        return this.dropDownData[key]
      }

      if (this.dropDownPromise[key]) {
        return await this.dropDownPromise[key]
      }

      try {
        const fetchPromise = getDropDownDataApi()
          .then((res) => {
            let { code, data, msg } = res
            if (code === 200) {
              formatOptions && (data = formatOptions(data))
              this.dropDownData[key] = data
              return data
            } else {
              ElMessage.error(msg || '获取数据失败')
            }
          })
          .finally(() => {
            Reflect.deleteProperty(this.dropDownPromise, key)
          })

        this.dropDownPromise[key] = fetchPromise

        const result = await fetchPromise
        return result
      } catch (e) {
        Reflect.deleteProperty(this.dropDownPromise, key)
      }
    }
  }
  // 添加持久化选项:如果需要持久化就引入persist
  // persist: {
  //   enabled: true, // 启用持久化
  //   strategies: [
  //     {
  //       paths: ['dictionary'], // 指定哪些状态需要被持久化。
  //       key: CACHE_DICTIONARY, // 本地存储的键
  //       storage: localStorage // 使用localStorage存储
  //     },
  //     {
  //       paths: ['dropDownData'],
  //       key: CACHE_DROP_DOWN_DATA,
  //       storage: sessionStorage
  //     }
  //   ]
  // }
})

/src/components/base-select.vue

ts 复制代码
<template>
  <el-select-v2
    style="width: 100%"
    v-bind="$attrs"
    v-model="selectValue"
    :options="finalOptions"
    :placeholder="placeholder"
    :clearable="clearable"
    :filterable="filterable"
  >
    <template #[slotName]="slotProps" v-for="(_slot, slotName) in $slots">
      <slot :name="slotName" v-bind="slotProps"></slot>
    </template>
  </el-select-v2>
</template>

<script lang="ts">
export default {
  name: 'base-select'
}
</script>
<script lang="ts" setup>
import { useVModel } from '@vueuse/core'
import { ref, watch } from 'vue'
import { useDataCache } from '~/stores/use-data-cache-store'
import type { Dictionary } from '~/types/conditional-search'
import { errorCaptured } from '~/utils/http-error-msg'
import { DataSourceType, type SelectDataItem } from '~/types/common/base-select'
import { deepCopy } from '~/utils/deep-copy'

const emits = defineEmits(['update:modelValue'])

interface Props {
  modelValue?: number | string | Array<string | number>
  options?: SelectDataItem[]
  clearable?: boolean
  filterable?: boolean
  placeholder?: string
  type?: `${DataSourceType}` // 数据源类型
  cacheKey?: string // 数据字典key | 产品属性key | 远程下拉数据缓存key
  remoteApi?: (params?: any) => Promise<any> // 远程下拉请求
  fetchCache?: boolean // 是否读取缓存
  formatOptions?: (options: any[]) => SelectDataItem[] // 格式化返回数据
}

const props = withDefaults(defineProps<Props>(), {
  placeholder: '请选择',
  fetchCache: true,
  clearable: true,
  filterable: true
})
const selectValue = useVModel(props, 'modelValue', emits)

const finalOptions = ref<SelectDataItem[]>([])
const useDataCacheStore = useDataCache()
const loading = ref(false)

const fetchData = async () => {
  try {
    const { options, type, remoteApi, cacheKey, fetchCache, formatOptions } = props

    if (!options && !type && !remoteApi && !cacheKey) {
      throw new Error('请提供相关组件参数')
    }

    loading.value = true
    let result: SelectDataItem[] = []
    if (options) {
      result = props.options as SelectDataItem[]
    } else if (type === DataSourceType.DICT && cacheKey) {
      result = await useDataCacheStore.getDictionaryByKey(cacheKey, fetchCache)
    } else if (type === DataSourceType.ATTR && cacheKey) {
      result = await useDataCacheStore.getAttributeByKey(cacheKey as Dictionary, fetchCache)
    } else if (remoteApi && cacheKey) {
      result = await useDataCacheStore.getDropDownData(cacheKey, remoteApi, fetchCache)
    } else if (remoteApi) {
      const [res] = await errorCaptured(remoteApi)
      if (res && res.code === 200) {
        result = res.data as SelectDataItem[]
      }
    }

    if (formatOptions) {
      finalOptions.value = formatOptions(deepCopy(result))
    } else {
      finalOptions.value = result
    }
  } finally {
    loading.value = false
  }
}
watch([() => props.options, () => props.type, () => props.remoteApi, () => props.cacheKey], fetchData, {
  immediate: true
})
</script>

扩展与优化

个人项目根据数据时效性是缓存在pinia,在刷新页面时会丢失缓存。 如果项目需要持久缓存可以引入persist进行配置。如果需要缓存一些变化比较小、数据量大的数据可以使用浏览器存储--IndexedDB数据库。

相关推荐
阿金要当大魔王~~1 分钟前
post get 给后端传参数
前端·javascript·html
慌张的葡萄5 分钟前
24岁裸辞,今年26岁了😭做乞丐做保安做六边形战士
前端·面试
weixin_535854229 分钟前
快手,蓝禾,优博讯,三七互娱,顺丰,oppo,游卡,汤臣倍健,康冠科技,作业帮,高途教育25届春招内推
java·前端·python·算法·硬件工程
excel10 分钟前
webpack 编译器
前端
IT、木易27 分钟前
大白话css第九章主要聚焦于前沿技术整合、生态贡献与技术传承
前端·css
星星点点洲1 小时前
【LangChain.js】Python版LangChain 的姊妹项目
javascript·langchain
徐同保1 小时前
vue3后端管理项目,左侧菜单可以拖拽调整宽度
前端·javascript·vue.js
qianmoQ1 小时前
第七章:项目实战 - 第四节 - Tailwind CSS 移动端适配实践
前端·css
川石课堂软件测试2 小时前
涨薪技术|持续集成Git使用详解
开发语言·javascript·git·python·功能测试·ci/cd·单元测试
imkaifan2 小时前
如何在前端项目中看出node_modules中的库是一个可选依赖库
前端·npm命令·可选依赖