【转载】vue动态路由的实现思路及踩坑

原文:Vue Router 实现动态路由和常见问题及解决方法

Vue项目实现动态路由的方式大体可分为两种:

  • 前端将全部路由规定好,登录时根据用户角色权限来动态展示路由;
  • 路由存储在数据库中,前端通过接口获取当前用户对应路由列表并进行渲染;

第一种方式在很多Vue UI Admin上都实现了,可以去读一下他们的源码理解具体的实现思路,这里就不过多展开。第二种方式现在来说也比较常见了,因为近期项目正好用到所以单独讲一下,这里我使用的方案是利用Vue Router的一些特性实现后端主导的动态路由。

使用到的功能特性

Vue Router 全局前置守卫

官网解释

这里我们主要借助全局前置守卫的「前置」特性,在页面加载前将当前用户所用到的路由列表注入到Router实例中,注入使用到的方法则是下面的router.addRoutes方法。

Vue Router router.addRoutes 实例方法

官网解释

router.addRoutes方法可以为Router实例动态添加路由规则,刚好为我们实现动态路由提供了注入方法。

Vue Router 路由懒加载

官网解释

懒加载这个功能不是动态路由的必要功能,但既然提供了这一特性,所以就直接在项目中使用了。

具体思路

基础信息准备

前端代码实现基本静态路由,例如:登录页路由,服务器错误页路由等(这里有一个坑,后面讲)。数据库存储全部动态路由信息。

数据库如何存储动态路由信息?我选择的方案是现将路由引用的对象字符串化,再将路由列表转化为JSON格式传输给后端,经后端处理后存储到数据库里。总之在前后端进行传递的是JSON格式的路由列表信息。

如何将路由中引用的对象字符串化?我遇到的实际问题是:使用的UI组件提供了布局方案,需要引用布局组件并在子路由处引用具体页面。我选择的解决方案是:区别对待需要引用布局组件的component属性,使用简短字符串代替布局组件,使用文件路径字符串代替页面引入。具体实现可以看后面的代码实例。

利用全局前置守卫对路由信息进行判断

判断用户是否登录 ——》若未登录,跳转至登录页面 ——》若已经登录,判断是否已获取路由列表 ——》若未获取,从后端获取、解析并保存到Vuex中 ——》若已获取,跳转至目标页面

这里我没做太多考察,直接将取到数据存储到了Vuex中,在实际项目应用的过程中应考虑数据存储的安全性。

如何实现路由列表解析?

  1. JSON格式的路由信息解析为JavaScript列表对象;
  2. 利用列表对象的filter方法实现解析函数,通过component判断是否为布局组件;
  3. 若为布局组件,使用布局组件代替component字符串;
  4. 若为具体页面,使用loadView函数加载对应的具体页面;
  5. 利用 router.addRoutes 方法动态添加路由

这一步就很简单了,将解析好的路由列表通过router.addRoutes方法添加到Router实例中即可。

简单的实现代码

// router/index.js
import Vue from 'vue'
import store from '@/store'
import Router from 'vue-router'
import { getToken } from '@/lib/util'
 
Vue.use(Router)
 
// 定义静态路由
const staticRoutes = [
 {
 path: '/login',
 name: 'login',
 meta: {
 title: '登录页面',
 hideInMenu: true
 },
 component: () => import('@/view/login/login.vue')
 },
 {
 path: '/401',
 name: 'error_401',
 meta: {
 hideInMenu: true
 },
 component: () => import('@/view/error-page/401.vue')
 },
 {
 path: '/500',
 name: 'error_500',
 meta: {
 hideInMenu: true
 },
 component: () => import('@/view/error-page/500.vue')
 }
]
 
// 定义登录页面名称(为了方便理解才定义的)
const LOGIN_PAGE_NAME = 'login'
 
// 实例化 Router 对象
const router = new Router({
 staticRoutes,
 mode: 'history'
})
 
