前言
在前端项目中经常会需要使用下拉选择框请求一些数据字典或者其他下拉接口数据,就比如在列表页搜索项中请求数据字典下拉数据,或者在一些表单编辑页请求下拉接口数据赋值给下拉选择框。这些下拉选择框通常是复用的,甚至一个页面调用多次一样的下拉选择框。为了减轻服务器负担,提高性能,节省带宽,同时提高开发效率,决定将这一类型数据进行优化。
背景
公司项目大量列表页面和编辑页面用到数据字典下拉框和其他远程接口的下拉框(如下图),作为前端项目负责人,希望将下拉框进行封装和数据缓存。
思路
采用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数据库。