45 后端动态路由基础配置 - udo-bit/naive_admin_pro GitHub Wiki

目标

完成对接mock后端接口路由的基础实现

动态路由的两种表达形式

动态路由一般情况下分了大概是两种实现:
一种是前端实现的动态路由。
第二种就是我们的后端接口实现的动态路由。
我们已经讲了前端实现动态路由的方案,那么接下来我们就来实现一下后端动态路由的实现方案。

应用场景

有些情况下,我们需要根据用户的权限,去获取对应的路由数据,并且要求我们可以在后台管理中去实现添加、删除以及配置菜单中的文字、图标等信息,那么这种情况下,就需要使用我们的后端动态路由方案。

开发

首先我们在stores/user.ts添加一个新的action。

const generateDynamicRoutes = async () => {

}

接口配置

这里我们先来实现一下我们整个的路由中需要哪些参数,我们以类型的方式来演示:
在api/user.ts中定义类型:

export interface MenuInfo {
  // 主键id
  id: number
  // 父级id
  pid?: number
  // 路由地址
  path: string
  // 路由名称
  name?: string
  // 路由标题
  title: string
  // 路由图标
  icon?: string
  // 路由组件
  component?: string
  // 路由重定向
  redirect?: string
}

// 路由地址
export const userRoutesUrl = '/user/menus'

// 调用接口
export const userRoutesApi = () => {
  return useGet<any, MenuInfo[]>(userRoutesUrl)
}

实现获取后端路由方法

在stores/user.ts中

const generateDynamicRoutes = async () => {
    const routerData = await userRoutesApi()
    console.log(routerData)
}

在 routes/router-guard.ts中调用。

// 判断当前页面是不是登录页面,如果是登录页面,就跳转到首页
// 调用获取路由信息
const routerRecord = await userStore.generateRoutes()
await userStore.generateDynamicRoutes()

然后我们可以看到,我们拿到的数据是一个被拉平的数据结构,这并不是我们想要的数据结构,那么我们就需要一个通用的方法把数据处理成一个树的结构。
在处理树结构之前,我们先分析一下我们拿到的数据,发现我们拿到的组件是一个字符串。
image.png
这是因为后端不能动态的给我们加载组件,我们需要在前端处理一下,首先我们需要定义一个默认路由信息:
这种定义方式属于我们与后端的约定默认的配置文件。

const defaultRoutes: Record<string, any> = {
  // 默认路由
  RouteView: () => import('~/layouts/base-layout/route-view.vue'),
  // 空白页面
  BlankView: () => import('~/layouts/base-layout/blank-view.vue'),
}

然后我们分别创建这两个文件:
layouts/base-layout/route-view.vue

<script lang="ts" setup>

</script>

<template>
  <router-view />
</template>

layouts/base-layout/blank-view.vue

<script lang="ts" setup>

</script>

<template>
  <div>
    blank page
  </div>
</template>

接下来其他页面我们如何加载呢?
我们这里通过提前加载模块的方式实现,在routes下创建一个modules文件夹,然后创建一个index.ts的文件以及workspace.ts的文件。
workspace.ts

const Workspace = () => import('~/pages/workspace/index.vue')
export default {
  Workspace,
}

index.ts

import Workspace from '~/routes/modules/workspace'
const Home = () => import('~/pages/index.vue')

export default {
  Home,
  ...Workspace,
}

然后接下里实现我们的路由代码逻辑,如下:
在routes目录下创建一个generate-route.ts的文件,用于处理我们扁平的路由。

import type { RouteRecordRaw } from 'vue-router'
import modules from './modules'
import type { MenuInfo } from '~/api/user'
import { userRoutesApi } from '~/api/user'
import { rootRouter } from '~/routes/dynamic-routes'

const defaultRoutes: Record<string, any> = {
  RouteView: () => import('~/layouts/base-layout/route-view.vue'),
  BlankView: () => import('~/layouts/base-layout/blank-view.vue'),
}

const getComponent = (component?: string) => {
  if (!component)
    return defaultRoutes.BlankView
  if (component in defaultRoutes)
    return defaultRoutes[component]

  return (modules as Record<string, any>)[component]
}

const generator = (menuInfo: MenuInfo[], pid?: number | string): RouteRecordRaw[] => {
  const routes: RouteRecordRaw[] = []
  let currentMenus: MenuInfo[] = []
  if (!pid)
    currentMenus = menuInfo.filter(item => !item.pid)
  else
    currentMenus = menuInfo.filter(item => item.pid === pid)

  for (const menuItem of currentMenus) {
    const currentRoute: RouteRecordRaw = {
      path: menuItem.path,
      name: menuItem.name,
      component: getComponent(menuItem.component),
      meta: {
        title: menuItem.title,
        icon: menuItem.icon,
        id: menuItem.id,
        pid: menuItem.pid,
      },
      children: generator(menuInfo, menuItem.id),
    }
    if (!currentRoute.children || currentRoute.children.length === 0)
      delete (currentRoute as RouteRecordRaw).children
    routes.push(currentRoute)
  }
  return routes
}

export const generateRoute = async () => {
  const { data } = await userRoutesApi()
  if (data) {
    const routes = generator(data)
    return {
      ...rootRouter,
      children: routes,
    }
  }
}

然后我们在stores/user.ts中调用

const generateDynamicRoutes = async () => {
  const routerData = await generateRoute()
  if (routerData)
    routerRecords.value = routerData.children

  return routerData
}

最后我们在路由拦截中替换,我们本地的静态路由部分:
routes/router-guard.ts

// const routerRecord = await userStore.generateRoutes()
  const routerRecord = await userStore.generateDynamicRoutes()
  if (to.path === loginRoute) {
    next({
      path: '/',
    })
    return
  }
  else if (routerRecord) {
    router.addRoute(routerRecord)
    next({
      ...to,
      replace: true,
    })
    return
  }

测试页面

warning解决

大家可以看到我们的控制台一直会报一个warning的信息,这是因为第一次跳转的时候找不对对应的路由地址导致的,那么这种情况下我们可以通过路由的正则匹配机制来处理,我们在static-routes.ts中增加如下:

{
  path: '/:pathMatch(.*)',
  component: () => import('~/pages/exception/error.vue'),
},

再测试。

⚠️ **GitHub.com Fallback** ⚠️