目录
- 前言
- 1. 基本知识
- 2. Demo
- 3. 实战
前言
🤟 找工作,来万码优才:👉 #小程序://万码优才/r6rqmzDaXpYkJZF
从实战中出发:
1. 基本知识
Vue3 和 Java 通信时如何进行字典数据管理
需要了解字典数据的结构。通常,字典数据包含一个键和一个值,有时候还有一些额外的属性,比如颜色类型或 CSS 类,这些在前端展示时可能会用到
库来发送 HTTP 请求到后端,获取这些字典数据。
需要分析如何在前端存储和管理这些字典数据。Pinia 是一个推荐的 Vue
通过 Pinia,可以定义一个字典 store,用来存储所有获取到的字典数据。这样,所有组件都可以方便地访问这些数据,而不需要每次都通过 API 获取.另外,前端缓存也是一个重要的考虑因素。由于字典数据通常是静态的或变化不大,可以在前端缓存这些数据,减少对后端的请求次数,提高应用的响应速度。Vue 的hooks,比如 useCache
如何在路由和权限控制中使用这些字典数据
在用户登录后,前端需要获取字典数据以完成菜单加载和路由动态生成。这里需要与 Vue Router 结合,根据字典数据生成对应的路由配置,确保用户只能访问其权限范围内的功能。还需要处理字典数据的更新和缓存的失效。如果字典数据有更新,前端需要有一个机制来刷新缓存,确保用户使用的是最新的数据。这可能涉及到设置缓存的有效期,或者在有更新时手动清除缓存并重新获取数据
字典数据管理是一个重要的组成部分,字典数据通常包括各种下拉列表、状态标识等,用于在页面中展示和交互
特别是当使用 Vue3 与 Java 后端进行通信时,字典数据的获取、存储、管理和reload都成为关键点
- 字典数据通常包括以下几个要素:
- 字典类型(dictType):表示字典的分类,如用户状态、订单状态等
- 字典值(dictValue):具体的一个字典项,包含 value 和 label,有时还包括额外属性如颜色或 CSS 类
- 其他属性:根据业务需求,可能还包括颜色类型、排序顺序等
interface DictDataVO {
dictType: string;
value: string;
label: string;
colorType?: string;
cssClass?: string;
}
后端会提供一个获取字典数据的接口,如:GET /api/system/dict/simple
返回的数据格式如下:
[
{
"dictType": "user_status",
"value": "0",
"label": "正常"
},
{
"dictType": "user_status",
"value": "1",
"label": "禁用"
},
// 其他字典项
]
- 使用 Pinia 进行字典数据的状态管理,Pinia 是 Vue3 推荐的状态管理库,适合管理全局状态,如字典数据
具体定义的Store:
export const useDictStore = defineStore('dict', {
state: () => ({
dictMap: new Map<string, any>(),
isSetDict: false
}),
getters: {
getDictMap: (state) => state.dictMap,
getIsSetDict: (state) => state.isSetDict
},
actions: {
async setDictMap() {
// 具体逻辑稍后分析
},
getDictByType(type: string) {
return this.dictMap.get(type) || [];
}
}
});
后续只需要初始化对应的Store即可:
const dictStore = useDictStore();
dictStore.setDictMap();
- 前端缓存可以减少对后端的请求,提高响应速度。可以存储在 sessionStorage 或 localStorage 中
使用自定义缓存钩子:
import { CACHE_KEY } from '@/hooks/web/useCache';
const { wsCache } = useCache('sessionStorage');
// 存储字典数据
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 });
// 获取字典数据
const cachedDict = wsCache.get(CACHE_KEY.DICT_CACHE);
- 字典数据的获取与初始化
在用户登录后,前端需要获取字典数据并存储到 Pinia 和缓存中
逻辑步骤:
检查缓存:首先尝试从缓存中获取字典数据,如果存在且未过期,直接使用
从后端获取:如果缓存不存在或过期,发送请求到后端获取最新数据
存储到 Pinia 和缓存:将获取到的字典数据存储到 Pinia 和 SessionStorage 中
动态生成路由(可选):根据字典数据动态加载菜单和路由,确保用户只能访问权限内的功能
主要的步骤如下:
async function initDictData() {
const dictStore = useDictStore();
if (dictStore.isSetDict) return;
// 从缓存获取
const cachedDict = wsCache.get(CACHE_KEY.DICT_CACHE);
if (cachedDict) {
dictStore.dictMap = cachedDict;
dictStore.isSetDict = true;
return;
}
// 从后端获取
try {
const response = await getSimpleDictDataList();
const dictDataMap = new Map<string, any>();
response.forEach((dictData: DictDataVO) => {
if (!dictDataMap.has(dictData.dictType)) {
dictDataMap.set(dictData.dictType, []);
}
dictDataMap.get(dictData.dictType)?.push({
value: dictData.value,
label: dictData.label,
colorType: dictData.colorType,
cssClass: dictData.cssClass
});
});
// 存储到 Pinia 和缓存
dictStore.dictMap = dictDataMap;
dictStore.isSetDict = true;
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 });
} catch (error) {
console.error('Failed to fetch dictionary data:', error);
}
}
- 在组件中使用字典数据
在需要使用字典数据的组件中,可以轻松访问 Pinia 存储的字典数据
<template>
<div>
<label>用户状态:</label>
<select v-model="selectedStatus">
<option v-for="(item, index) in dictStatus" :key="index" :value="item.value">
{{ item.label }}
</option>
</select>
</div>
</template>
<script setup lang="ts">
const dictStore = useDictStore();
const dictStatus = dictStore.getDictByType('user_status');
const selectedStatus = ref('0');
</script>
``
6. 字典数据的更新与缓存失效
为确保字典数据的一致性,需要处理缓存的更新和失效
```js
async function refreshDict() {
const dictStore = useDictStore();
// 清除缓存
wsCache.delete(CACHE_KEY.DICT_CACHE);
// 重新加载字典数据
await dictStore.setDictMap();
}
2. Demo
以下主要是围绕第三章的实战中的Demo,提供的思路
前端与后端通信:使用 Vue3 和 Axion 发送 HTTP 请求到 Java 后端,获取字典数据
状态管理:使用 Pinia 管理字典数据,确保全局状态的唯一性和一致性
前端缓存:通过自定义缓存钩子,将字典数据存储在 sessionStorage 中,减少对后端的请求
组件化开发:创建可复用的字典选择组件,提升代码的可维护性和扩展性
动态数据加载:在应用初始化时加载字典数据,支持动态生成路由和菜单
项目结构:
src/
├── api/
│ └── dict.ts # 后端 API 接口声明
├── hooks/
│ └── web/
│ └── useCache.ts # 缓存钩子
├── store/
│ └── modules/
│ └── dict.ts # Pinia Store 定义
├── components/
│ └── DictSelect.vue # 字典选择组件
└── main.ts # 应用入口
定义Api接口:
// src/api/dict.ts
export interface DictDataVO {
dictType: string;
value: string;
label: string;
colorType?: string;
cssClass?: string;
}
export async function getSimpleDictDataList(): Promise<DictDataVO[]> {
return await request({
url: '/api/system/dict/simple',
method: 'GET'
});
}
- 创建字典状态管理 Store:
// src/store/modules/dict.ts
import { defineStore } from 'pinia';
import { CACHE_KEY, useCache } from '@/hooks/web/useCache';
const { wsCache } = useCache('sessionStorage');
import { getSimpleDictDataList } from '@/api/dict';
export interface DictState {
dictMap: Map<string, any>;
isSetDict: boolean;
}
export const useDictStore = defineStore('dict', {
state: () => ({
dictMap: new Map<string, any>(),
isSetDict: false
}),
getters: {
getDictMap: state => state.dictMap,
getIsSetDict: state => state.isSetDict
},
actions: {
async setDictMap() {
if (this.isSetDict) return;
const cachedDict = wsCache.get(CACHE_KEY.DICT_CACHE);
if (cachedDict) {
this.dictMap = cachedDict;
this.isSetDict = true;
return;
}
try {
const response = await getSimpleDictDataList();
const dictDataMap = new Map<string, any>();
response.forEach(dictData => {
if (!dictDataMap.has(dictData.dictType)) {
dictDataMap.set(dictData.dictType, []);
}
dictDataMap.get(dictData.dictType)?.push({
value: dictData.value,
label: dictData.label,
colorType: dictData.colorType,
cssClass: dictData.cssClass
});
});
this.dictMap = dictDataMap;
this.isSetDict = true;
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 });
} catch (error) {
console.error('Failed to fetch dictionary data:', error);
}
},
getDictByType(type: string) {
return this.dictMap.get(type) || [];
}
}
});
- 创建字典选择组件:
<!-- src/components/DictSelect.vue -->
<template>
<div class="dict-select">
<label>{{ label }}</label>
<select v-model="selectedValue" @change="handleChange">
<option v-for="(item, index) in dictOptions" :key="index" :value="item.value">
{{ item.label }}
</option>
</select>
</div>
</template>
<script setup lang="ts">
import { useDictStore } from '@/store/modules/dict';
interface DictSelectProps {
dictType: string;
label?: string;
modelValue?: string;
}
const props = defineProps<DictSelectProps>();
const emit = defineEmits(['update:modelValue']);
const dictStore = useDictStore();
const dictOptions = dictStore.getDictByType(props.dictType);
const selectedValue = ref(props.modelValue);
const handleChange = (e: Event) => {
const value = (e.target as HTMLSelectElement).value;
selectedValue.value = value;
emit('update:modelValue', value);
};
</script>
<style scoped>
.dict-select {
margin: 10px 0;
}
select {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
- 在主应用中初始化字典数据:
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import pinia from './store';
import { useDictStore } from './store/modules/dict';
const app = createApp(App)
.use(router)
.use(pinia);
router.isReady().then(() => {
app.mount('#app');
// 初始化字典数据
const dictStore = useDictStore();
dictStore.setDictMap();
});
- 使用字典选择组件:
<!-- src/App.vue -->
<template>
<div id="app">
<h1>字典数据管理示例</h1>
<dict-select
dictType="user_status"
label="用户状态"
v-model:modelValue="selectedStatus"
/>
</div>
</template>
<script setup lang="ts">
import DictSelect from './components/DictSelect.vue';
const selectedStatus = ref('0');
</script>
3. 实战
以下实战来源:https://gitee.com/zhijiantianya/ruoyi-vue-pro
附上基本的代码注释讲解:
权限管理模块:
import router from './router'
import type { RouteRecordRaw } from 'vue-router'
import { isRelogin } from '@/config/axios/service'
import { getAccessToken } from '@/utils/auth'
import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress'
import { usePageLoading } from '@/hooks/web/usePageLoading'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useUserStoreWithOut } from '@/store/modules/user'
import { usePermissionStoreWithOut } from '@/store/modules/permission'
// 初始化进度条和页面加载状态
const { start, done } = useNProgress()
const { loadStart, loadDone } = usePageLoading()
/**
* 解析孔(URL)为路径和参数对象
* @param url 输入的完整URL
* @returns 包含 basePath 和 paramsObject的对象
*/
const parseURL = (
url: string | null | undefined
): { basePath: string; paramsObject: { [key: string]: string } } => {
// 如果输入为 null 或 undefined,返回空字符串和空对象
if (url == null) {
return { basePath: '', paramsObject: {} }
}
// 找到问号的位置,分割基础路径和查询参数
const questionMarkIndex = url.indexOf('?')
let basePath = url
const paramsObject: { [key: string]: string } = {}
// 如果有查询参数,进行解析
if (questionMarkIndex !== -1) {
basePath = url.substring(0, questionMarkIndex)
const queryString = url.substring(questionMarkIndex + 1)
const searchParams = new URLSearchParams(queryString)
searchParams.forEach((value, key) => {
paramsObject[key] = value
})
}
// 返回解析后的结果
return { basePath, paramsObject }
}
// 不需要重定向的白名单路径
const whiteList = [
'/login',
'/social-login',
'/auth-redirect',
'/bind',
'/register',
'/oauthLogin/gitee'
]
// 路由加载前的钩子函数
router.beforeEach(async (to, from, next) => {
start() // 开始进度条
loadStart() // 开始页面加载
if (getAccessToken()) { // 检查是否有访问令牌
if (to.path === '/login') {
// 已经登录的情况下,重定向到主页
next({ path: '/' })
} else {
// 获取字典、用户、权限仓库
const dictStore = useDictStoreWithOut()
const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStoreWithOut()
// 检查字典是否加载完成
if (!dictStore.getIsSetDict) {
await dictStore.setDictMap() // 如果没有加载过,加载字典数据
}
// 检查用户信息是否加载完成
if (!userStore.getIsSetUser) {
isRelogin.show = true // 显示重新登录提示
await userStore.setUserInfoAction() // 加载用户信息
isRelogin.show = false
// 加载权限路由
await permissionStore.generateRoutes()
permissionStore.getAddRouters.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw) // 动态添加路由
})
// 处理重定向路径
const redirectPath = from.query.redirect || to.path
const redirect = decodeURIComponent(redirectPath as string)
const { basePath, paramsObject: query } = parseURL(redirect)
// 根据情况决定是否替换当前路由
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect, query }
next(nextData)
} else {
next() // 用户信息已加载,继续导航
}
}
} else {
// 没有登录,检查是否在白名单中
if (whiteList.indexOf(to.path) !== -1) {
next() // 白名单路径,允许访问
} else {
// 重定向到登录页,并携带当前路径作为 redirect 参数
next(`/login?redirect=${to.fullPath}`)
}
})
// 路由加载完成后的钩子函数
router.afterEach((to) => {
useTitle(to?.meta?.title as string) // 更新页面标题
done() // 结束进度条
loadDone() // 结束页面加载状态
})
字典管理模块:
import { defineStore } from 'pinia'
import { store } from '../index'
import { DictDataVO } from '@/api/system/dict/types'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache('sessionStorage')
import { getSimpleDictDataList } from '@/api/system/dict/dict.data'
/**
* 字典值的类型定义
*/
export interface DictValueType {
value: any
label: string
colorType?: string
cssClass?: string
}
/**
* 字典类型的类型定义
*/
export interface DictTypeType {
dictType: string
dictValue: DictValueType[]
}
/**
* 字典状态类型定义
*/
export interface DictState {
dictMap: Map<string, any>
isSetDict: boolean
}
/**
* 定义字典 Pinia Store
*/
export const useDictStore = defineStore('dict', {
state: (): DictState => ({
dictMap: new Map<string, any>(), // 字典映射表
isSetDict: false // 表示字典是否已加载
}),
getters: {
/**
* 获取字典映射表,从缓存中读取
*/
getDictMap(): Recordable {
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
if (dictMap) {
this.dictMap = dictMap
}
return this.dictMap
},
/**
* 检查字典是否已加载
*/
getIsSetDict(): boolean {
return this.isSetDict
}
},
actions: {
/**
* 设置字典映射表,从缓存或API获取
*/
async setDictMap() {
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
if (dictMap) {
this.dictMap = dictMap
this.isSetDict = true
} else {
const res = await getSimpleDictDataList()
// 构建字典数据映射
const dictDataMap = new Map<string, any>()
res.forEach((dictData: DictDataVO) => {
// 按照 dictType 分组
if (!dictDataMap.has(dictData.dictType)) {
dictDataMap.set(dictData.dictType, [])
}
// 添加字典值
dictDataMap.get(dictData.dictType)?.push({
value: dictData.value,
label: dictData.label,
colorType: dictData.colorType,
cssClass: dictData.cssClass
})
})
// 更新状态和缓存
this.dictMap = dictDataMap
this.isSetDict = true
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 缓存60秒
}
},
/**
* 根据字典类型获取对应的字典值
* @param type 字典类型
*/
getDictByType(type: string) {
if (!this.isSetDict) {
this.setDictMap() // 如果未加载,先加载字典
}
return this.dictMap[type]
},
/**
* 重置字典数据,清空缓存并重新加载
*/
async resetDict() {
wsCache.delete(CACHE_KEY.DICT_CACHE) // 清空缓存
const res = await getSimpleDictDataList()
const dictDataMap = new Map<string, any>()
res.forEach((dictData: DictDataVO) => {
// 重新构建字典映射
if (!dictDataMap.has(dictData.dictType)) {
dictDataMap.set(dictData.dictType, [])
}
dictDataMap.get(dictData.dictType)?.push({
value: dictData.value,
label: dictData.label,
colorType: dictData.colorType,
cssClass: dictData.cssClass
})
})
this.dictMap = dictDataMap
this.isSetDict = true
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 更新缓存
}
}
})
/**
* 提供一个不带 store 的字典 Store 实例
*/
export const useDictStoreWithOut = () => {
return useDictStore(store)
}