vite-plugin-pages 路由高级配置 参考
简介
vue-router是来动态生成路由信息的插件,可以根据对应的文件结构来生成路由信息,不再需要手动的去配置路由.Nuxt中就是相似的方式来实现路由,可以通过nuxtStyle
配置项来使用nuxt
的方式来设置文件结构.
案例
例如我的vue目录如下:
页面效果
路由文件这样配置
所有在
pages
页面下的文件都会自动添加到generatedRoutes
里,那怎么对generatedRoutes
进行配置,比如添加常路由,异步路由,权限管理?
先来打印下 这个所谓的generatedRoutes
发现和我们手动引用的区别不大,下一步就可以使用vuerouter方法进行处理路由了
参照的项目是fantastic-admin-master
Fantastic-admin: ⭐⭐⭐⭐⭐)
1.区分 constantRoutes
和 asyncRoutes
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from 'virtual:generated-pages'
if (useSettingsOutsideStore().app.routeBaseOn === 'filesystem') {
constantRoutes = generatedRoutes.filter(item => {
return item.meta?.enabled !== false && item.meta?.constant === true
})
asyncRoutes = setupLayouts(generatedRoutes.filter(item => {
return item.meta?.enabled !== false && item.meta?.constant !== true && item.meta?.layout !== false
}))
}
2.router.beforeEach拦截
注意 routeOutsideStore.generateRoutesAtFront(asyncRoutes)
这个方法,动态加载
switch (settingsOutsideStore.app.routeBaseOn) {
case 'frontend':
await routeOutsideStore.generateRoutesAtFront(asyncRoutes)
break
case 'backend':
await routeOutsideStore.generateRoutesAtBack()
break
case 'filesystem':
await routeOutsideStore.generateRoutesAtFilesystem(asyncRoutes)
switch (settingsOutsideStore.menu.baseOn) {
case 'frontend':
await menuOutsideStore.generateMenusAtFront()
break
case 'backend':
await menuOutsideStore.generateMenusAtBack()
break
}
break
}
在vuex实现,几个方法
actions: {
// 根据权限动态生成路由(前端生成)
generateRoutesAtFront(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
},
// 根据权限动态生成路由(后端获取)
generateRoutesAtBack() {
return new Promise(resolve => {
api.get('route/list', {
baseURL: '/mock/'
}).then(async res => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let asyncRoutes = formatBackRoutes(res.data)
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
})
},
// 根据权限动态生成路由(文件系统生成)
generateRoutesAtFilesystem(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
},
// 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(routes) {
this.currentRemoveRoutes = routes
},
// 清空路由
removeRoutes() {
this.isGenerate = false
this.routes = []
this.currentRemoveRoutes.forEach(removeRoute => {
removeRoute()
})
this.currentRemoveRoutes = []
}
}
filterAsyncRoutes 方法
function filterAsyncRoutes(routes, permissions) {
const res = []
routes.forEach(route => {
let tmpRoute = deepClone(route)
if (hasPermission(permissions, tmpRoute)) {
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, permissions)
tmpRoute.children.length && res.push(tmpRoute)
} else {
res.push(tmpRoute)
}
}
})
return res
}
参考
1.src\store\modules\route.js
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import { deepClone, isExternalLink } from '@/util'
import api from '@/api'
import { useSettingsStore } from './settings'
import { useUserStore } from './user'
function hasPermission(permissions, route) {
let isAuth = false
if (route.meta && route.meta.auth) {
isAuth = permissions.some(auth => {
if (typeof route.meta.auth == 'string') {
return route.meta.auth === auth
} else {
return route.meta.auth.some(routeAuth => {
return routeAuth === auth
})
}
})
} else {
isAuth = true
}
return isAuth
}
function filterAsyncRoutes(routes, permissions) {
const res = []
routes.forEach(route => {
let tmpRoute = deepClone(route)
if (hasPermission(permissions, tmpRoute)) {
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, permissions)
tmpRoute.children.length && res.push(tmpRoute)
} else {
res.push(tmpRoute)
}
}
})
return res
}
function formatBackRoutes(routes, views = import.meta.glob('../../views/**/*.vue')) {
return routes.map(route => {
switch (route.component) {
case 'Layout':
route.component = () => import('@/layout/index.vue')
break
default:
if (route.component) {
route.component = views[`../../views/${route.component}`]
}
}
if (route.children) {
route.children = formatBackRoutes(route.children, views)
}
return route
})
}
// 将多层嵌套路由处理成平级
function flatAsyncRoutes(routes, breadcrumb, baseUrl = '') {
let res = []
routes.forEach(route => {
if (route.children) {
let childrenBaseUrl = ''
if (baseUrl == '') {
childrenBaseUrl = route.path
} else if (route.path != '') {
childrenBaseUrl = `${baseUrl}/${route.path}`
}
let childrenBreadcrumb = deepClone(breadcrumb)
if (route.meta.breadcrumb !== false) {
childrenBreadcrumb.push({
path: childrenBaseUrl,
title: route.meta.title
})
}
let tmpRoute = deepClone(route)
tmpRoute.path = childrenBaseUrl
tmpRoute.meta.breadcrumbNeste = childrenBreadcrumb
delete tmpRoute.children
res.push(tmpRoute)
let childrenRoutes = flatAsyncRoutes(route.children, childrenBreadcrumb, childrenBaseUrl)
childrenRoutes.map(item => {
// 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复
if (res.some(v => v.path == item.path)) {
res.forEach((v, i) => {
if (v.path == item.path) {
res[i] = item
}
})
} else {
res.push(item)
}
})
} else {
let tmpRoute = deepClone(route)
if (baseUrl != '' && !isExternalLink(tmpRoute.path)) {
if (tmpRoute.path != '') {
tmpRoute.path = `${baseUrl}/${tmpRoute.path}`
} else {
tmpRoute.path = baseUrl
}
}
// 处理面包屑导航
let tmpBreadcrumb = deepClone(breadcrumb)
if (tmpRoute.meta.breadcrumb !== false) {
tmpBreadcrumb.push({
path: tmpRoute.path,
title: tmpRoute.meta.title
})
}
tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb
res.push(tmpRoute)
}
})
return res
}
export const useRouteStore = defineStore(
// 唯一ID
'route',
{
state: () => ({
isGenerate: false,
routes: [],
currentRemoveRoutes: []
}),
getters: {
// 扁平化路由(将三级及以上路由数据拍平成二级)
flatRoutes: state => {
const settingsStore = useSettingsStore()
let routes = []
if (state.routes) {
if (settingsStore.app.routeBaseOn !== 'filesystem') {
state.routes.map(item => {
routes.push(...deepClone(item.children))
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
} else {
state.routes.map(item => {
routes.push(deepClone(item))
})
}
}
return routes
}
},
actions: {
// 根据权限动态生成路由(前端生成)
generateRoutesAtFront(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
},
// 根据权限动态生成路由(后端获取)
generateRoutesAtBack() {
return new Promise(resolve => {
api.get('route/list', {
baseURL: '/mock/'
}).then(async res => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let asyncRoutes = formatBackRoutes(res.data)
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
})
},
// 根据权限动态生成路由(文件系统生成)
generateRoutesAtFilesystem(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
},
// 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(routes) {
this.currentRemoveRoutes = routes
},
// 清空路由
removeRoutes() {
this.isGenerate = false
this.routes = []
this.currentRemoveRoutes.forEach(removeRoute => {
removeRoute()
})
this.currentRemoveRoutes = []
}
}
}
)
export function useRouteOutsideStore() {
return useRouteStore(piniaStore)
}
2.src\router\index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import { useSettingsOutsideStore } from '@/store/modules/settings'
import { useKeepAliveOutsideStore } from '@/store/modules/keepAlive'
import { useUserOutsideStore } from '@/store/modules/user'
import { useMenuOutsideStore } from '@/store/modules/menu'
import { useRouteOutsideStore } from '@/store/modules/route'
import '@/assets/styles/nprogress.scss'
import { useNProgress } from '@vueuse/integrations/useNProgress'
const { isLoading } = useNProgress()
// 固定路由
let constantRoutes = [
{
path: '/login',
name: 'login',
component: () => import('@/views/login.vue'),
meta: {
title: '登录'
}
},
{
path: '/',
component: () => import('@/layout/index.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/index.vue'),
meta: {
title: () => {
const settingsOutsideStore = useSettingsOutsideStore()
return settingsOutsideStore.dashboard.title
}
}
},
{
path: 'personal/setting',
name: 'personalSetting',
component: () => import('@/views/personal/setting.vue'),
meta: {
title: '个人设置',
cache: 'personalEditPassword',
breadcrumbNeste: [
{ title: '个人设置', path: '/personal/setting' }
]
}
},
{
path: 'personal/edit/password',
name: 'personalEditPassword',
component: () => import('@/views/personal/edit.password.vue'),
meta: {
title: '修改密码',
breadcrumbNeste: [
{ title: '修改密码', path: '/personal/edit/password' }
]
}
},
{
path: 'reload',
name: 'reload',
component: () => import('@/views/reload.vue')
}
]
}
]
import MultilevelMenuExample from './modules/multilevel.menu.example'
import BreadcrumbExample from './modules/breadcrumb.example'
import KeepAliveExample from './modules/keep.alive.example'
import ComponentBasicExample from './modules/component.basic.example'
import ComponentExtendExample from './modules/component.extend.example'
import PermissionExample from './modules/permission.example'
import MockExample from './modules/mock.example'
import JsxExample from './modules/jsx.example'
import ExternalLinkExample from './modules/external.link.example'
// import VideosExample from './modules/videos.example'
import EcologyExample from './modules/ecology.example'
import CooperationExample from './modules/cooperation.example'
// 动态路由(异步路由、导航栏路由)
let asyncRoutes = [
{
meta: {
title: '演示',
icon: 'sidebar-default'
},
children: [
MultilevelMenuExample,
BreadcrumbExample,
KeepAliveExample,
ComponentBasicExample,
ComponentExtendExample,
PermissionExample,
MockExample,
JsxExample,
ExternalLinkExample
]
},
// {
// meta: {
// title: '教程',
// icon: 'sidebar-videos'
// },
// children: [
// ...VideosExample
// ]
// },
{
meta: {
title: '生态',
icon: 'sidebar-ecology'
},
children: [
...EcologyExample
]
},
{
meta: {
title: '战略合作',
icon: 'sidebar-cooperation'
},
children: [
...CooperationExample
]
}
]
const lastRoute = {
path: '/:all(.*)*',
name: 'notFound',
component: () => import('@/views/[...all].vue'),
meta: {
title: '找不到页面'
}
}
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from 'virtual:generated-pages'
if (useSettingsOutsideStore().app.routeBaseOn === 'filesystem') {
constantRoutes = generatedRoutes.filter(item => {
return item.meta?.enabled !== false && item.meta?.constant === true
})
asyncRoutes = setupLayouts(generatedRoutes.filter(item => {
return item.meta?.enabled !== false && item.meta?.constant !== true && item.meta?.layout !== false
}))
}
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes
})
router.beforeEach(async(to, from, next) => {
const settingsOutsideStore = useSettingsOutsideStore()
const userOutsideStore = useUserOutsideStore()
const menuOutsideStore = useMenuOutsideStore()
const routeOutsideStore = useRouteOutsideStore()
settingsOutsideStore.app.enableProgress && (isLoading.value = true)
// 是否已登录
if (userOutsideStore.isLogin) {
// 是否已根据权限动态生成并挂载路由
if (routeOutsideStore.isGenerate) {
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
settingsOutsideStore.menu.menuMode !== 'single' && menuOutsideStore.setActived(to.path)
if (to.name) {
if (to.matched.length !== 0) {
// 如果已登录状态下,进入登录页会强制跳转到控制台页面
if (to.name == 'login') {
next({
name: 'dashboard',
replace: true
})
} else if (!settingsOutsideStore.dashboard.enable && to.name == 'dashboard') {
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块
if (menuOutsideStore.sidebarMenus.length > 0) {
next({
path: menuOutsideStore.sidebarMenusFirstDeepestPath,
replace: true
})
} else {
next()
}
} else {
next()
}
} else {
// 如果是通过 name 跳转,并且 name 对应的路由没有权限时,需要做这步处理,手动指向到 404 页面
next({
path: '/404'
})
}
} else {
next()
}
} else {
switch (settingsOutsideStore.app.routeBaseOn) {
case 'frontend':
await routeOutsideStore.generateRoutesAtFront(asyncRoutes)
break
case 'backend':
await routeOutsideStore.generateRoutesAtBack()
break
case 'filesystem':
await routeOutsideStore.generateRoutesAtFilesystem(asyncRoutes)
switch (settingsOutsideStore.menu.baseOn) {
case 'frontend':
await menuOutsideStore.generateMenusAtFront()
break
case 'backend':
await menuOutsideStore.generateMenusAtBack()
break
}
break
}
let removeRoutes = []
routeOutsideStore.flatRoutes.forEach(route => {
if (!/^(https?:|mailto:|tel:)/.test(route.path)) {
removeRoutes.push(router.addRoute(route))
}
})
if (settingsOutsideStore.app.routeBaseOn === 'filesystem') {
const otherRoutes = generatedRoutes.filter(item => item.meta?.constant !== true && item.meta?.layout === false)
otherRoutes.length && removeRoutes.push(router.addRoute(...otherRoutes))
} else {
removeRoutes.push(router.addRoute(lastRoute))
}
// 记录的 accessRoutes 路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
routeOutsideStore.setCurrentRemoveRoutes(removeRoutes)
next({ ...to, replace: true })
}
} else {
if (to.name != 'login') {
next({
name: 'login',
query: {
redirect: to.fullPath
}
})
} else {
next()
}
}
})
router.afterEach((to, from) => {
const settingsOutsideStore = useSettingsOutsideStore()
const keepAliveOutsideStore = useKeepAliveOutsideStore()
settingsOutsideStore.app.enableProgress && (isLoading.value = false)
// 设置页面 title
to.meta.title && settingsOutsideStore.setTitle(typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title)
// 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
if (to.meta.cache) {
let componentName = to.matched[to.matched.length - 1].components.default.name
if (componentName) {
keepAliveOutsideStore.add(componentName)
} else {
console.warn('该页面组件未设置组件名,会导致缓存失效,请检查')
}
}
// 判断离开页面是否开启缓存,如果开启,则根据缓存规则判断是否需要清空 keep-alive 全局状态里离开页面的 name 信息
if (from.meta.cache) {
let componentName = from.matched[from.matched.length - 1].components.default.name
// 通过 meta.cache 判断针对哪些页面进行缓存
switch (typeof from.meta.cache) {
case 'string':
if (from.meta.cache != to.name) {
keepAliveOutsideStore.remove(componentName)
}
break
case 'object':
if (!from.meta.cache.includes(to.name)) {
keepAliveOutsideStore.remove(componentName)
}
break
}
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
if (to.name == 'reload') {
keepAliveOutsideStore.remove(componentName)
}
}
})
export default router