前端实现动态路由(前端控制全部路由,后端返回用户角色)

优缺点

优点:

  • 不用后端帮助,路由表维护在前端
  • 逻辑相对比较简单,比较容易上手
  • 权限少的系统用前端鉴权更加方便

缺点:

  • 线上版本每次修改权限页面,都需要重新打包项目
  • 大型项目不适用
  • 如果需要在页面中增加角色并且控制可以访问的页面,则不能用前端鉴权

具体思路

1、前端定义静态路由和动态路由,创建vue实例的时候vue-router挂载静态路由(登录等不需要权限的页面)

2、登录时获取用户信息,存入vuex中,存储token

3、在路由拦截器中,通过token去获取用户角色,拿角色去获取所有可访问的路由

4、调用router.addrouters(store.state.addRouters)添加可访问的路由

5、退出时清空用户信息,清空角色,清空路由

步骤一:前端定义静态路由和动态路由

router/index.js

javascript 复制代码
import Vue from "vue"
import VueRouter from "vue-router"
import Layout from "@/layout"

Vue.use(VueRouter)

// 解决重复点击路由报错的BUG
// 下面这段代码主要解决这个问题 :Uncaught (in promise) Error: Redirected when going from "/login" to "/index" via a navigation guard.
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch((err) => err)
}

// 定义好静态路由
export const constantRoutes = [
  {
    path: "/login",
    name: "login",
    component: () => import("../views/login"),
    hidden: true,
  },
]

// 定义动态路由,以及每个页面对应的roles(写在meta中,不写代表都可以访问)
export const asyncRoutes = [
  {
    id: 1,
    name: "/",
    path: "/",
    component: Layout,
    redirect: "/index",
    hidden: false,
    children: [
      {
        name: "index",
        path: "/index",
        meta: { title: "index" },
        component: () => import("@/views/index"),
      },
    ],
  },
  {
    id: 2,
    name: "/form",
    path: "/form",
    component: Layout,
    redirect: "/form/index",
    hidden: false,
    children: [
      {
        name: "/form/index",
        path: "/form/index",
        meta: { title: "form" },
        component: () => import("@/views/form"),
      },
    ],
  },
  {
    id: 3,
    name: "/example",
    path: "/example",
    component: Layout,
    redirect: "/example/tree",
    meta: { title: "example" },
    hidden: false,
    children: [
      {
        name: "/tree",
        path: "/example/tree",
        meta: { title: "tree" },
        component: () => import("@/views/tree"),
      },
      {
        name: "/copy",
        path: "/example/copy",
        meta: { title: "copy" },
        component: () => import("@/views/tree/copy"),
      },
    ],
  },
  {
    id: 4,
    name: "/table",
    path: "/table",
    component: Layout,
    redirect: "/table/index",
    hidden: false,
    meta: { roles: ["admin"] },
    children: [
      {
        name: "/table/index",
        path: "/table/index",
        meta: { title: "table", roles: ["admin"] },
        component: () => import("@/views/table"),
      },
    ],
  },
  {
    id: 5,
    name: "/admin",
    path: "/admin",
    component: Layout,
    redirect: "/admin/index",
    hidden: false,
    meta: { roles: ["admin"] },
    children: [
      {
        name: "/admin/index",
        path: "/admin/index",
        meta: { title: "admin", roles: ["admin"] },
        component: () => import("@/views/admin"),
      },
    ],
  },
  {
    id: 6,
    name: "/people",
    path: "/people",
    component: Layout,
    redirect: "/people/index",
    hidden: false,
    meta: { roles: ["admin", "common_user"] },
    children: [
      {
        name: "/people/index",
        path: "/people/index",
        meta: { title: "people", roles: ["admin", "common_user"] },
        component: () => import("@/views/people"),
      },
    ],
  },
  {
    id: 7,
    name: "/404",
    path: "/404",
    component: () => import("@/views/404"),
  },
  // 注意404页面要放到最后
  { path: "*", redirect: "/404", hidden: true },
]

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes: constantRoutes,
})

export default router

这里我们根据 vue-router官方推荐 的方法通过meta标签来标示改页面能访问的权限有哪些。如meta: { role: ['admin','super_editor'] }表示该页面只有admin和超级编辑才能有资格进入。

注意事项:这里有一个需要非常注意的地方就是 404 页面一定要最后加载,如果放在constantRoutes一同声明了404,后面的所以页面都会被拦截到404

步骤二:登录时获取用户信息,存入vuex中,存储token

login/index.vue

