前言
看了 nextjs 的路由,用新项目的时候,总是不想自己写路由文件了,并且总想自己写一个自动导入的,用了 vue 后,发现可以用到了,于是乎就做一个
下面将介绍一个简易版的自动生成路由,以及仿 nextjs 的目录结构路由,以及带入菜单的原因
ps
:似乎是有相关插件,就是不用,任性哈
路由和菜单
这里为什么要做一个 element 的菜单呢,实际上是因为很多人以前写路由时,会将菜单和路由写到一起,一起处理管理,一套多用,因此很多人对自动路由不太感冒,实际上路由和菜单混合到一起,页有不少问题
路由是路由,菜单菜单
,菜单主要包含了目录、icon、跳转路径等,实际上不一定包含所有路径,就像前端经常讨论的model、viewModel
一样,有时用的一样,有时用不一样,实际上他们不是一个概念- 放到一起,那么路由和菜单将耦合在一起,多出来的还需要有剔除逻辑,或者分离合并,甚至一些权限逻辑都放到了路由中,再加上权限,耦合会严重,维护成本持续增加
- 路由和菜单分开后,功能隔离,模块清晰,即使加入权限后,仍然是权限 + 路由绑定,因此会形成
菜单 + 路由
、权限 + 路由
等逻辑,自由组合,代码解耦,并且逻辑易读,而权限判断则是在 beforeEach 中判断即可,要改哪一个就改哪一个,很合适
自动生成路由(import.meta.glob)
vite
中可以使用 import.meta.glob
函数 (webpack
中使用 require.context
函数),这里只介绍 import.meta.glob
通过 import.meta.glob
可以获取到指定目录下的所有文件,结果为一个对象,key为路径,value 为内容(value.default 是 component)
js
const modules: Record<string, unknown> = import.meta.glob(
['@/views/**/*.vue', '@/views/**/*.jsx'], //排除前面加上感叹号 !@/views/Dashboard/*.vue
{
eager: true, //默认false懒加载import,设置为false则直接导入所有
},
)
下面就是 modules 的内容,后续可以通过改进,形成我们路由的样子
看了路由后,我们的路由可能存在嵌套关系,因此要处理成类似下面的样子,这也是后续我们需要加工 modules 的逻辑了,根据实际要求处理逻辑也不同
js
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: 'profile',
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: 'posts',
component: UserPosts,
},
],
},
]
我们将写一个路由,并且在准备一个重定向的路由,重定向的路由,我们不处理,可以后续拼接到里面
js
import nextAppImportRoutes from './import-next-routes'
//用于重定向的,拼接到我们规划好的路由前面
const defaultRoutes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/login',
},
{ path: '/dashboard', redirect: '/dashboard/location' },
{ path: '/:pathMatch(.*)', component: NotFound404 },
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
//拼接上我们生成的路由,后续可能还有匹配 404 之类的可以拼接到后面
routes: defaultRoutes.concat(nextAppImportRoutes),
})
ps
:下面将介绍我们的处理逻辑和要做的功能
将指定目录的文件生成路由
这里我们将下面的目录生成我们的路由,其中 index 则是目录哪一级的(类似 nextjs 的 layout,路由对应着文件夹路径),用于分发子路由(带标题或者菜单前缀的页面)
上面的格式也可以看到,基础的逻辑有了,但是缺点也很明显,如果要对单个文件添加 name、动态路由匹配等,不太方便,或者文件会汇集到一起,看起来比较乱,有动态路由匹配的,推荐使用 query 模式,就不会影响了,name 也不是那么必要
下面就将上面的逻辑处理成嵌套路由的方式
js
import type { RouteRecordRaw } from 'vue-router'
//声明类型,也是为了使用方便,直接声明,后续强转,实际类型算是包含关系
type DirType = {
path: string
component?: unknown
children?: DirType[]
}
const generateImportRoutes = (): RouteRecordRaw[] => {
//webpack require.context vite import.meta.glob
const modules: Record<string, unknown> = import.meta.glob(
['@/views/**/*.vue', '@/views/**/*.tsx', '@/views/**/*.jsx'], //排除前面加上感叹号 !@/views/Dashboard/*.vue
{
eager: true, //默认false懒加载import,设置为false则直接导入所有
},
)
//直接整理一下key、default,避免后续取出麻烦
const comObj: Record<string, unknown> = {}
const paths: string[][] = []
for (const key in modules) {
const path = key.replace(/^(\/src\/views)|(\.vue|\.tsx|\.jsx)$/g, '').toLocaleLowerCase()
comObj[path] = (modules[key] as { default: unknown }).default
const temPaths = path.split('/').filter((e) => e)
paths.push(temPaths)
}
const structs = generateStruct(paths, comObj)
console.log('structs', structs)
return structs as RouteRecordRaw[]
}
//根据路径、对照表、父节点递归出我们的路由结构
const generateStruct = (
paths: string[][],
comObj: Record<string, unknown>,
parent?: DirType,
): DirType[] => {
const structs: DirType[] = []
const map = new Map<string, string[][]>()
for (let idx = 0; idx < paths.length; ) {
const item = paths[idx]
let path = parent ? item[0] : `/${item[0]}`
if (item.length === 1) {
if (parent) {
path = `${parent.path}/${path}`
}
paths.splice(idx, 1)
//index 当做我们的布局分发组件,对应着文件夹
if (item[0] === 'index' && parent) {
parent.component = comObj[path]
continue
}
structs.push({
path,
component: comObj[path],
})
} else {
const paths = map.get(path) || []
item.splice(0, 1)
paths.push(item)
map.set(path, paths)
idx++
}
}
for (const [key, value] of map) {
const obj: DirType = {
path: parent ? `${parent.path}/${key}` : key,
}
obj.children = generateStruct(value, comObj, obj)
structs.push(obj)
}
return structs
}
const importRoutes = generateImportRoutes()
export default importRoutes
不多说,成功了
仿 nextjs 路由(改进版本page、layout)
用过 nextjs 的 app 模式的应该都了解,其理由规范很好,结构很清晰,也很方便扩展,因此模仿了一下,其路由结构如下所示,其中 layout 对应的路径是父级别文件夹,page也是,只不过 page 是页面,layout 则是分发子路由用的(例如:菜单),可有可无,page 则必须要有
里面加入了 json(也可以改成 ts),比上面案例改进了一些,加了页面,也加了动态路由匹配(为了并存,创建了 view2 文件夹s)
话不多说直接上代码
js
import type { RouteRecordRaw } from 'vue-router'
//声明类型,也是为了使用方便,直接声明,后续强转,实际类型算是包含关系
type DirType = {
path: string
name?: string
component?: unknown
children?: DirType[]
}
const generateImportNextAppsRoutes = (): RouteRecordRaw[] => {
//webpack require.context vite import.meta.glob
const modules: Record<string, unknown> = import.meta.glob(
[
'@/views2/**/page.vue',
'@/views2/**/page.tsx',
'@/views2/**/page.jsx',
'@/views2/**/page.json',
'@/views2/**/layout.vue',
'@/views2/**/layout.tsx',
'@/views2/**/layout.jsx',
'@/views2/**/page.json',
], //排除前面加上感叹号 !@/views/Dashboard/*.vue
{
eager: true, //默认false懒加载import,设置为false则直接导入所有
},
)
//直接整理一下key、default,避免后续取出麻烦
const comObj: Record<string, unknown> = {}
const paths: string[][] = []
for (const key in modules) {
const path = key.replace(/^(\/src\/views2)|(\.vue|\.tsx|\.jsx)$/g, '').toLocaleLowerCase()
comObj[path] = (modules[key] as { default: unknown }).default
const temPaths = path.split('/').filter((e) => e)
paths.push(temPaths)
}
const structs = generateStruct(paths, comObj)
console.log('structs', structs)
return structs as RouteRecordRaw[]
}
//根据路径、对照表、父节点递归出我们的路由结构
const generateStruct = (
paths: string[][],
comObj: Record<string, unknown>,
parent?: DirType,
): DirType[] => {
const structs: DirType[] = []
const map = new Map<string, string[][]>()
for (let idx = 0; idx < paths.length; ) {
const item = paths[idx]
let path = parent ? item[0] : `/${item[0]}`
if (item.length === 1) {
if (parent) {
path = `${parent.path}/${path}`
}
paths.splice(idx, 1)
if (parent) {
//对于 page 文件,其为组件,直接给父路径赋值 component 即可
//name 必然在文件夹那一级拼接好了
if (item[0] === 'page') {
parent.component = parent.component || comObj[path]
} else if (item[0].match(/^(page)\.json$/)) {
//存在配置文件,json 里面是有 path 的
parent.component = parent.component || comObj[`${parent.path}/page`]
parent.path += (comObj[path] as { path: string }).path || ''
parent.name = (comObj[path] as { name: string }).name || ''
} else if (item[0] === 'layout') {
parent.component = comObj[path]
}
continue
}
structs.push({
path,
component: comObj[path],
})
} else {
const paths = map.get(path) || []
item.splice(0, 1)
paths.push(item)
map.set(path, paths)
idx++
}
}
for (const [key, value] of map) {
const obj: DirType = {
path: parent ? `${parent.path}/${key}` : key,
}
obj.children = generateStruct(value, comObj, obj)
structs.push(obj)
}
return structs
}
const nextAppImportRoutes = generateImportNextAppsRoutes()
export default nextAppImportRoutes
结果如下所示,也是没啥问题,并且还支持了动态路由匹配
我们想配置动态路由,只需要创建一个 page.json、layout.json
即可
js
{
"path": "/:id?"
}
菜单
js
//模版
<template>
<div class="w-full h-dvh flex">
<el-menu
active-text-color="#ffd04b"
background-color="#545c64"
class="w-[200px] h-full"
:default-active="route.path" //直接绑定route.path刷新时能够切换到确切标签
text-color="#fff"
router
>
//遍历节点,一共两层,直接展开即可
<template v-for="(item, index) in allMenus" :key="index">
<template v-if="item.children">
<el-sub-menu :index="item.key">
<template #title>
<el-image v-if="item.icon" :src="item.icon" />
<span>{{ item.name }}</span>
</template>
<template v-for="(subItem, idx) in item.children" :key="idx">
<el-menu-item :index="subItem.key">{{ subItem.name }}</el-menu-item>
</template>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="item.key">
<el-image v-if="item.icon" :src="item.icon" />
<span class="ml-4">{{ item.name }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
<RouterView />
</div>
</template>
//ts
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { reactive } from 'vue'
const route = useRoute()
//获取所有的 Menus 配置,用于展开到列表,内容多可以提取到外部(例如:menus.ts),然后导入使用即可
const allMenus = reactive([
{
key: '/dashboard/location',
name: '首页',
icon: new URL('@/assets/react-logo.png', import.meta.url).href,
},
{
key: '/dashboard/menu',
name: '菜单',
icon: new URL('@/assets/react-logo.png', import.meta.url).href,
},
{
key: '/dashboard/setting',
name: '设置',
icon: new URL('@/assets/react-logo.png', import.meta.url).href,
children: [
{
key: '/dashboard/setting/setting1',
name: '设置一',
children: [],
},
{
key: '/dashboard/setting/setting2',
name: '设置二',
children: [],
},
],
},
])
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
console.log('query', route.query)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const onSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
最后
就介绍到这里吧,慢慢学习,慢慢进步,一个持续进步的开发仔🤣