携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情
vue3 + nestjs 自给自足,实现动态路由(二)
前端我们使用了 vue3 + ts + ant-design-vue 来编写
vue-router使用 v4 版本,使用 addRoute 添加路由
效果图
路由配置
上篇文章中,我们可以通过接口拿到路由信息,那我们需要在 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 鉴权登录、用户角色权限
- 路由、角色管理
- ...