前言:
项目中,当每一个角色得到的界面不一致的时候,我们就不能使用静态菜单了,而是要从后端得到动态的菜单数据,然后动态的将菜单数据展示在界面上。
除了在界面展示,也还要将界面的路由动态添加,在路由动态添加之后,你可能会出现刷新界面,界面变白的情况,页面刷新白屏其实是因为vuex引起的,由于刷新页面vuex数据会丢失,所以动态添加路由这一步也就失效了。这种情况我会在最后给一个处理方法。
所以这个博客会做两个部分的处理:
- 动态菜单生成
- 动态添加路由
动态菜单生成
1.获得后端数据(有mock模拟数据,也可以使用后端接口)
1.1使用mock得到模拟数据
没有下载mock的可以查看:Vue项目中使用mockjs实现mock模拟数据 – ykCoder – 博客园 (cnblogs.com)
mock/modules/menu.js 保存模拟的后端数据
function list(res) {// res是一个请求对象,包含: url, type, bodyreturn {code: 200,message: "请求成功",//菜单数据,可以修改成你自己要的菜单data: [{id: "600d4075e218daaf4ec77e50",menuType: "1",menuName: "首页",path: "/Home",icon: "house",},{id: "600d4075e218daaf4ec77e51",menuType: "1",menuName: "公司管理",path: "/company",icon: "location",children: [{id: "600d525e602f452aaeeffcd9",menuType: "1",menuName: "公司资料",path: "/company/Company",},{id: "601bc4f8a794e23c2e42efa9",menuType: "1",menuName: "个人资料",path: "/company/Person",},],},};}//暴露listexport default { list };
mock/index.js 引入mock/menu.js
// 引入mockjsimport Mock from 'mockjs'// 引入模板函数类import menu from './modules/menu'// Mock函数const { mock } = Mock// 设置延时Mock.setup({timeout: 400})// 使用拦截规则拦截命中的请求,mock(url, post/get, 返回的数据);Mock.mock('/mock/menu', 'get', menu.list)
在界面引用
export default {data() {return {menuData:[],};},methods: {getMenu() {this.$http.get('/mock/menu').then((res) => {console.log(res)if (res.data.code === 200) {this.menuData = res.data.data;// console.log(this.menuData,"menuData")//获取菜单的数据,存入store中this.$store.commit("setMenu",this.menuData)//动态生成路由this.$store.commit("addMenu",this.$router)}})},handleOpen(key, keyPath) {console.log(key, keyPath);},handleClose(key, keyPath) {console.log(key, keyPath);},},};
1.2连接后端接口(统一接口管理),从后端得到菜单数据
如果对统一接口管理,有不明白的可以查看:
016-尚硅谷-尚品汇-API接口统一管理_哔哩哔哩_bilibili
Vue封装接口思路(包括请求(响应拦截器))_vue接口封装_忧郁火龙果的博客-CSDN博客
api/request.js
import axios from 'axios';//1.利用axios对象的方法create,去创建一个axios实例。const requests = axios.create({//配置对象//接口当中:路径都带有/api 基础路径,发送请求的时候,路径当中会出现apibaseURL:"/api",//代表请求超时的时间timeout:5000,})//请求拦截器:requests.interceptors.request.use((config) =>{//config:配置对象,对象里面有一个属性很重要,header请求头return config;})//响应拦截器requests.interceptors.response.use((res)=>{//成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测,可以做一些事情return res.data;},(error)=>{//失败的回调函数return Promise.reject(new Error('faile'));})//对外暴露export default requests;
api/menu.js
import requests from "./request";export const menuList = (data) => {return requests({url: "/user/menus",method: 'GET',data: data,});};
在界面引用
import { menuList } from "@/api/menu.js";export default {data() {return {menuData:[],};},methods: {getMenu() {const id = 1;//假数据const res = menuList(id);console.log(res.data);if (res.code == 200) {//获得菜单导航数据this.menuData = res;} else {//没有获得菜单数据}},},};
2.接收界面数据,实现动态界面
2.1界面实现
我的菜单界面是用两个vue文件写的。组件间的传值要用vuex,这里建议去看一下官网学习一下。
组件间的传值:Vue组件之间的传值 – 掘金 (juejin.cn)
HomeMenu.vue
这里实现二级菜单用的是递归的方法,这个是我觉得很神奇的地方,我第一次在vue中使用到了递归。在此之前我觉得vue就只能实现做界面的功能,没有想到过vue也可以这么灵活。
//在组件中调用组件本身,使用递归的方法实现二级目录。
全部代码
0 &&item.children[0].menuType.toString() === '1'">{{ item.menuName }}{{ item.menuName }}export default {name: "home-menu",//为了实现组件间的传值props: ["menuData"],methods: {//点击菜单clickMenu(item) {console.log("item:" + item);//当前路由与跳转路由不一致时跳转if (this.$route.path !== item.path && !(this.$route.path === '/home' && (item.path === '/'))) {this.$router.push(item.path);}},},};
HomeAside.vue
{{ isCollapse ? "排班" : "智能排班系统" }}
import HomeMenu from "@/components/menu/HomeMenu.vue";import { thisTypeAnnotation } from "@babel/types";import { mapState } from 'vuex';export default {components: {"home-menu": HomeMenu,},data() {return {menuData:[],};},mounted() {//获得菜单this.getMenu();},computed: {//给store传递menuData的值...mapState({menuData: (state) => state.menu.menuData}),},methods: {getMenu() {this.$http.get('/mock/menu').then((res) => {console.log(res)if (res.data.code === 200) {this.menuData = res.data.data;// console.log(this.menuData,"menuData")//获取菜单的数据,存入store中this.$store.commit("setMenu",this.menuData)//动态生成路由this.$store.commit("addMenu",this.$router)}})},handleOpen(key, keyPath) {console.log(key, keyPath);},handleClose(key, keyPath) {console.log(key, keyPath);},},}; .el-menu-vertical-demo:not(.el-menu--collapse) {width: 200px;min-height: 400px;}.el-menu {height: 100vh;border-right: none;h3 {color: #fff;text-align: center;line-height: 48px;font-size: 16px;font-weight: 400px;}}
2.2组价间传值的使用
store/menu.js这里的addMenu函数值实现动态路由的关键,在下面会有分析
export default {state: {// 动态菜单menuData: [],},//修改字段mutations: {//设置菜单的数据setMenu(state, val) {state.menuData = val;},//动态注册路由addMenu(state, router) {// 处理动态路由的数据const menuData = JSON.parse(JSON.stringify(state.menuData));const menuArray = [];menuData.forEach((item) => {if (item.children && item.children.length >= 1) {menuArray.push(...item.children);} else {menuArray.push(item);}});console.log(menuArray, "menuArray");// 路由的动态添加if (menuArray[0] !== "") {menuArray.forEach((item) => {router.addRoute("main", { path: `${item.path}`,component: () => import(`@/views${item.path}.vue`) });});}},},};
menu/index.js
import { createStore } from 'vuex'import createPersistedState from "vuex-persistedstate"import menu from './menu'export default createStore({state: {},getters: {},mutations: {},actions: {},modules: {menu},/* vuex数据持久化配置 */plugins: [createPersistedState({// 存储方式:localStorage、sessionStorage、cookiesstorage: window.sessionStorage,// 存储的 key 的key值key: "store",reducer(state) { //render错误修改// 要存储的数据:本项目采用es6扩展运算符的方式存储了state中所有的数据return { ...state };}})]})
3.实现动态路由
3.1实现动态路由的代码分析
上面的代码已经实现了动态路由,这里是解释一下动态路由的关键上面,网上关于动态路由的代码很多,但是对于第一次做动态路由的人来说,想要去看懂事有点难度的。
创建一个新的空数组,遍历menuData的时候根据元素有没有children来分别处理,将需要的数据保存到新数组中,通过传入的path路径来添加路由。
最重要的部分来了,vue router4的版本不再使用router.addRoutes而是router.addRoute,这个地方建议看官方文档
动态路由 | Vue Router (vuejs.org)
router.addRoute("main", { path: `${item.path}`,component: () => import(`@/views${item.path}.vue`) });
//动态注册路由addMenu(state, router) {// 处理动态路由的数据const menuData = JSON.parse(JSON.stringify(state.menuData));const menuArray = [];menuData.forEach((item) => {if (item.children && item.children.length >= 1) {menuArray.push(...item.children);} else {menuArray.push(item);}});console.log(menuArray, "menuArray");// 路由的动态添加if (menuArray[0] !== "") {menuArray.forEach((item) => {router.addRoute("main", { path: `${item.path}`,component: () => import(`@/views${item.path}.vue`) });});}},
下面贴一下vue2项目的写法,这里我使用时先把元素中添加component属性,这里和上面的写法有点不一样,但是我建议还是用上面的好一点,这个写法可能会出bug。
item.component =(resolve) => require([`@/views/home/${item.url}`], resolve)
//动态注册路由addMenu(state, router) {// 处理动态路由的数据const menuArray = []state.menuData.forEach(item => {if (item.children) {item.children = item.children.map(item => {item.component = (resolve) => require([`@/views/home/${item.url}`], resolve)return item})menuArray.push(...item.children)} else {item.component =(resolve) => require([`@/views/home/${item.url}`], resolve)menuArray.push(item)}})console.log(menuArray, 'menuArray')// 路由的动态添加menuArray.forEach(item => {router.addRoute('main', item)})},
还有一个处理方法,你可以在传过来的数据中就传path,component的值,最后直接使用
router.addRoute() 调用就行了。
在HomeAside.vue界面之间的引用
//获取菜单的数据,存入store中this.$store.commit("setMenu",this.menuData)//动态生成路由this.$store.commit("addMenu",this.$router)
3.2刷新界面后,白屏处理
恭喜你,来到最后一步,在最前面的时候说过,白屏问题是因为vuex引起的,由于刷新页面vuex数据会丢失,所以动态添加路由这一步也就失效了。我在网上找了很多方法都没有解决,最后回归到问题的本质,刷新界面vuex的数据会丢失,那么我们不让数据丢失不就行了,我的处理方法是在
mian.ts中再保存一遍路由的数据。
这个是vue3的处理:
import { createApp } from "vue";import App from "./App.vue";import router from "./router";import store from "./store";import ElementPlus from "element-plus";import "element-plus/dist/index.css";import * as ElIconModules from "@element-plus/icons";import '@/mock';import axios from 'axios';import VueAxios from 'vue-axios';//动态菜单路由的生成const addMenu = () => {store.commit("addMenu",router)}addMenu()const app = createApp(App);app.use(store).use(router).use(ElementPlus).use(VueAxios,axios).mount("#app");// 统一注册Icon图标for (const iconName in ElIconModules) {if (Reflect.has(ElIconModules, iconName)) {const item = ElIconModules[iconName];app.component(iconName, item);}}
这个是vue2的处理:
import Vue from 'vue';import router from './router'import store from './store'import App from './App.vue'import ElementUI from 'element-ui'import 'element-ui/lib/theme-chalk/index.css'Vue.use(ElementUI);import axios from 'axios'//配置请求根路径axios.defaults.baseURL = "http://localhost:8088" //将axios作为全局的自定义属性,每个组件可以在内部组件访问Vue.prototype.$http = axios//添加全局前置导航守卫router.beforeEach((to, from, next) => {//判断token是否存在// localStorage.setItem("token", message.data.token);const token = localStorage.getItem("token");// localStorage.clear();console.log(token,'token')if( !token && to.name !== 'login' ){//token不存在,没有登录next({ name : 'login' })} else {next();}})new Vue({router,store,el: '#app',created() {store.commit('addMenu',router)},render: h => h(App)});
好了,最后希望大家都能看懂,如果有什么问题可以提出来。