前言
在后台与中台系统开发领域,数据字典是极为常见且至关重要的概念,相信大多数从事相关开发工作的朋友都对其耳熟能详。几乎每一个成熟的项目,都会专门设置一个字典模块,用于精心维护各类字典数据。这些字典数据在系统中扮演着举足轻重的角色,为下拉框、转义值、单选按钮等组件提供了不可或缺的基础数据支撑。
我自工作以来参与过很多个项目,既有从零开始搭建的,也有接手他人项目的。在实践过程中,我发现不同项目对字典的实现方式各不相同,且各有侧重。例如,对于项目中的字典基本不会发生变化的,项目通常会采用首次全部加载到本地缓存的方式。这种方式能显著节省网络请求次数,提升系统响应速度。然而,对于项目中的字典经常变动的,项目则会采用按需加载的方式,即哪里需要使用字典值,就在哪里进行加载。但这种方式也存在弊端,当某个页面需要使用十多个字典值时,首次进入页面会一次性发出十多个请求来获取这些字典值,影响用户体验。
常见字典方案剖析
在当下,数据字典的实现方案丰富多样,各有优劣。下面将详细介绍几种常见的方案,并分析其特点。我将详细介绍几种常见的方案,并深入剖析其特点。这几种方案皆是我通过实践精心总结而来,其中方案四的思路是由我不爱吃鱼啦提供。
方案一:首次全部加载到本地进行缓存
方案描述
系统启动或用户首次访问时,将所有字典数据一次性加载到本地缓存中。后续使用过程中,直接从缓存中获取所需字典数据,无需再次向服务器发起请求。
优点
- 访问速度快:后续访问时直接从本地缓存读取数据,无需等待网络请求,响应速度极快。
- 减少网络请求:一次性加载后,后续使用无需频繁发起网络请求,降低了网络开销。
- 网络依赖小:即使在网络不稳定的情况下,也能正常使用已缓存的字典数据,保证了系统的稳定性。
缺点
- 首次加载时间长:若字典数据量较大,首次加载时可能需要较长时间,影响用户体验。
- 占用存储空间:将所有字典数据存储在本地,会占用较多的本地存储空间,尤其是当字典数据量庞大时。
- 缓存更新复杂:若字典数据频繁更新,需要设计复杂的缓存同步和更新机制,否则容易出现数据不一致的问题。
方案二:按需加载不缓存
方案描述
当用户触发特定操作,需要使用字典数据时,才从后端实时加载所需数据,且不进行本地缓存。每次使用字典数据时,都重新从服务器获取最新数据。
优点
- 节省存储空间:不进行本地缓存,节省了本地存储空间,尤其适用于存储资源有限的设备。
- 数据实时性高:每次获取的数据都是最新的,不存在缓存数据与后端不一致的问题,保证了数据的准确性。
缺点
- 网络请求频繁:每次使用都需要发起网络请求,在网络状况不佳时,会导致加载时间变长,影响用户体验。
- 增加服务器负担:频繁的网络请求会增加服务器的负担,尤其是在高并发场景下,可能影响服务器的性能。
方案三:首次按需加载并缓存
方案描述
用户首次访问某个字典数据时,从后端加载该数据并缓存到本地。后续再次访问该字典数据时,直接从缓存中读取,无需再次向服务器发起请求。
优点
- 减少网络请求:结合了前两种方案的部分优点,既在一定程度上减少了网络请求次数,又不会一次性加载过多数据。
- 节省存储空间:相较于首次全部加载到本地缓存的方式,不会一次性占用大量本地存储空间,节省了部分存储资源。
缺点
- 缓存管理复杂:需要记录哪些数据已缓存,以便后续判断是否需要从缓存中读取或重新加载,增加了缓存管理的复杂度。
- 缓存占用问题:对于不常使用的字典数据,缓存可能会占用不必要的存储空间,造成资源浪费。
- 缓存更新难题:同样面临缓存更新的问题,需要设计合理的缓存更新策略,以保证数据的准确性和一致性。
方案四:按需加载 + 版本校验更新缓存
方案描述
用户按需发起字典数据请求,首次访问某个字典数据时,从后端加载并缓存到本地。在后端响应头中携带该字典数据的版本信息,后续每次请求该字典数据时,前端对比本地缓存的版本信息和响应头中的版本信息。若版本信息不一致,则清除本地缓存中对应的字典数据,并重新从后端加载最新数据;若版本信息一致,则直接使用本地缓存的数据。
优点
- 数据实时性有保障:通过版本校验机制,能够及时获取到字典数据的更新,确保前端使用的数据与后端保持一致,避免了因缓存数据未及时更新而导致的业务问题。
- 减少不必要的网络请求:在字典数据未更新时,直接使用本地缓存,无需发起网络请求,节省了网络带宽和服务器资源。
- 平衡存储与性能:既不会像首次全部加载那样占用大量本地存储空间,又能在一定程度上减少网络请求,在存储和性能之间取得了较好的平衡。
缺点
- 版本管理复杂:后端需要维护字典数据的版本信息,并且要确保版本号的准确性和唯一性,这增加了后端开发的复杂度和维护成本。
- 额外开销:每次请求都需要进行版本信息对比操作,虽然开销较小,但在高并发场景下,可能会对系统性能产生一定影响。
- 首次加载体验:首次加载字典数据时,依然需要从后端获取数据,若数据量较大或网络状况不佳,可能会影响用户体验。
方案选型建议
建议根据项目特性选择方案,没有最好的技术方案,只有最适合项目的技术方案:
- 字典稳定且量小:方案一全量缓存
- 字典频繁更新:方案四版本校验缓存
- 存储敏感场景:方案三按需缓存
- 实时性要求极高:方案二无缓存方案
ps:如果大家有更好的方案,也可以在评论区提出,让我们大家一起学习成长
代码实现(方案四)
下述代码的实现基于vue3+pinia,该代码实现了统一管理全局字典数据,支持按需加载、缓存复用、版本控制、动态更新、批量处理字典数据等功能。
pinia store的实现
ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getDictDetails, type Details } from '@/api/system/dict'
export const useDictStore = defineStore('dict', () => {
// 存储字典数据,键为字典名称,值为字典详情数组
const dictData = ref<Record<string, Details[]>>({})
// 存储字典版本信息,键为字典名称,值为版本号
const dictVersions = ref<string>('')
/**
* 更新字典版本信息
* @param version 新的字典版本号
*/
const updateDictVersion = (version: string) => {
dictVersions.value = version
}
/**
* 获取字典版本
* @returns 字典版本号
*/
const getDictVersion = () => {
return dictVersions.value || ''
}
/**
* 加载字典数据
* @param dictNames 字典名称数组
* @returns 加载的字典数据对象
*/
const getDicts = async (dictNames: string[]) => {
try {
if (!Array.isArray(dictNames)) {
return {};
}
// 过滤并去重有效字典名称
const uniqueNames = [...new Set(dictNames.filter(name =>
typeof name === 'string' && name.trim()
))];
if (uniqueNames.length === 0) {
return {};
}
const result: Record<string, Details[]> = {};
const unloadedDicts: string[] = [];
// 分离已加载和未加载的字典
dictNames.forEach(name => {
if (dictData.value[name]) {
result[name] = dictData.value[name];
} else {
unloadedDicts.push(name);
}
});
// 如果有未加载的字典,从接口请求获取
if (unloadedDicts.length > 0) {
const { data } = await getDictDetails(unloadedDicts);
// 合并新加载的数据到结果
Object.assign(result, data);
// 更新全局字典缓存
Object.assign(dictData.value, data);
}
return result;
} catch (error) {
console.error('加载字典数据失败:', error);
return {};
}
};
/**
* 根据字典名称获取字典数据
* @param name 字典名称
* @returns 字典详情数组
*/
const getDict = (name: string) => {
return dictData.value[name] || []
}
/**
* 根据字典名称和值获取字典标签
* @param name 字典名称
* @param value 字典值
* @returns 字典标签
*/
const getDictLabel = (name: string, value: string) => {
const dict = getDict(name)
const item = dict.find(item => item.value === value)
return item?.label || ''
}
/**
* 根据字典名称和标签获取字典值
* @param name 字典名称
* @param label 字典标签
* @returns 字典值
*/
const getDictValue = (name: string, label: string) => {
const dict = getDict(name)
const item = dict.find(item => item.label === label)
return item?.value || ''
}
/**
* 清除指定字典数据
* @param names 字典名称
*/
const clearDicts = (names: string[]) => {
names.forEach(name => {
clearDict(name)
})
}
/**
* 清除指定字典数据
* @param name 字典名称
*/
const clearDict = (name: string) => {
delete dictData.value[name]
}
/**
* 清除所有字典数据
*/
const clearAllDict = () => {
dictData.value = {}
}
return {
dictData,
updateDictVersion,
getDictVersion,
getDict,
getDicts,
getDictLabel,
getDictValue,
clearDict,
clearDicts,
clearAllDict
}
})
useDict 实现
为组件提供字典数据的统一访问入口,封装了字典数据的初始化加载、详情查询、标签/值转换等高频操作,简化组件层对字典数据的调用逻辑。
ts
import { type Details } from '@/api/system/dict'
import { useDictStore } from '@/store/dict'
// 根据字典值的name获取字典详情
export const useDict = (params: string[] = []) => {
const dict = ref<Record<string, Details[]>>()
const dictStore = useDictStore()
const getDicts = async () => {
dict.value = await dictStore.getDicts(params)
}
// 初始化字典数据
getDicts()
// 根据字典名称获取字典数据
const getDict = (name: string) => {
return dictStore.getDict(name)
}
// 根据字典值获取字典label
const getDictLabel = (name: string, value: string) => {
return dictStore.getDictLabel(name, value)
}
return {
dict,
getDict,
getDictLabel
}
}
响应拦截
主要用于获取字典的版本信息,通过对比版本信息,从而确定是否清除本地的字典缓存数据,并更新本地缓存的版本信息
ts
// 响应拦截器
service.interceptors.response.use(
// AxiosResponse
(response: AxiosResponse) => {
const dictVersion = response.headers['x-dictionary-version']
if (dictVersion) {
const dictStore = useDictStore()
// 对比版本是否有更新
if (dictStore.getDictVersion() !== dictVersion) {
dictStore.clearAllDict()
dictStore.updateDictVersion(dictVersion || '')
}
}
// ...项目中的业务逻辑
}
)
项目中的具体使用
下述的怎么使用封装的字典管理的简单demo
ts
<script setup lang="ts">
import { useDict } from '@/hooks/useDict'
// 获取dict
const { dict, getDictLabel } = useDict(['status', 'sex'])
console.log(dict.status, dict.sex)
</script>
结语
本文介绍了四种主流的数据字典实现方案,从全量加载到按需加载,从无缓存到版本校验缓存,每种方案都展现了其独特的优势与缺点。通过对比分析,我们不难发现,没有一种方案能够适用于所有场景,而是需要根据项目的具体特性进行灵活选择。对于字典稳定且量小的项目,全量缓存方案能够带来极致的响应速度;对于字典频繁更新的场景,版本校验缓存方案则能在保障数据实时性的同时,实现存储空间与网络请求的平衡优化。未来,随着技术的不断进步与应用场景的不断拓展,数据字典的实现方案也将持续演进。
博客主要记录一些学习的文章,如有不足,望大家指出,谢谢。