javascript 复制代码
methods: {
    login () {
      this.$refs.userForm.validate((valid) => {
        if (valid) {
          // 模拟登录接口去请求用户数据
          setTimeout(() => {
            // 这里的res就是模拟后台返回的用户数据(不包含用户角色,一般角色是由单独的一个接口返回)
            const res = dynamicUserData.filter((item) => item.username === this.user.username)[0]
            console.log(res)
            // 存储用户的信息及token到vuex,并做sessionStorage持久化处理
            this.$store.commit('User/saveUserInfo', res)
            Message({ type: 'success', message: "登录成功", showClose: true, duration: 3000 })
            this.$router.push({ path: "/index" })
          }, 1000)
        } else return false
      })
    }
  }

附:vuex持久化处理

javascript 复制代码
import Vue from 'vue'
import Vuex from 'vuex'
import User from './modules/user'
import permission from './modules/permission'
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    User,
    permission,
  },
  plugins: [
    createPersistedState({
      storage: window.sessionStorage, // 可选sessionStorage localStorage
      reducer(val) {
        return {
          User: val.User,
        }
      },
    }),
  ],
})

步骤【三四】:在路由拦截器中,通过token去获取用户角色,拿角色去获取所有可访问的路由,调用router.addrouters(store.state.addRouters)添加可访问的路由

路由钩子逻辑:

javascript 复制代码
是否为白名单页面
  是:   直接进入
  不是: 判断是否有token
  			无token:跳转到login登录页
  			有token: 判断用户是否有角色权限表
  						有权限表:直接进入
  						无权限表:调接口获取用户角色,并存储到vuex
  								根据返回的角色和路由表每个页面的需要的权限对比,生成可访问的路由表
  								使用router.addRouters()添加路由

路由导航守卫:

javascript 复制代码
import router from "./index"
import NProgress from "nprogress" // progress bar
import store from "@/store"
import menu from "@/mock/menu.js"

NProgress.configure({ showSpinner: false }) // NProgress Configuration

// 白名单页面直接进入
const whiteList = ["/login"]

router.beforeEach((to, from, next) => {
  NProgress.start()
  // 白名单页面,不管是否有token,是否登录都直接进入
  if (whiteList.indexOf(to.path) !== -1) {
    next()
    return false
  }
  // 有token(代表了有用户信息,但是不确定有没有角色权限数组)
  if (store.state.User.token) {
    // 判断当前用户是否有角色权限数组, 是登录状态则一定有路由,直接放行,不是登录状态则去获取路由菜单登录
    // 刷新时hasRoles会重置为false,重新去获取 用户的角色列表
    const hasRoles = store.state.permission.roles && store.state.permission.roles.length > 0
    if (!hasRoles) {
      setTimeout(async () => {
        const roles = menu.filter((item) => item.token === store.state.User.token)[0].roles
        // 将该角色权限数组存储到vuex中
        store.commit("permission/setRoles", roles)
        // 根据返回的角色信息去过滤异步路由中该角色可访问的页面
        const accessRoutes = await store.dispatch("permission/generateRoutes", roles)
        // dynamically add accessible routes
        router.addRoutes(accessRoutes)
        // hack方法 router.addRoutes之后的next()可能会失效,因为可能next()的时候路由并没有完全add完成 next(to)解决
        next({ ...to, replace: true })
      }, 500)
    } else {
      next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
    }
  } else {
    next({ path: "/login" })
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

vuex中做的事为: 将定义好的动态路由 通过 角色权限数组(后台返回的)进行过滤,过滤出用户有的路由,然后将该过滤后的路由添加到静态路由后面去

store/permission.js

javascript 复制代码
import { asyncRoutes, constantRoutes } from '@/router'
/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}

function hasPermission(roles, route) {
  console.log(roles)
  console.log(route)
  if (route.meta && route.meta.roles) {
    console.log(roles.some(role => route.meta.roles.includes(role)))
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

const state = {
  roles: [],
  routes: [],
  addRoutes: [],
}
const mutations = {
  setRoles(state, val) {
    state.roles = val
  },
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  },
}
const actions = {
  generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) { // admin直接添加所有权限
        accessedRoutes = asyncRoutes || []
      } else {
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  },
}
export default {
  namespaced: true,
  state,
  mutations,
  actions,
}

步骤四:退出时清空用户信息,清空角色,清空路由

javascript 复制代码
methods: {
    // 退出登录
    handleLogout() {
      window.localStorage.removeItem("token")
      // 清除用户信息
      this.$store.commit("User/removeUserInfo")
      // 清除角色权限列表
      this.$store.commit("permission/setRoles", [])
      // 清除角色权限数组
      this.$store.commit("permission/SET_ROUTES", [])
      Message({
        type: "success",
        message: "退出登录",
        showClose: true,
        duration: 3000,
      })
      this.$router.push({ path: "/login" })
    },
  }

希望能帮到你

文章参考:

花裤衩大佬:花裤衩大佬

本文代码:github 求 star

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax