需求

前端页面用户登录成功时,接口返回用户菜单,菜单里面包含新增的路由信息,将菜单解析成新增路由,并将其添加到router里面,以及进行持久化操作

配置alias

vue项目在经过编译打包之后,后期无法使用import导入组件,并且项目路径发生了改变,也无法简单使用require动态导入组件,这里解决方法是配置alias,同时用resolve配合require一起使用。

配置如下:

安装webpack(已安装则跳过),这里可以直接复制以下代码到package.json文件中的"devDependencies"属性中,重新install即可。

    "webpack": "^5.37.0",
    "webpack-bundle-analyzer": "^4.5.0",
    "webpack-cli": "^4.9.0",
    "webpack-dev-server": "^3.11.2",
    "webpack-merge": "^5.7.3",
    "webpackbar": "^5.0.0-3

复制以下代码到vue.config.js配置文件中,可以使用开启alia功能,使用@符号替代src

const { defineConfig } = require('@vue/cli-service')

const path = require('path');//引入path模块

function resolve(dir){
  return path.join(__dirname,dir)//path.join(__dirname)设置绝对路径
}

module.exports = defineConfig({
  transpileDependencies: true,
  devServer:{
    port:8080,
    //port:80,
    // proxy: {                 //设置代理,必须填
    //     '/api': {              //设置拦截器  拦截器格式   斜杠+拦截器名字,名字可以自己定
    //         target: 'http://localhost:9999',     //代理的目标地址(后端设置的端口号)
    //         changeOrigin: true,              //是否设置同源,输入是的
    //         pathRewrite: {                   //路径重写
    //             '/api': ''                     //选择忽略拦截器里面的单词
    //         }
    //         /*也就是在前端使用/api可以直接替换为(http://localhost:9090)*/
    //     }
    // }
  },
  chainWebpack:(config)=>{
    config.resolve.alias
        .set('@',resolve('./src'))
        .set('components',resolve('./src/components'))
        .set('views',resolve('./src/views'))
        .set('assets',resolve('./src/assets'))
    //set第一个参数:设置的别名,第二个参数:设置的路径
  },
  publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
  outputDir: 'dist',
  assetsDir: 'static',
  productionSourceMap: false,
  lintOnSave: false
})

router配置

src/router/index.js文件

import Router from 'vue-router'
import store from "../store/index";

//菜单根页面
const menuRoot={
    path:'/home',
    name:'home',
    component:()=>import('@/views/home/Home'),
    redirect:'/home/index',
    meta:{
        title: '首页',
    }
}

//最基本的路由
const commonRouter=[
    {
        path: "/",
        redirect: "/login"
    },
    {
        path:'/login',
        name:'login',
        component:()=>import('@/views/login/Login.vue'),
        meta:{
            title:'登录'
        }
    },
    {
        path:'/404',
        name:'404',
        component:()=>import('@/views/error/404'),
        meta:{
            title:'404'
        }
    }
]

//这个路由在接口返回之后再添加,不然会出现页面刷新之后直接重定向到404,没机会添加动态路由
const Router_404={
    path:'*',
    redirect: '/404'
}


//将的菜单树结构转换成路由,直接滑到下面看一下接口返回值格式更容易理解
//menus:登录成功时返回的menus
//res:接口返回值
//parentPath:父级路径多级拼接而成,分隔符为 /
//parentName:父级路径名,多级拼接而成,分隔符为 -
const menuRouter=function(menus,res,parentPath,parentName){
    res=res||[];
    menus&&menus.forEach(elem=>{
        const path=!parentPath?elem.path:parentPath+'/'+elem.path;
        const name=!parentName?elem.name:parentName+'-'+elem.name;
        if(!elem.children){
            let obj={...elem};
            obj.path=path;
            obj.name=name;
            obj.component=resolve=>(require([`components/${elem.component}`],resolve));
            //注意这里的components是上面配置的alias路径别名,是路径./src/components的别名,新增的组件我都放在了该路径下,可以自己修改alias里面映射的路径
            res.push(obj);
        }else{
            menuRouter(elem.children,res,path,name);
        }
    })
    return res;
}


//创建最基本的路由
const createRouter = () => {
    return new Router({
        // mode: 'history', // require service support
        mode:'history',
        scrollBehavior: () => ({ y: 0 }),
        routes: commonRouter
    })
}

let router=createRouter();

//重置路由
export function resetRouter() {
    const newRouter = createRouter()
    router.matcher = newRouter.matcher // reset router
}

//根据用户菜单创建存在登录状态的用户路由,menus:登录接口返回的菜单
export async function createMenuRouter(menus){
    //重置路由
    resetRouter();
    return new Promise((resolve)=>{
        
        const menuRouterTemp={...menuRoot};
        
        menuRouterTemp.children=menuRouter(menus);
        
        //添加新增的路由
        router.options.routes.push(menuRouterTemp);
        
        //添加新增的路由
        router.addRoute(menuRouterTemp);
        
        //添加path匹配不到时重定向到404路由
        router.addRoute(Router_404);
        
        /*
        	如果要添加根路由:
        				1.router.options.routes.push(新增的根路由)
        				2.router.addRoute(新增的根路由)
        				
        	如果要往一个指定路由中添加子路由,需要进行两步操作:
        				1.router.options.routes中找到该指定路由(dfs或bfs)
        				2.往该路由的children里面push你要添加的子路由
        				3.找到该指定路由的根路由,添加新增的子路由,使用router.addRoute重新载入该路由
        */
        resolve();
    })
}

router.beforeEach((to,from,next)=>{
    if(to.path==='/login'||to.path==='/404') {
        next();
    }else{
        if(store.getters.token){
            let menuRouterIsExist=false,item=router.options.routes;
            for(let index in item){
                if(item[index].name==='home'){
                    menuRouterIsExist=true;
                    break;
                }
            }
            //如果Cookie中有新增的路由
            if(!menuRouterIsExist){
                //重新载入新增的路由
                createMenuRouter(store.getters.menus).then(()=>{
                    //载入成功,替换页面,放行
                    next({...to, replace:true})
                }).catch(()=>{
                    //载入失败
                    next('/login');
                })
            }else{
                //有新增的路由,放行
                next();
            }
        }else{
            next('/login');
        }
    }
})

router.afterEach((to)=>{
    if(!to.meta.title){
        document.title = to.meta.title //修改网页的title
    }
})

export default router;

main.js中导入路由

...
import router from "./router";
...
new Vue({
  render: h => h(App),
  router,
  store,
  beforeCreate() {
    Vue.prototype.$bus=this;
  }
}).$mount('#app')

路由持久化

安装js-cookie

npm install --save js-cookie

将接口返回的路由信息存到Vuexcookie

src/store/moudles/user.js文件:

import request from "../../http/request";
import Cookies from 'js-cookie'
import {createMenuRouter} from "@/router";

const state={
    token:Cookies.get('Authorization')||null,
    name:'',
    account:'',
    roles:[],
    menus:Cookies.get('menus')?JSON.parse(Cookies.get('menus')):[]
}

const mutations={
    setToken(state,token){
        state.token=token;
    },
    setName(state,name){
        state.name=name;
    },
    setAccount(state,account){
        state.account=account
    },
    setRoles(state,roles){
        state.roles=roles
    },
    setMenus(state,menus){
        state.menus=menus;
    }
}

const actions={
	//登录
    async login({commit},{account,password}){
        return new Promise((resolve,reject)=>{
            request.post('user/login',{
                account,
                password
            }).then(res=>{
                if(res.code===200){
                    Cookies.set('Authorization',res.data.token,{expires:1});
                    Cookies.set('menus',JSON.stringify(res.data.menus),{expires:1});
                    commit('setToken',res.data.token);
                    commit('setMenus',res.data.menus);
                    //更新路由
                    createMenuRouter(res.data.menus)
                }
                resolve({
                    code:res.code,
                    msg:res.msg
                })
            }).catch(error=>{
                reject(error)
            })
        })
    }
}

export default {
    namespaced:true,
    state,
    mutations,
    actions
}

src/store/getters.js文件:

const getters={
    token:state=>state.user.token,
    name:state=>state.user.name,
    account:state=>state.user.account,
    roles:state=>state.user.roles,
    menus:state=>state.user.menus
}
export default getters

src/store/index.js文件:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)


import getters from "./getters";

import user from './moudules/user'



const modules= {
    user
}

export default new Vuex.Store({
    modules,
    getters
})

登录模块

	  this.loginLoading=true;
      this.$store.dispatch('user/login',this.loginForm).then(res=>{
        if(res.code===200){
          this.$message.success(res.msg);
          setTimeout(()=>{
            router.push('/home');
          },200);
        } else{
          this.$message.error(res.msg);
        }
        this.loginLoading=false;
      }).catch(()=>{
        this.$message.error("登录失败");
        this.loginLoading=false;
      })

