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

179 阅读1分钟

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

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

前端我们使用了 vue3 + ts + ant-design-vue 来编写

vue-router使用 v4 版本,使用 addRoute 添加路由

效果图

image.png

路由配置

上篇文章中,我们可以通过接口拿到路由信息,那我们需要在 router.beforeEach 守卫中,将这个路由信息整理好格式,进行路由配置

/**
 * 得到正确的路由列表
 */
export function fetchRoutes(routerList): RouteRecordRaw[] {
    const rou = []
    routerList.forEach((e) => {
        let obj: RouteRecordRaw = {
            path: e.url,
            name: e.name,
            component:
                e.component === 'App'
                    ? () => import(`@/App.vue`)
                    : () => import(`@/pages/demo/${e.component}.vue`)
        }

        if (e.redirect !== '') {
            ;(obj.redirect as any) = e.redirect
        }

        if (e.children.length > 0) {
            const child = addRouter(e.children)
            obj = { ...obj, children: child }
        }
        rou.push(obj)
    })
    return rou
}
/**
 * 添加路由
 * router.addRoute
 */
export function addRouter(routerList) {
    return new Promise((resolve) => {
        console.log(routerList)
        routerList.forEach((e) => {
            if (e.children) {
                router.addRoute(e)
                e.children.forEach((r) => {
                    router.addRoute(e.name, r)
                })
                addRouter(e.children)
            } else {
                router.addRoute(e)
            }
        })
        resolve(true)
    })
    // eslint-disable-next-line no-param-reassign
}

/**
 * 基础路由
 */

const getRoutes = async () => {
    const { data } = await axios.get('http://localhost:3010/api/route')
    return data
}

router.beforeEach(async (to, from, next) => {
  // 后续添加 token 登录等逻辑
  const routerList = await getRoutes()
  const r = fetchRoutes(routerList)
  await addRouter(r)
  next()
})

编写到这里发现个问题,当每次切换路由,它都会去执行: 获取路由信息-添加路由。

我们是不需要每次都去执行这样的操作,所以我们使用 store 保存一个 flag 作为判断是否成功初始化了路由。


function routerInit(routerList, demo) {
    return new Promise((resolve) => {
        demo.routerList = routerList // 路由信息存放在 pinia 中
        demo.init = true
        resolve(true)
    })
}

router.beforeEach(async (to, from, next) => {
  // 后续添加 token 登录等逻辑
  const demo = useDemo()
  if (!demo.init) {
      const routeList = await getRoutes()
      const r = addRouter(routeList)
      console.log(r)
      await getRouter(r)

      await routerInit(r, demo)
      // 用于解决刷新后,地址正确但是页面空白的问题:在刷新后动态路由需要重新获取,而to对象是在动态路由生成之前产生,所以获取不到真正路由信息。
      if (to.path) next({ path: to.path })
  }
  next()
})

路由配置完毕,那我们现在就开始编写侧边导航栏组件

template写法

app-layout.vue

<a-layout>
    <a-layout-sider>
        <a-menu
            id="dddddd"
            :selected-keys="[route.path]"
            style="width: 200px"
            mode="inline"
            :open-keys="openKeys"
            @click="handleClick"
        >
            <SideItem v-for="item in demo.routerList" :key="item.path" :item="item" />
        </a-menu>
    </a-layout-sider>
    <a-layout>
        <div class="z-layout-header">
            <a-layout-header>Header</a-layout-header>
        </div>
        <a-layout-content>
            <RouterView />
        </a-layout-content>
    </a-layout>
</a-layout>

递归组件 side-item.vue

<div>
    <template v-if="!item.children">
        <a-menu-item :key="item.path">
            <template #icon>
                <PieChartOutlined />
            </template>
            {{ item.name }}
        </a-menu-item>
    </template>
    <a-sub-menu v-else :key="item.path" :title="item.name">
        <side-item v-for="child in item.children" :key="child.path" :item="child" />
    </a-sub-menu>
</div>

刷新自动高亮 item,展开对应导航


const demo = useDemo()

const router = useRouter()
const route = useRoute()

const openKeys = ref([])

function getOpenKeys(path: string) {
    const paths = path.split('/')
    const arr = paths.splice(1, paths.length - 2)

    let item = ''
    return arr.map((i) => {
        item = `${item}/${i}`
        return item
    })
}

watch(
    () => route.path,
    (val) => {
        openKeys.value = getOpenKeys(val)
    }
)

const handleClick: MenuProps['onClick'] = (e) => {
    router.push({ path: e.key as string })
}

可能有疑问,为什么要 watch 路由参数 route.path?

可以直接使用 route.path 获取一下,会发现得到的是 /

当 vue 实例调用 created、mounted 的时候,vueRouter里面的 next 还没执行,还没执行到 next(),所以 vueRouter 的状态还没切换,所以此时拿到 path 还是 /,只有当动态路由加载完成,才会执行 next()。

jsx写法

app-layout.tsx

const renderMenu = (list) => list.map((i) => {
    if (i.children) {
        return (
            <a-sub-menu key={i.path} title={i.name}>
                {renderMenu(i.children)}
            </a-sub-menu>
        )
    }
    return (
        <a-menu-item
            key={i.path}
            v-slots={{
                icon: () => <PieChartOutlined />
            }}
        >
            {i.name}
        </a-menu-item>
    )
})

return () => (
    <a-layout>
        <a-layout-sider>
          <a-menu
              id="ddddd"
              style={{ width: '200px' }}
              mode="inline"
              onClick={handleClick}
          >
              {renderMenu(demo.routerList)}
          </a-menu>
        </a-layout-sider>
        <a-layout>
            <div class="z-layout-header">
                <a-layout-header>Header</a-layout-header>
            </div>
            <a-layout-content>
                <router-view />
            </a-layout-content>
        </a-layout>
    </a-layout>
)

我还是更喜欢 jsx 写法,比较简单,主要就是一个递归函数 renderMenu() 的编写。

后续

  • token 鉴权登录、用户角色权限
  • 路由、角色管理
  • ...