// 定义全局前置守卫(里面有两个坑要注意)
router.beforeEach((to, from, next) => {
 // 通过自定义方法获取用户 token 用来判断用户登录状态
 const token = getToken()
 if (!token && to.name !== LOGIN_PAGE_NAME) {
 // 如果没有登录而且前往的页面不是登录页面,跳转到登录页
 next({ name: LOGIN_PAGE_NAME })
 } else if (!token && to.name === LOGIN_PAGE_NAME) {
 // 如果没有登录而且前往的页面是登录页面,跳转到登录页面
 // 这里有一个坑,一定要注意这一步和上一步得分开写
 // 如果把前两步判断合并为 if (!token) next({ name:login })
 // 则会形成登录页面无限刷新的错误,具体成因后面解释
 next()
 } else {
 // 如果登录了
 if (!store.state.app.hasGetRoute) {
 // 如果没有获取路由信息,先获取路由信息而后跳转
 store.dispatch('getRouteList').then(() => {
 router.addRoutes(store.state.app.routeList)
 // 这里也是一个坑,不能使用简单的 next()
 // 如果直接使用 next() 刷新后会一直白屏
 next({ ...to, replace: true })
 })
 } else {
 // 如果已经获取路由信息,直接跳转
 next()
 }
 }
})
 
export default router
// store/index.js
import router from '@/router'
import Main from '@/components/main'
import { getToken } from '@/lib/util'
import { getRoute } from '@/api/app'
 
const loadView = (viewPath) => {
 // 用字符串模板实现动态 import 从而实现路由懒加载
 return () => import(`@/view/${viewPath}`)
}
 
const filterAsyncRouter = (routeList) => {
 return routeList.map((route) => {
 if (route.component) {
 if (route.component === 'Main') {
 // 如果 component = Main 说明是布局组件
 // 将真正的布局组件赋值给它
 route.component = Main
 } else {
 // 如果不是布局组件就只能是页面的引用了
 // 利用懒加载函数将实际页面赋值给它
 route.component = loadView(route.component)
 }
 // 判断是否存在子路由,并递归调用自己
 if (route.children && route.children.length) {
 route.children = filterAsyncRouter(route.children)
 }
 }
 })
}
 
export default {
 state: {
 routeList: [],
 token: getToken(),
 hasGetRoute: false
 },
 mutations: {
 setRouteList(state, data) {
 // 先将 JSON 格式的路由列表解析为 JavaScript List
 // 再用路由解析函数解析 List 为真正的路由列表
 state.routeList = filterAsyncRouter(JSON.parse(data))
 // 修改路由获取状态
 state.hasGetRoute = true
 }
 },
 atcions: {
 getRouteList({ state, commit }) {
 return new Promise((resolve) => {
 const token = state.token
 getRoute({ token }).then((res) => {
 let data = res.data.data
 // 注意这里取出的是 JSON 格式的路由列表
 commit('setRouteList', data)
 resolve()
 })
 })
 }
 }
}

常见问题

页面卡在登录页面而且不断刷新

这个问题的解决方案在「实现代码」中已经提到了,只需要在判断登录状态的时候注意不要将两种未登录状态混为一谈即可。但这样治标不治本,因为同样的问题可以由不同形式的代码导致,那导致问题的原因是什么那?然我们慢慢分析:

我们先假设不小心把两种未登录的状态混在一起判断:

if (!token) {
 next({ name: LOGIN_PAGE_NAME })
}

这里的next({ name: LOGIN_PAGE_NAME })方法会再一次激活全局前置守卫,从而导致再一次进入判断并触发next({ name: LOGIN_PAGE_NAME }),如此递归调用下去,页面就会卡主并且不断刷新。

动态路由配合路由懒加载

实现这一目的的方案也在代码示例中展示了:

const loadView = (viewPath) => {
 return () => import(`@/view/${viewPath}`)
}

这里是运用了一个 JavaScript 不太常用的特性:字符串模板,使用此特性让不支持字符串拼接的import操作能够实现动态import不同的模块。

动态路由刷新后 404

这应该是本方案中最常见的一个错误之一,其原意是很多人在创建「基本静态路由」的时候回把 404 页面的路由也加入在里面,从而导致页面加载初期动态路由还没有加入到路由实例中,匹配范围最广的 404 页面就会跳出来。解决方法就是将 404 页面的路由也加入到动态路由中。

动态路由刷新后变空白页

造成这一问题的原因有很多,我这里遇到的问题是使用 vue-router addRouter添加动态路由,第一次跳转正常,刷新后变空白?

let flag = 0
router.beforeEach((to, from, next) => {
    if(to.path !== '/login'){
        if(flag == 0){
            console.log(flag)
            let privileges = ['list','edit'];
            let permission_routes = routeUtil.GenerateRoutes(privileges);
            router.addRoutes(permission_routes);
            flag++
            next({ ...to, replace: true })
        }else{
            next()
        }
    }else{
        next();
    }
})

动态路由页面刷新时 Title 不稳定

造成这一问题的原因很简单:因为页面刷新的时候路由信息还没加载进来,所以根本没有标题信息可供加载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351