自动生成路(以及仿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>

最后

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

相关推荐
流烟默36 分钟前
vue和微信小程序处理markdown格式数据
前端·vue.js·微信小程序
梨落秋溪、43 分钟前
输入框元素覆盖冲突
java·服务器·前端
菲力蒲LY1 小时前
vue 手写分页
前端·javascript·vue.js
天下皆白_唯我独黑1 小时前
npm 安装扩展遇到证书失效解决方案
前端·npm·node.js
~欸嘿1 小时前
Could not download npm for node v14.21.3(nvm无法下载节点v14.21.3的npm)
前端·npm·node.js
化作繁星2 小时前
React 高阶组件的优缺点
前端·javascript·react.js
zpjing~.~2 小时前
vue 父组件和子组件中v-model和props的使用和区别
前端·javascript·vue.js
做一颗卷心菜2 小时前
Promise
开发语言·前端·javascript
bin91532 小时前
DeepSeek 助力 Vue 开发:打造丝滑的 键盘快捷键(Keyboard Shortcuts)
前端·javascript·vue.js·计算机外设·ecmascript·deepseek
格式化小拓3 小时前
在vue2中操作数组,如何保证其视图的响应式
前端·javascript·vue.js