Skip to content

路由结构

路由结构

路由名称的命名规则建议使用 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/:orderIdname: "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 对象包含以下常用属性:

属性名类型说明
titlestring页面标题
iconstring菜单图标(使用项目中定义的 SVG 图标)
rolesstring[]可访问角色列表
hiddenboolean是否在菜单中隐藏
isKeepAliveboolean是否缓存页面

权限控制

项目通过角色权限控制路由的访问,在 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
}

通过这种方式,项目实现了基于角色的动态路由加载和权限控制。