路由是项目中最重要的部分。本节将实现路由权限控制,动态生成菜单
目录结构
src router modules dashboard.js base.js constant.js index.js router-guards.js store modules asyncRoute.js
|
组件常量
创建src/router/constant.js
,填入以下内容:
router/constant.jsexport const RedirectName = 'Redirect';
export const ErrorPage = () => import('@/views/exception/404.vue');
export const Layout = () => import('@/layout/index.vue');
|
Layout将在项目布局篇中予以实现,这里为了不报错可以先写一个空的index.vue
单个路由
定义单独的页面路由组件,后续会统一读取添加到路由以及生成菜单。
错误和重定向页面路由
创建src/router/base.js
,填入以下内容:
router/base.jsimport { ErrorPage, RedirectName, Layout } from '@/router/constant';
export const ErrorPageRoute = { path: '/:path(.*)*', component: Layout, meta: { title: 'ErrorPage', hideBreadcrumb: true, }, children: [ { path: '/:path(.*)*', component: ErrorPage, meta: { title: 'ErrorPage', hideBreadcrumb: true, }, }, ], };
export const RedirectRoute = { path: '/redirect', name: RedirectName, component: Layout, meta: { title: RedirectName, hideBreadcrumb: true, }, children: [ { path: '/redirect/:path(.*)', name: RedirectName, component: () => import('@/views/redirect/index.vue'), meta: { title: RedirectName, hideBreadcrumb: true, }, }, ], };
|
Dashboard页面路由
创建src/router/modules/dashboard.js
,填入以下内容:
router/modules/dashboard.jsconst routeName = 'dashboard'; import { Layout } from '@/router/constant'; import { DashboardOutlined } from '@vicons/antd'; import { renderIcon } from '@/utils/index';
const routes = [ { path: '/dashboard', name: routeName, redirect: '/dashboard/console', component: Layout, meta: { title: 'Dashboard', icon: renderIcon(DashboardOutlined), permissions: ['dashboard_console', 'dashboard_workplace'], sort: 0, }, children: [ { path: 'console', name: `${routeName}_console`, meta: { title: '主控台', permissions: ['dashboard_console'], affix: true, }, component: () => import('@/views/dashboard/console/console.vue'), }, { path: 'workplace', name: `${routeName}_workplace`, meta: { title: '工作台', keepAlive: true, permissions: ['dashboard_workplace'], }, component: () => import('@/views/dashboard/workplace/workplace.vue'), }, ], }, ];
export default routes;
|
路由整合
新建src/router/index.js
,内容如下:
Vite 方式请如用如下代码组装路由
const modules = import.meta.glob('./modules/**/*.js', { eager: true });
const routeModuleList = Object.keys(modules).reduce((list, key) => { const mod = modules[key].default ?? {}; const modList = Array.isArray(mod) ? [...mod] : [mod]; return [...list, ...modList]; }, []);
|
router/index.jsimport { PageEnum } from '@/enums/pageEnum'; import { createRouter, createWebHashHistory } from 'vue-router'; import { RedirectRoute } from '@/router/base'; import { ErrorPageRoute } from './base';
const modules = require.context('@/router/modules/', false, /\.js$/);
const routeModuleList = [];
modules.keys().forEach((key) => { const mod = modules(key).default || {}; const modeList = Array.isArray(mod) ? [...mod] : [mod]; routeModuleList.push(...modeList); });
function sortRoute(a, b) { return (a.meta?.sort || 0) - (b.meta?.sort || 0); }
routeModuleList.sort(sortRoute);
export const RootRoute = { path: '/', name: 'Root', redirect: PageEnum.BASE_HOME, meta: { title: 'Root', }, };
export const LoginRoute = { path: '/login', name: 'Login', component: () => import('@/views/login/index.vue'), meta: { title: '登录', }, };
export const asyncRoutes = [...routeModuleList];
export const constantRouter = [ LoginRoute, RootRoute, RedirectRoute, ErrorPageRoute ];
const router = createRouter({ history: createWebHashHistory(''), routes: constantRouter, strict: true, scrollBehavior: () => ({ left: 0, top: 0 }), }); export function setupRouter(app) { app.use(router); }
export default router;
|
挂载路由
在src/main.js
中挂载路由
main.jsimport './styles/tailwind.css'; import { createApp } from 'vue'; import App from './App.vue'; import router, { setupRouter } from './router'; import { setupStore } from './store'; import { setupNaive } from './plugins';
async function bootstrap() { const app = createApp(App);
setupStore(app);
setupNaive(app);
setupRouter(app);
await router.isReady();
app.mount('#app', true); }
bootstrap();
|
动态路由
动态路由一般有两种实现方式:
- 后端获取路由列表数据,在前端做数据处理后动态生成路由和菜单信息
- 使用权限过滤,过滤账户是否拥有某一个权限,并将菜单从加载列表移除
本次项目使用的是权限过滤方式。
创建src/store/modules/asyncRoute.js
store/modules/asyncRoute.jsimport { asyncRoutes, constantRouter } from '@/router'; import { defineStore } from 'pinia'; import { toRaw, unref } from 'vue'; import { store } from '@/store'; import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
const DEFAULT_CONFIG = { id: 'id', children: 'children', pid: 'pid', }; const getConfig = (config) => Object.assign({}, DEFAULT_CONFIG, config);
function filter(tree, func, config = {}) { config = getConfig(config); const children = config.children;
function listFilter(list) { return list .map((node) => ({ ...node })) .filter((node) => { node[children] = node[children] && listFilter(node[children]); return func(node) || (node[children] && node[children].length); }); } return listFilter(tree); }
export const useAsyncRouteStore = defineStore({ id: 'app-async-route', state: () => ({ menus: [], routers: constantRouter, addRouters: [], keepAliveComponents: [], isDynamicAddedRoute: false, }), getters: { getMenus() { return this.menus; }, getIsDynamicAddedRoute() { return this.isDynamicAddedRoute; }, }, actions: { getRouters() { return toRaw(this.addRouters); }, setDynamicAddedRoute(added) { this.isDynamicAddedRoute = added; }, setRouters(routers) { this.addRouters = routers; this.routers = constantRouter.concat(routers); }, setMenus(menus) { this.menus = menus; }, setKeepAliveComponents(compNames) { this.keepAliveComponents = compNames; }, generateRoutes(data) { let accessedRouters; const permissionsList = data.permissions || []; const routeFilter = (route) => { const { meta } = route; const { permissions } = meta || {}; if (!permissions) return true; return permissionsList.some((item) => permissions.includes(item.value)); }; const { getPermissionMode } = useProjectSetting(); const permissionMode = unref(getPermissionMode); if (permissionMode === 'BACK') { } else { try { accessedRouters = filter(asyncRoutes, routeFilter); } catch (error) { console.log(error); } } accessedRouters = accessedRouters.filter(routeFilter); this.setRouters(accessedRouters); this.setMenus(accessedRouters); return toRaw(accessedRouters); }, }, });
export function useAsyncRouteStoreWidthOut() { return useAsyncRouteStore(store); }
|
generateRoutes(data)
该方法将在获取到用户信息后执行,用于动态生成路由和侧边栏菜单信息,在路由守卫中执行
路由守卫
路由守卫负责拦截每次的路由跳转,我们可以在每次路由跳转前,判断Token是否过期、获取用户信息、动态添加路由等操作;在路由跳转之后,可以缓存组件等。
创建src/router/router-guards.js
,内容如下:
router/router-guards.jsimport { PageEnum } from '@/enums/pageEnum'; import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute'; import { useUserStoreWidthOut } from '@/store/modules/user'; import { ACCESS_TOKEN } from '@/store/mutation-types'; import { storage } from '@/utils/Storage'; import { isNavigationFailure } from 'vue-router'; import { ErrorPageRoute } from './base';
const LOGIN_PATH = PageEnum.BASE_LOGIN;
const whitePathList = [LOGIN_PATH];
export function createRouterGuards(router) { const userStore = useUserStoreWidthOut(); const asyncRouteStore = useAsyncRouteStoreWidthOut(); router.beforeEach(async (to, from, next) => { const Loading = window['$loading'] || null; Loading && Loading.start(); if (from.path === LOGIN_PATH && to.name === 'errorPage') { next(PageEnum.BASE_HOME); return; }
if (whitePathList.includes(to.path)) { next(); return; } const token = storage.get(ACCESS_TOKEN); if (!token) { if (to.meta.ignoreAuth) { next(); return; } const redirectData = { path: LOGIN_PATH, replace: true, }; if (to.path) { redirectData.query = { ...redirectData.query, redirect: to.path, }; } next(redirectData); return; } if (asyncRouteStore.getIsDynamicAddedRoute) { next(); return; }
const userInfo = await userStore.GetInfo(); const routes = asyncRouteStore.generateRoutes(userInfo); routes.forEach((item) => { router.addRoute(item); });
const isErrorPage = router .getRoutes() .findIndex((item) => item.name === ErrorPageRoute.name); if (isErrorPage === -1) { router.addRoute(ErrorPageRoute); }
const redirectPath = from.query.redirect || to.path; const redirect = decodeURIComponent(redirectPath); const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }; asyncRouteStore.setDynamicAddedRoute(true); next(nextData); Loading && Loading.finish(); });
router.afterEach((to, _, failure) => { document.title = to?.meta?.title || document.title; if (isNavigationFailure(failure)) { } const asyncRouteStore = useAsyncRouteStoreWidthOut(); const keepAliveComponents = asyncRouteStore.keepAliveComponents; const currentComName = to.matched.find( (item) => item.name == to.name )?.name; if ( currentComName && !keepAliveComponents.includes(currentComName) && to.meta?.keepAlive ) { keepAliveComponents.push(currentComName); } else if (!to.meta?.keepAlive || to.name == 'Redirect') { const index = asyncRouteStore.keepAliveComponents.findIndex( (name) => name == currentComName ); if (index != -1) { keepAliveComponents.splice(index, 1); } } asyncRouteStore.setKeepAliveComponents(keepAliveComponents); const Loading = window['$loading'] || null; Loading && Loading.finish(); });
router.onError((error) => { console.log(error, '路由错误'); }); }
|
在src/router/index.js
,加载路由守卫:
router/index.jsimport { createRouterGuards } from './router-guards';
export function setupRouter(app) { app.use(router); createRouterGuards(router); }
|