vue3 + nestjs 自给自足,实现动态路由(一)

609 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

vue3 + nestjs 自给自足,实现动态路由

vue3.x + vue-router@4 + nestjs

效果图

动画2.gif

思路

  • 与后端商量基本数据格式编写接口;

  • 前端在本地只配置不参与权限控制的路由,比如 layout、login、not-found,其他权限路由登录之后请求后端查询接口,返回实例格式如下:

    {
        code: 200,
        message: "success",
        data: {
            // 当前角色路由信息
            "menus": [{
                name: 'demo01',
                path: '/demo01',
                redirect: '/demo01/demo05',
                component: 'app',
                children: [/* ... */],
                ...title、icon、meta
            }],
            // 当前角色
            "role": 'xxx'
        }
    }
    
  • 前端在 router.beforeEach 路由守卫使用 addRoute 添加路由,基本代码如下:

    router.beforeEach(async (to, from, next) => {
        const demo = useDemo()
    
        if (!demo.init) {
            const routeList = await getRoutes()
            const r = fetchRoutes(routeList)
            await addRouter(r)
            await routerInit(r, demo)
            if (to.path) next({ path: to.path })
        }
        next()
    })
    
  • 把路由列表存储到 store(pinia 或者 vuex),layout 页面从 store 获取路由列表进行动态渲染侧边导航栏。

接口开发

使用 nest@8.0.x + typeorm@^0.2.45 开发接口,我也是刚接触这种树形实体,所以不懂的就多看看文档吧。

typeorm文档

树形实体Route

import {
  Entity,
  Tree,
  Column,
  PrimaryGeneratedColumn,
  TreeChildren,
  TreeParent,
} from 'typeorm';

@Entity()
@Tree('closure-table')
export class Route {
  // 可以用 @PrimaryGeneratedColumn('uuid') 自动生成 uuid
  @PrimaryGeneratedColumn() 
  id: number;

  @Column({ length: 50 })
  component: string;

  @Column({ length: 50, default: '' })
  redirect?: string;

  @Column({ length: 50 })
  url: string;

  @Column({ length: 50 })
  name: string;

  @TreeChildren()
  children: Route[];

  @TreeParent()
  parent: Route;
}

controller

@Get()
@SkipJwtAuth()
findAll() {
  return this.routeService.findAll();
}

service

findTrees 返回树的所有数据,可以看成是 findAll()

async findAll() {
  // const res = await this.routeRepository.findTrees();
  return await this.routeRepository.findTrees();
}

mock数据

树形实体的保存,需要先存在一个模型,然后子模型通过 .parent 的方式设置父子关系,再 save 到表中,采用递归是比较合适的做法。

await connection.synchronize(); // 表不存在建表
await connection.createQueryBuilder().delete().from(Route).execute(); // 删除所有记录
​
const mockRoute = [
  {
    name: 'demo01',
    path: '/demo01',
    redirect: '/demo01/demo05',
    component: 'App',
    children: [
      {
        name: 'demo10',
        path: '/demo01/demo05',
        component: 'demo-test',
        children: null,
      },
      {
        name: 'demo06',
        path: '/demo01/demo02',
        component: 'App',
        children: [
          {
            name: 'demo07',
            path: '/demo01/demo02/demo03',
            component: 'demo03',
          },
        ],
      },
    ],
  },
  {
    name: 'demo02',
    path: '/demo02',
    redirect: '/demo02/demo04',
    component: 'App',
    children: [
      {
        name: 'demo08',
        path: '/demo02/demo04',
        component: 'demo-04',
        children: null,
      },
    ],
  },
  {
    name: 'demo03',
    path: '/demo03',
    component: 'demo-03',
    children: null,
  },
  {
    name: 'demo04',
    path: '/demo04',
    component: 'demo-04',
    children: null,
  },
  {
    name: 'demo-test',
    path: '/demo-test',
    component: 'demo-test',
    children: null,
  },
];
// 添加树形数据
function mock(list, parent = undefined) {
  list.map(async (u) => {
    const r1 = new Route();
    r1.url = u.path;
    r1.name = u.name;
    r1.component = u.component;
    if (u.redirect) r1.redirect = u.redirect;
    if (parent) {
      r1.parent = parent;
    }
    const p1 = await routeRepository.save(r1);
    if (u.children) {
      mock(u.children, p1);
    }
  });
}
​
await mock(mockRoute);

最后接口返回的数据如下:

code: 200
data: [{id: 16, component: "App", redirect: "/demo01/demo05", url: "/demo01", name: "demo01",},]
0: {id: 16, component: "App", redirect: "/demo01/demo05", url: "/demo01", name: "demo01",}
1: {id: 17, component: "App", redirect: "/demo02/demo04", url: "/demo02", name: "demo02",}
2: {id: 18, component: "demo-test", redirect: "", url: "/demo-test", name: "demo-test", children: []}
3: {id: 19, component: "demo-03", redirect: "", url: "/demo03", name: "demo03", children: []}
4: {id: 20, component: "demo-04", redirect: "", url: "/demo04", name: "demo04", children: []}]

这里说明一下字段的用处把

  • url 对应跳转的 path;

  • component 用于引入页面,比如我的页面都存放在 src/pages 目录下,那我路由配置组件的路径如下

    // demo-03 => src/pages/demo-03/demo-03.vue
    component: () => import(`@/pages/${component}/${component}.vue`)
    
  • redirect 应该是可选的,到时候配置路由应该判断它是否是 "",如果是的话就不赋值

    let obj: RouteRecordRaw = {
      path: r.url,
      name: r.name,
      component:
      r.component === 'App'
      ? () => import(`@/App.vue`)
      : () => import(`@/pages/demo/${e.component}.vue`)
    }
    ​
    if (r.redirect !== '') {
      ;(obj.redirect as any) = r.redirect
    }
    

本篇主要讲了动态路由思路,后端接口不重要,但是还是把代码放一下,可以把重点放在思路那里,下一篇主要讲解并编写前端的逻辑。

后续

  • vue3 递归组件使用方式和 tsx 方式编写侧边导航栏
  • 学习基本递归函数的编写
  • vue-router@4 addRoute api使用、
  • token 鉴权登录、用户角色权限
  • 路由、角色管理
  • ...