记一次动态注册路由导致的路由跳转阻塞问题
背景
最近在写的项目遇到这样一个问题:
页面在登录后,查询一个接口获取租户 ID,然后异步刷新页面,把获取到的租户 ID 挂载在路由路径里
在获取到租户 ID 后的这次刷新页面的路由守卫(Router Guard)的钩子里,代码会动态下载一段脚本并执行,该脚本会调用 Vue Router 的 addRoute()
方法动态注册路由
之后执行路由守卫的 next()
放行本次跳转
问题
问题来了,由于该次刷新页面是异步操作,而且动态下载脚本也是异步操作,有一定的几率,addRoute()
和 next()
在同一个 Vue 的刷新任务队列里执行,然后就会发生路由跳转停止,反映到页面上,就是
页面点击登录后不再跳转到主页面
解析
之前有同事跟我说,这是因为 addRoute()
会打断路由跳转,所以我们写了一些代码来规避这件事
总的思路是,在这次异步 next()
之前发送事件通知脚本延后执行 addRoute()
,这样做有一个问题,即延后多久执行 addRoute()
,我们原先设定的是200ms,但结果是仍然有概率与 next()
运行在同一个任务队列里,再延长这个时间其实也不是最好的办法,故这个问题搁置了很久
直到有客户的自动化测试因为这个问题而打断运行,我终于不得不仔细研究这个问题
查看 Vue Router 3 的源码,我发现在 addRoute()
的定义如下:
1 | addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) { |
其中的 this.matcher.addRoute(parentOrRoute, route)
这一段只是在对路由信息进行维护,并不会有打断路由跳转的风险
问题出在下面的 if 判断中,对于我们的登录后异步加载脚本并执行路由注册的代码逻辑,this.history.current
一定不会与 START
相等(因为 this.history.current
是登录页,而 START
是根路由),那么根据这段代码,Vue Router 会尝试路由到当前页面,即登录页
在 history.transitionTo()
方法里,根据传来的路由,设置目标跳转对象 pending
,简化后的源码如下
1 | transitionTo ( |
由上面的代码可知,此时的 pending
已经由根路由改为了登录页路由
好了,当异步的 next()
在这个任务队列里被调用,就会发现,目标跳转路由是登录页,当前路由也是登录页,路由跳转停止
解决
了解了打断路由跳转的原因,兼容代码就好写了,我翻看了 github 的 issues,发现有人提出,这里其实不做那次 history.transitionTo()
也是可以正常注册成功的,于是我调整了注册路由的代码,不再调用 router.addRoute()
,而是调用 router.matcher.addRoute()
绕过了多余的 history.transitionTo()
1 | const addRoutes = function(routes) { |
深入
那么,代价呢?
其实我还没看明白 Vue Router 在这里刻意这么做的目的是什么,但是由此确实带来一个问题,即当登录后跳转的页面是新注册的路由时,由于该路由在跳转时正在注册,于是 next()
时便找不到该路由信息,于是页面会停止渲染,变成空白页面
我现在的解决方案是,当识别到 pending
是准备注册的路由时,在执行 addRoute()
的200ms后,使用 router.replace()
刷新当前页面,从而使得已注册的路由被正确地渲染