自动生成路(以及仿nextjs路由)、并做一个简易Element菜单

前言

看了 nextjs 的路由,用新项目的时候,总是不想自己写路由文件了,并且总想自己写一个自动导入的,用了 vue 后,发现可以用到了,于是乎就做一个

下面将介绍一个简易版的自动生成路由,以及仿 nextjs 的目录结构路由,以及带入菜单的原因

案例demo

ps:似乎是有相关插件,就是不用,任性哈

路由和菜单

这里为什么要做一个 element 的菜单呢,实际上是因为很多人以前写路由时,会将菜单和路由写到一起,一起处理管理,一套多用,因此很多人对自动路由不太感冒,实际上路由和菜单混合到一起,页有不少问题

  1. 路由是路由,菜单菜单,菜单主要包含了目录、icon、跳转路径等,实际上不一定包含所有路径,就像前端经常讨论的 model、viewModel 一样,有时用的一样,有时用不一样,实际上他们不是一个概念
  2. 放到一起,那么路由和菜单将耦合在一起,多出来的还需要有剔除逻辑,或者分离合并,甚至一些权限逻辑都放到了路由中,再加上权限,耦合会严重,维护成本持续增加
  3. 路由和菜单分开后,功能隔离,模块清晰,即使加入权限后,仍然是权限 + 路由绑定,因此会形成 菜单 + 路由权限 + 路由 等逻辑,自由组合,代码解耦,并且逻辑易读,而权限判断则是在 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>

最后

就介绍到这里吧,慢慢学习,慢慢进步,一个持续进步的开发仔🤣

相关推荐
豐儀麟阁贵3 分钟前
8.5在方法中抛出异常
java·开发语言·前端·算法
zengyuhan50332 分钟前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休36 分钟前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running44 分钟前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔44 分钟前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654261 小时前
Android的自定义View
前端
WILLF1 小时前
HTML iframe 标签
前端·javascript
枫,为落叶1 小时前
Axios使用教程(一)
前端
小章鱼学前端1 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah1 小时前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript