vue3 + vite 项目搭建 - 引入vue-router及实现权限控制

2,061 阅读3分钟

一般后台管理系统都会有管理员和普通用户的区分,所以要做权限控制

思路

  1. 创建公用页面login,可以让用户进行登录操作,根据用户信息对应的身份,匹配不同的权限模块
  2. 在路由前置钩子进行身份验证拦截,将拦截条件分为 白名单(不做拦截)、登录态未登录、登录态已登陆
  3. 路由注册分为两步,公用页面直接注册,权限路由根据登录用户的身份来注册
  4. 解决路由层级过深会导致keep-alive不缓存问题

实现

  1. 安装依赖
"dependencies": {
	"nprogress": "^0.2.0",
    "vue-router": "^4.0.6"
  }
  1. 在src目录下创建配置文件config,统一管理一些设置信息 新建setting.config.js
/** src/config/setting.config.js */
// 项目名称
export const title: string = '风控管理平台'
// 标题分隔符
export const titleSeparator: string = ' - '
// 标题是否反转
// 如果为false: "page - title"
// 如果为ture : "title - page"
export const titleReverse: boolean = true
// 缓存路由的最大数量
export const keepAliveMaxNum: number = 20
// 路由模式,可选值为 history 或 hash
export const routerMode: 'hash' | 'history' = 'hash'
// 不经过token校验的路由
export const routesWhiteList: string[] = [
  '/login',
  '/login/vip',
  '/404',
  '/403'
]
// 是否开启登录拦截
export const loginInterception: boolean = true

  1. 在src目录下创建router文件夹,新建index.ts router.ts
// src/router/index.ts
import { createRouter, createWebHashHistory, createWebHistory, RouteRecordRaw } from 'vue-router'
import { loginInterception, routerMode, routesWhiteList, title, titleReverse } from '@/config/setting.config'
import Layout from '@/views/layout/index.vue'
import VabProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { store } from '@/store'

// 每次刷新都重新加载权限路由
let routerLoad = false

VabProgress.configure({
  easing: 'ease',
  speed: 500,
  trickleSpeed: 200,
  showSpinner: false
})

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    component: Layout,
    redirect: '/home'
  },
  {
    path: '/home',
    name: 'Home',
    component: () => import('@/views/index/index.vue'),
    meta: {
      noKeepAlive: true
    }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login/index.vue')
  },
  {
    path: '/403',
    name: '403',
    component: () => import('@/views/403.vue')
  },
  {
    path: '/:pathMatch(.*)*',
    name: '404',
    component: () => import('@/views/404.vue')
  }
]

const router = createRouter({
  history: routerMode === 'hash'
    ? createWebHashHistory(import.meta.env.BASE_URL)
    : createWebHistory(import.meta.env.BASE_URL),
  routes
})

export function resetRoute (routes: any): void {
  router.addRoute({
    path: '/',
    component: Layout,
    redirect: '/home',
    children: getAllFirstRoute(routes)
  })
}

/**
 * 解构成一级路由,解决多级嵌套的keep-alive问题
 * @param routes
 * @param result
 */
function getAllFirstRoute (routes: any, result: any = []): any {
  routes.forEach((route: any) => {
    result.push({
      ...route,
      children: [],
      path: route.meta.realPath
    })
    if (route.children && route.children.length > 0) {
      result = getAllFirstRoute(route.children, result)
    }
  })
  return result
}

router.beforeEach(async (to, from, next) => {
  VabProgress.start()
  let hasToken = store.getters['user/token']
  if (!loginInterception) hasToken = true
  if (routesWhiteList.includes(to.path)) {
    // 白名单内的不做验证
    next()
  } else if (hasToken) {
    // 已登录
    if (routerLoad) {
      next()
    } else {
      await store.dispatch('router/setRoutes')
      routerLoad = true
      next(to.path)
    }
  } else {
    // 未登录
    next(store.getters['router/logoutUrl'])
  }
  VabProgress.done()
})

router.afterEach(route => {
  if (route.meta && route.meta.title) {
    if (titleReverse) {
      document.title = `${route.meta.title} - ${title}`
    } else {
      document.title = `${title} - ${route.meta.title}`
    }
  }
})

export default router

  1. 状态管理新增router模块,更新state.d.ts ,新建 router.ts
// src/state/state.d.ts
declare namespace MyStore {
  type language = 'zh-cn' | 'en'
  type RouterRoleType = 'administrators' | 'headquarters' | 'subsidiary'
  interface RouteMeta {
    title: string,
    icon: string,
    hidden: boolean,
    noKeepAlive: boolean,
    fullPath: string,
    realPath: string
  }
  interface Route {
    path: string,
    component: any,
    redirect: string,
    name: string,
    meta: RouteMeta,
    children: Array<Route>
  }
  interface State {
    count: number
  }
  interface SettingState {
    language: language
  }
  interface RouterState {
    roleType: RouterRoleType,
    routes: Route,
    cachedRoutes: Array<Route>
  }
}

// src/state/modules/router.ts
// import routers from '@/router/router.json'
import routers from '@/router/router'
import { convertRouter } from '@/utils/routes'
import { resetRoute } from '@/router'
export default {
  name: 'router',
  namespaced: true,
  state: () => {
    const state: MyStore.RouterState = {
      roleType: 'headquarters', // 角色类型
      routes: {
        children: []
      },
      cachedRoutes: []
    }
    return state
  },
  getters: {
    logoutUrl () {
      return '/login'
    }
  },
  mutations: {
    setRoutes (state: MyStore.RouterState, routes: MyStore.Route) {
      state.routes = routes
    },
    setRoleType (state: MyStore.RouterState, type: MyStore.RouterRoleType) {
      state.roleType = type
    },
    setCachedRoutes (state: MyStore.RouterState, routes: Array<MyStore.Route>) {
      state.cachedRoutes = routes
    }
  },
  actions: {
    async setRoutes ({ commit, state }: any) {
      // @ts-ignore
      const routes = routers[state.roleType]
      commit('setRoutes', routes)
      const syncRoutes = convertRouter([routes])
      resetRoute(syncRoutes)
    },
    async setCachedRoutes ({ commit }: any, routes: any) {
      commit('setCachedRoutes', routes)
    }
  }
}

  1. 创建工具方法,解析路由
// src/utils/router.ts
export function convertRouter (asyncRoutes: Array<any>, parentUrl: string = ''): any {
  return asyncRoutes.map(route => {
    // 动态拼接组件,但是发布到线上后失效,暂时搁置
    // if (route.component) {
    //   if (route.component === 'Layout') {
    //     route.component = () => import('../views/layout/index.vue')
    //   } else {
    //     const index = route.component.indexOf('views')
    //     const path =
    //         index > 0 ? route.component.slice(index) : `views/${route.component}`
    //     // route.component = () => import(`../${path}/index.vue`)
    //     route.component = defineAsyncComponent(() => import(`../${path}/index.vue`))
    //   }
    // }
    // 检测meta
    // fullPath 左侧menu使用,用来激活
    // realPath 组件全路径,用来注册路由
    if (!route.meta) route.meta = { hidden: false, fullPath: '', realPath: '' }
    if (!parentUrl) {
      // 第一级没有传入 parentUrl
      route.meta.fullPath = route.path
      route.meta.realPath = route.path
    } else if (route.meta.hidden) {
      // 如果隐藏当前,不在menu展示,则fullPath设置为上一级路由
      route.meta.fullPath = parentUrl
      route.meta.realPath = `${parentUrl}/${route.path}`
    } else {
      route.meta.fullPath = `${parentUrl}/${route.path}`
      route.meta.realPath = `${parentUrl}/${route.path}`
    }

    if (route.children && route.children.length) { route.children = convertRouter(route.children, route.meta.fullPath) }
    if (route.children && route.children.length === 0) delete route.children
    return route
  })
}

  1. 在登录页更新状态管理器 router/roleType,再进行页面跳转,在前置钩子处就会自动匹配并注册当前权限下的路由了。