路由结构
路由结构
路由名称的命名规则建议使用 kebab-case 风格,如下:
path(地址): kebab-case,层级用 / 分隔;用连字符 - 分层,不要用下划线分层
- 示例:
/components/message,/user-profile/security-settings
name(路由名): PascalCase,语义化,且与组件 defineOptions({ name }) 完全一致(便于 keep-alive)
- 示例:
ComponentsMessage,UserProfileSecuritySettings
组件文件名与目录: kebab-case;目录结构与 path 对齐
- 示例:
src/views/components/message/index.vue,则路由的name: "ComponentsMessage"
动态路由:
- 路由段静态部分用
kebab-case,参数标识用camelCase - 示例:
/orders/:orderId,name: "OrdersDetail"
嵌套路由:
- 子路由
path写相对路径(不要以/开头) - 父路由:
path: "/orders";子路由:path: "detail/:orderId"
一级路由
在 sunlight-admin 项目中,一级路由定义在 src/router/modules/staticRouter.ts 文件中:
ts
import { RouteRecordRaw } from 'vue-router'
const Layout = () => import('@/layout/index.vue')
export const staticRouter: RouteRecordRaw[] = [
{
path: '/',
component: Layout,
redirect: '/home',
children: [
{
path: 'home',
component: () => import('@/views/home/index.vue'),
name: 'Home',
meta: {
title: '首页',
icon: 'menu-home'
}
}
]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
hidden: true
}
}
]二级路由
提示
如果没有给父级路由指定 redirect,那么默认指向第一个子级路由。
sunlight-admin 项目的动态路由(包含二级及以上路由)定义在 src/router/modules/asyncRouter.ts 文件中:
ts
import { RouteRecordRaw } from 'vue-router'
const Layout = () => import('@/layout/index.vue')
export const asyncRoutes: MenuType.MenuOptions[] = [
{
path: '/project',
name: 'project',
component: Layout,
meta: {
title: '项目列表',
icon: 'menu-project',
roles: ['administrator'], // 角色权限控制
isKeepAlive: true
},
children: [
{
path: 'project-test',
component: () => import('@/views/project/index.vue'),
name: 'ProjectTest',
meta: {
title: '项目测试',
icon: 'menu-project',
roles: ['administrator'],
isKeepAlive: true
}
}
]
},
{
path: '/sunlightInput',
name: 'SunlightInput',
component: Layout,
meta: {
title: 'sunlightInput',
icon: 'menu-components',
roles: ['admin', 'administrator'],
isKeepAlive: true
},
children: [
{
path: 'input1',
component: () => import('@/views/sunlightInput/input1/index.vue'),
name: 'Input1',
meta: {
title: '基础使用',
icon: 'menu-component',
roles: ['admin', 'administrator'],
isKeepAlive: true
}
},
{
path: 'input3',
component: () => import('@/views/sunlightInput/input3/index.vue'),
name: 'input3',
meta: {
title: '键盘事件',
icon: 'menu-component',
roles: ['admin', 'administrator'],
isKeepAlive: true
}
}
]
}
]路由创建与初始化
在 src/router/index.ts 文件中创建路由实例并配置守卫:
ts
import { createRouter, createWebHashHistory } from 'vue-router'
import { staticRouter, errorRouter } from '@/router/modules/staticRouter'
import NProgress from '@/plugins/nprogress'
import { getToken } from '@/utils/auth'
import { useUserStore } from '@/store/modules/user'
import { useAuthStore } from '@/store/modules/auth'
const whiteList = ['/login'] // 无需登录的白名单路由
const CreateRouter = () =>
createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
history: createWebHashHistory(),
routes: [...staticRouter, ...errorRouter]
})
const router = CreateRouter()
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
NProgress.start()
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
const userStore = useUserStore()
const hasRoles = userStore.roles.length > 0
if (hasRoles) {
next()
} else {
try {
const { roles } = await userStore.getInfo()
const authStore = useAuthStore()
const accessRoutes = await authStore.generateAsyncRoutes(roles)
accessRoutes.forEach(item => router.addRoute(item))
next({ ...to, replace: true })
} catch (error) {
await userStore.resetToken()
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
whiteList.includes(to.path) ? next() : next(`/login?redirect=${to.path}`)
NProgress.done()
}
})
// 全局后置守卫
router.afterEach(() => {
NProgress.done()
})错误路由
项目还定义了错误页面路由,同样在 src/router/modules/staticRouter.ts 文件中:
ts
export const errorRouter = [
{
path: '/401',
name: '401',
component: () => import('@/views/error-page/401.vue'),
meta: {
title: '401',
hidden: true
}
},
{
path: '/404',
name: '404',
component: () => import('@/views/error-page/404.vue'),
meta: {
title: '404',
hidden: true
}
},
{
path: '/500',
name: '500',
component: () => import('@/views/error-page/500.vue'),
meta: {
title: '500',
hidden: true
}
},
// 404 兜底路由
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/error-page/404.vue')
}
]路由元信息说明
在 sunlight-admin 项目中,路由的 meta 对象包含以下常用属性:
| 属性名 | 类型 | 说明 |
|---|---|---|
| title | string | 页面标题 |
| icon | string | 菜单图标(使用项目中定义的 SVG 图标) |
| roles | string[] | 可访问角色列表 |
| hidden | boolean | 是否在菜单中隐藏 |
| isKeepAlive | boolean | 是否缓存页面 |
权限控制
项目通过角色权限控制路由的访问,在 src/store/modules/auth.ts 中实现动态路由生成:
ts
// 基于角色生成可访问路由
export function generateAsyncRoutes(roles: string[]): Promise<any> {
return new Promise(resolve => {
// 管理员角色拥有所有路由权限
let accessedRoutes = roles.includes('administrator') ? asyncRoutes : filterAsyncRoutes(asyncRoutes, roles)
resolve(accessedRoutes)
})
}
// 根据角色过滤路由
function filterAsyncRoutes(routes: MenuType.MenuOptions[], roles: string[]): MenuType.MenuOptions[] {
const res: MenuType.MenuOptions[] = []
routes.forEach(route => {
const tmp = { ...route } as MenuType.MenuOptions
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}通过这种方式,项目实现了基于角色的动态路由加载和权限控制。