当前位置: 首页 > news >正文

vue3 前端路由权限控制与字典数据缓存实践(附Demo)

目录

  • 前言
  • 1. 基本知识
  • 2. Demo
  • 3. 实战

前言

🤟 找工作,来万码优才:👉 #小程序://万码优才/r6rqmzDaXpYkJZF在这里插入图片描述

从实战中出发:

1. 基本知识

Vue3 和 Java 通信时如何进行字典数据管理

需要了解字典数据的结构。通常,字典数据包含一个键和一个值,有时候还有一些额外的属性,比如颜色类型或 CSS 类,这些在前端展示时可能会用到

库来发送 HTTP 请求到后端,获取这些字典数据。

需要分析如何在前端存储和管理这些字典数据。Pinia 是一个推荐的 Vue
通过 Pinia,可以定义一个字典 store,用来存储所有获取到的字典数据。这样,所有组件都可以方便地访问这些数据,而不需要每次都通过 API 获取.另外,前端缓存也是一个重要的考虑因素。由于字典数据通常是静态的或变化不大,可以在前端缓存这些数据,减少对后端的请求次数,提高应用的响应速度。Vue 的hooks,比如 useCache

如何在路由和权限控制中使用这些字典数据

在用户登录后,前端需要获取字典数据以完成菜单加载和路由动态生成。这里需要与 Vue Router 结合,根据字典数据生成对应的路由配置,确保用户只能访问其权限范围内的功能。还需要处理字典数据的更新和缓存的失效。如果字典数据有更新,前端需要有一个机制来刷新缓存,确保用户使用的是最新的数据。这可能涉及到设置缓存的有效期,或者在有更新时手动清除缓存并重新获取数据

字典数据管理是一个重要的组成部分,字典数据通常包括各种下拉列表、状态标识等,用于在页面中展示和交互

特别是当使用 Vue3 与 Java 后端进行通信时,字典数据的获取、存储、管理和reload都成为关键点

  1. 字典数据通常包括以下几个要素:
  • 字典类型(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": "禁用"
  },
  // 其他字典项
]
  1. 使用 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();
  1. 前端缓存可以减少对后端的请求,提高响应速度。可以存储在 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);
  1. 字典数据的获取与初始化

在用户登录后,前端需要获取字典数据并存储到 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);
  }
}
  1. 在组件中使用字典数据

在需要使用字典数据的组件中,可以轻松访问 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'
  });
}
  1. 创建字典状态管理 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) || [];
    }
  }
});
  1. 创建字典选择组件:
<!-- 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>
  1. 在主应用中初始化字典数据:
// 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();
});
  1. 使用字典选择组件:
<!-- 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)
}

相关文章:

  • STM32F407 cubeIDE Bootloader APP 如何写
  • 【从零开始学习计算机科学】数据库系统(二)关系数据库 与 关系代数
  • AI学习——深度学习核心技术深度解析
  • 时间序列预测(十九)——卷积神经网络(CNN)在时间序列中的应用
  • g++链接及动态库和静态库浅析
  • 2025年Java面试题目收集整理归纳(持续更新)
  • 模板(初阶)
  • Java 浅拷贝和深拷贝
  • 【空间插值】地理加权回归模型(GWR, Geographically Weighted Regression)
  • Windows 发票闪印 PrintPDF-v3.6.10-第三方发票打印辅助工具,无需安装阅读器即可使用
  • 使用 ESP32 和 Python 进行手势识别
  • 蓝桥与力扣刷题(蓝桥 等差数列)
  • Word中把参考文献引用改为上标
  • Linux上位机开发实战(按钮响应)
  • AI绘画软件Stable Diffusion详解教程(10):图生图进阶篇(局部手绘修正)
  • Python 正则表达式模块 re
  • 「基于大模型的智能客服系统」语义理解、上下文记忆与反馈机制设计
  • 实现悬浮按钮拖动,兼容h5和微信小程序
  • LinPEAS 使用最佳实践指南
  • Profinet转Profinet以创新网关模块为核心搭建西门子和欧姆龙PLC稳定通讯架构案例​
  • 广东水利全面升级洪水和泄洪预警发布机制
  • 第78届世界卫生大会20日审议通过“大流行协定”
  • 改造老旧小区、建立“一张图”,五部委将多举措支持城市更新
  • 益阳通报“河水颜色异常有死鱼”:未发现排污,原因待鉴定
  • 国家统计局:消费对我国经济增长的拉动有望持续增长
  • 美国务卿会见叙利亚外长,沙特等国表示将支持叙利亚重建