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


Posted in Javascript onMarch 06, 2020

个人理解:动态路由不同于常见的静态路由,可以根据不同的「因素」而改变站点路由列表。常见的动态路由大都是用来实现:多用户权限系统不同用户展示不同导航菜单。

如何利用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属性,使用简短字符串代替布局组件,使用文件路径字符串代替页面引入。具体实现可以看后面的代码实例。

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

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

这里我没做太多考察,直接将取到数据存储到了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 页面的路由也加入到动态路由中。

动态路由刷新后变空白页

造成这一问题的原因有很多,我这里遇到的问题是使用参考文章3解决的,但具体原理我还没弄清楚,等我做一下研究再来更新。

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

造成这一问题的原因很简单:因为页面刷新的时候路由信息还没加载进来,所以根本没有标题信息可供加载。但是我还没找到比较好的解决方案,同样等我研究一下再更新。

参考大师兄:

Vue 动态路由的实现……

Vue Router 文档页面

rambo:vue router 动态路由 刷新后变空白页

总结

到此这篇关于Vue Router 实现动态路由和常见问题及解决方法的文章就介绍到这了,更多相关vue router 动态路由内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript function、指针及内置对象
Feb 19 Javascript
使用jQuery+HttpHandler+xml模拟一个三级联动的例子
Aug 09 Javascript
JAVASCRIPT车架号识别/验证函数代码 汽车车架号验证程序
Jan 08 Javascript
js实现二代身份证号码验证详解
Nov 20 Javascript
微信小程序 Video API实例详解
Oct 02 Javascript
Bootstrap 实现查询的完美方法
Oct 26 Javascript
基于zTree树形菜单的使用实例
Dec 25 Javascript
Vue CLI3搭建的项目中路径相关问题的解决
Sep 17 Javascript
浅谈Vue为什么不能检测数组变动
Oct 14 Javascript
js 计算月/周的第一天和最后一天代码
Feb 01 Javascript
javascript中正则表达式语法详解
Aug 07 Javascript
Vue实现图书管理案例
Jan 20 Vue.js
Vue基于iview实现登录密码的显示与隐藏功能
Mar 06 #Javascript
Vue状态模式实现窗口停靠功能(灵动、自由, 管理后台Admin界面)
Mar 06 #Javascript
javascript中可能用得到的全部的排序算法
Mar 05 #Javascript
js的Object.assign用法示例分析
Mar 05 #Javascript
Element的el-tree控件后台数据结构的生成以及方法的抽取
Mar 05 #Javascript
element-ui树形控件后台返回的数据+生成组织树的工具类
Mar 05 #Javascript
vue中使用vue-print.js实现多页打印
Mar 05 #Javascript
You might like
生成静态页面的PHP类
2006/07/15 PHP
PHP配置心得包含MYSQL5乱码解决
2006/11/20 PHP
PHP通用检测函数集合
2011/02/08 PHP
显示youtube视频缩略图和Vimeo视频缩略图代码分享
2014/02/13 PHP
深入浅析用PHP实现MVC
2016/03/02 PHP
让你的PHP7更快之Hugepage用法分析
2016/05/31 PHP
PHP中数组转换为SimpleXML教程
2019/01/27 PHP
项目实践之javascript技巧
2007/12/06 Javascript
JavaScript Event学习第五章 高级事件注册模型
2010/02/07 Javascript
JavaScript 小型打飞机游戏实现原理说明
2010/10/28 Javascript
extjs 3.31 TreeGrid实现静态页面加载json到TreeGrid里面
2013/04/02 Javascript
兼容IE和Firefox火狐的上下、左右循环无间断滚动JS代码
2013/04/19 Javascript
使用jquery hover事件实现表格的隔行换色功能示例
2013/09/03 Javascript
JavaScript实现多维数组的方法
2013/11/20 Javascript
JS字符串拼接在ie中都报错的解决方法
2014/03/27 Javascript
node.js中的定时器nextTick()和setImmediate()区别分析
2014/11/26 Javascript
node.js中的fs.fstatSync方法使用说明
2014/12/15 Javascript
jquery动态导航插件dynamicNav用法实例分析
2015/09/06 Javascript
JS 日期与时间戮相互转化的简单实例
2016/06/22 Javascript
基于JS实现类似支付宝支付密码输入框
2016/09/02 Javascript
jquery文字填写自动高度的实现方法
2016/11/07 Javascript
详解React native全局变量的使用(跨组件的通信)
2017/09/07 Javascript
9种使用Chrome Firefox 自带调试工具调试javascript技巧
2017/12/22 Javascript
jQuery实现鼠标响应式淘宝动画效果示例
2018/02/13 jQuery
详解mpvue小程序中怎么引入iconfont字体图标
2018/10/01 Javascript
Python中使用wxPython开发的一个简易笔记本程序实例
2015/02/08 Python
C#返回当前系统所有可用驱动器符号的方法
2015/04/18 Python
Python之数据序列化(json、pickle、shelve)详解
2019/08/30 Python
Python3+PyCharm+Django+Django REST framework配置与简单开发教程
2021/02/16 Python
让IE6、IE7、IE8支持CSS3的脚本
2010/07/20 HTML / CSS
澳大利亚免息网上购物:Shop Zero
2016/09/17 全球购物
快时尚眼镜品牌,全国连锁眼镜店:LOHO眼镜生活
2018/10/08 全球购物
J2EE系统只能是基于web
2015/09/08 面试题
电台编导求职信
2014/05/06 职场文书
Python的这些库,你知道多少?
2021/06/09 Python
vue+iview实现手机号分段输入框
2022/03/25 Vue.js