接口返回值

user/login接口返回值如下
menus数据里面只有叶子节点有component属性,最后会将多级菜单压缩成一层路由,并将该层路由赋值给指定路由的children属性
menus数据还用于渲染管理页面的左侧菜单

{
  code: 200,
  msg: "登录成功",
  data: {
    token:'abcdefg',
    menus: [
            {
                "path": "index",
                "name": "indexxx",
                "component": "Index",
                "meta": {
                    "title": "首页",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "menu1",
                "name": "menu1",
                "meta": {
                    "title": "多级菜单1",
                    "icon": "el-icon-news"
                },
                "children": [
                    {
                        "path": "subMenu1",
                        "name": "subMenu1",
                        "component": "TestOne",
                        "meta": {
                            "name": "测试名1",
                            "title": "测试标题",
                            "icon": "el-icon-location"
                        }
                    },
                    {
                        "path": "subMenu2",
                        "name": "subMenu2",
                        "meta": {
                            "name": "测试名2",
                            "title": "测试标题2",
                            "icon": "el-icon-location"
                        },
                        "children": [
                            {
                                "path": "subMenu1",
                                "name": "subMenu1",
                                "component": "TestOne",
                                "meta": {
                                    "name": "测试名1",
                                    "title": "测试标题",
                                    "icon": "el-icon-location"
                                }
                            },
                            {
                                "path": "subMenu2",
                                "name": "subMenu2",
                                "component": "TestTwo",
                                "meta": {
                                    "name": "测试名2",
                                    "title": "测试标题2",
                                    "icon": "el-icon-location"
                                }
                            }
                        ]
                    }
                ]
            },
            {
                "path": "index12",
                "name": "index12",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index122",
                "name": "index122",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index123",
                "name": "index123",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index124",
                "name": "index124",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index125",
                "name": "index125",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index126",
                "name": "index126",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index127",
                "name": "index127",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index128",
                "name": "index128",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index129",
                "name": "index129",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index1211",
                "name": "index1211",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index1212",
                "name": "index1212",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            },
            {
                "path": "index1213",
                "name": "index1213",
                "component": "Index",
                "meta": {
                    "title": "首页2",
                    "icon": "el-icon-location"
                }
            }
        ]
    }
}

菜单组件

<template>
  <div class="menu-wrapper">
    <template v-for="item in menu">
      <!-- 最后一级菜单 -->
      <el-menu-item
          v-if="!item.children"
          :key="item.path"
          :index="parent ? parent + '/' + item.path : item.path"
      >
        <i :class="item.meta.icon" class="iconStyle"></i>
        <span slot="title">{{ item.meta.title.trim() }}</span>
      </el-menu-item>

      <!-- 此菜单下还有子菜单 -->
      <el-submenu
          v-if="item.children"
          :key="item.path"
          :index="parent ? parent + '/' + item.path : item.path">
        <template slot="title">
          <i :class="item.meta.icon" class="iconStyle"></i>
          <span slot="title">{{ item.meta.title.trim() }}</span>
        </template>
        <!-- 递归 -->
        <sidebar-item
            :menu="item.children"
            :parent="parent ? parent + '/' + item.path : item.path"/>
      </el-submenu>
    </template>
  </div>
</template>

<script>
export default {
  name: "SidebarItem",
  props: ["menu", "parent"]
};
</script>

<style scoped>
.iconStyle{
  font-size: 20px;
  padding-right: 10px;
  color: rgb(191, 203, 217);
}

.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 100%;
}
/*隐藏文字*/
.el-menu--collapse  .el-submenu__title span{
  display: none;
}
/*隐藏 > */
.el-menu--collapse  .el-submenu__title .el-submenu__icon-arrow{
  display: none;
}

:deep(.el-menu-item:hover),:deep(.el-submenu__title:hover){
  background-color: #1f2d3d !important;
}
</style>

<el-menu  :collapse="asideIsCollapse"
              collapse-transition="collapse-transition"
              text-color="rgb(191, 203, 217)"
              active-text-color="rgb(64, 158, 255)"
              router
              background-color="rgb(48, 65, 86)">
      <SidebarItem :menu="this.$store.getters.menus" parent="/home"></SidebarItem>
</el-menu>

效果演示

该项目存在一定的保密性,所以这里只能放出动态路由代码,这里给出效果演示
如图所示:login界面有三个根路由,这个404路由基本上用不到,也可以登录成功时再加上去
在这里插入图片描述
如图所示:这是登录成功之后的页面
在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