概要:
公司的一个项目中使用了根据路由配置生成对应的Route,而配置会存在一份在store中,当store中的RouteConfig变化时,会根据最新的配置来生成最新的试图。
因为路由配置系统实现上的一些缺陷本次需要对其就行性能上的一些优化,优化后的路由系统在运行时偶尔会导致页面白屏,在排查后总结一下问题导致的原因和问题的解决方案,和过程中的一些思考,以便后期回归。
背景:
在业务系统中我们的路由方案采用了根据文件夹格式生成的方式,例如系统中存在文件夹:
/home
/plantform
--/list
--/detail
--/create
/system-setting
--/banner-setting
--/notice-setting
那么系统中就会存在对应的路由配置FullRouteConfig
:
[{id: 'home',path: '/home',component: Home,},{id: 'plantform',path: '/plantform',component: Home,children: [{id: 'plantform_list',path: 'list',component: HomeList,},{id: 'plantform_detail',path: 'detail',component: HomeDetail,},{id: 'plantform_create',path: 'create',component: HomeCreate,}]},{id: 'system_setting',path: '/system-setting',component: SystemSetting,children: [{id: 'system_setting_banner_setting',path: 'banner-setting',component: BannerSetting,},{id: 'system_setting_notice_setting',path: 'notice-setting',component: NoticeSetting,},]},
]
有了整个网站的FullRouteConfig
根据接口返回权限数据UseableMenus
可以diff出当前用户具有的路由权限UseableRouteConfig
。
接口返回的权限数据UseableMenus
:
[{id: 'home',},{id: 'plantform',children: [{id: 'plantform_list',}]},{id: 'system_setting',children: [{id: 'system_setting_banner_setting',},{id: 'system_setting_notice_setting',},]},
那么UseableRouteConfig
的数据就是:
[{id: 'home',path: '/home',component: Home,},{id: 'home',path: '/plantform',component: Home,children: [{id: 'home_list',path: 'list',component: HomeList,},]},{id: 'system_setting',path: '/system-setting',component: SystemSetting,children: [{id: 'system_setting_notice_setting',path: 'notice-setting',component: NoticeSetting,},]},
]
没有权限的plantform路由下的两个路由就被删掉了:
[{id: 'plantform_list',path: 'list',component: HomeList,},{id: 'plantform_detail',path: 'detail',component: HomeDetail,},
]
到这里基本的路由方案已经完成,但还是不够完善,在实际使用中,很多业务系统由于过于庞大会采用根据路由分包的手段来对项目进行项目进行优化。采用分包优化后我们的FullRouteConfig
中的路由配置就会不完整,例如:
[...,{id: 'dynamic',path: '/dynamic/*',compnent: React.lazy(import('./dynamic')),children: [],}
]
在路由dynamic/*
下其实是有children但是因为在文件’./dynamic’
中,所以静态定义不出来,需要等到代码运行的时候才能将文件’./dynamic’
中的路由补上。
‘./dynamic’
:
const dynamicRouteConfig = [{id: 'dynaimc_crate',path: 'create',compnent: DynamicCreate,children: [],}
]// 将自身子路由dynamicRouteConfig更新到对应的父路由dynamic下
updateUseableRouteConfig('dynamic', dynamicRouteConfig)export default function Dynamic(props) {return props.children
}
所以如果接口中有对应路由权限,那么完整的UseableRouteConfig
如下:
[{id: 'home',path: '/home',component: Home,},{id: 'home',path: '/plantform',component: Home,children: [{id: 'home_list',path: 'list',component: HomeList,},]},{id: 'system_setting',path: '/system-setting',component: SystemSetting,children: [{id: 'system_setting_notice_setting',path: 'notice-setting',component: NoticeSetting,},]},// -------- 以下是children是动态添加的 ---------{id: 'dynamic',path: '/dynamic/*',compnent: React.lazy(import('./dynamic')),children: [{id: 'dynaimc_crate',path: 'create',compnent: DynamicCreate,children: [],}],}
]
为什么动态路由的更新是作用在
UseableRouteConfig
上而不是FullRouteConfig
上?
可能是当时脑子抽了,想着FullRouteConfig
+UseableMenus
+DynamicRouteConifg
=UseableRouteConfig
这个思路本身没问题,但确实导致了意想不到的问题。
所以到这里页面初始化流程是:加载页面后执行js得到FullRouteConfig
(动态加载的RouteConfig不在其中),通过接口获取菜单权限信息,生成UseableRouteConfig
,根据UseableRouteConfig
生成对应页面,如果是动态路由对应的页面(dynamic),加载分包文件,然后动态注册路由,将动态路由更新到UseableRouteConfig
上。
所以视图的更新是响应UseableRouteConfig
变化的,会随着UseableRouteConfig
的变化而变化。
到这里路由系统工作的很正常,没有问题。
要做什么?
从上面的流程中可以发现页面渲染需要等到接口菜单权限UseableMenus
返回后和FullRouteConfig
一起计算出UseableRouteConfig
然后开始渲染页面。所以接口请求和页面渲染是串行的,对性能有一定损耗,我们希望可以将流程改为并行,将权限接口请求的时间节省出来,让页面更快响应。并且经过评估用户权限并不会频繁变更,通过缓存用户菜单权限可以做到。
首先,我们将权限接口返回的数据缓存在localStorage中,在进入网站后去localStorage中读取,如果有则先试用改数据,同时请求对应的权限接口,接口返回后将数据和localStorage中的数据就行对比,如果不相等,使用最新的权限数据UseableMenus
根据FullRouteConfig
重新计算UseableRouteConfig
在根据这个结果渲染对应页面。
遇到的问题?
上面的方案看似没有问题,实现之后我们发现当我切换用户登录后刷新网站有的时候会白屏,而有的时候则不会。这个奇怪的现象一开始让我们摸不着头脑,经过反复尝试后发现在动态加载的路由下偶尔可以复现这个问题,而在非动态加载的路由下则没有这个问题。
问题分析
- 网页切换用户后刷新老页面会白屏(对应路由的组件没有渲染)
- 复现问题的路由是动态加载的路由
- 问题并非稳定复现,有时候有,有时候没有
用户切换登录,代码执行流程为:下载js得到FullRouteConfig
(这里的路由不完整,动态路由要代码执行的时候才能获取到),根据缓存的权限数据UseableMenus
计算出UseableRouteConfig
,根据UseableRouteConfig
渲染页面,发现是动态加载的路由,执行代码将动态加载的路由注入到UseableRouteConfig
中。在前面获取缓存的权限数据的同时发送请求寻找最新的权限数据。因为切换了登录用户,所以缓存数据一定是失效的需要根据新的UseableMenus
和FullRouteConfig
重新计算UseableRouteConfig
。
在这个过程中是分包的文件先下载并执行完成还是最新的权限数据UseableMenus
下载并执行完成和网络速度有关,并不能保证先后顺序。
如果是新的权限数据先返回就会根据FullRouteConfig
和 UseableMenus
计算出一个有权限的FullRouteConfig
配置也就是UseableRouteConfig
,然后动态路由返回并执行后会将DynamicRouteConfig 依据parentId注入到UseableRouteConfig
中形成完整的UseableRouteConfig
。
如果是动态路由分包文件的**DynamicRouteConfig先返回那么之后返回的UseableMenus
会导致重新根据FullRouteConfig
去计算UseableRouteConfig
而又因为DynamicRouteConfig并不在FullRouteConfig
上,那么计算出来的UseableRouteConfig
自然没有后期注入的DynamicRouteConfig,就导致页面白屏(白屏指的是路由对应的组件没有渲染)。
因为动态路由将自己挂载到UseableRouteConfig只会在文件被加载并执行的时候做一次,所以如果做过之后,UseableRouteConfig
又重新计算了,那么之前做的事情就相当于被冲掉了,因为FullRouteConfig
没有DynamicRouteConfig 这份数据。
例如:
FullRouteConfig
: a, b, c, d
Cached**UseableMenus
: a, b, dynamic, c
UseableRouteConfig
: a, b, c
dynamicRouteConfig
: parentId: b, id: dynamic. 和 UseableRouteConfig
: a, b, c 计算之后:
UseableRouteConfig
: a, b, b>dynamic, c
NewUseableMenus
: a, b, dynamic, c, d
和FullRouteConfig
a, b, c, d 计算之后:
UseableRouteConfig
: a, b, b, c
可以看到dynamic没了,这就是问题的根本原因。
解决问题
到这里问题的原因已经解释清楚了,就是动态路由的挂载在新的权限数据获取之前完成,导致新的权限数据获取到之后将挂载的动态路由数据冲掉了。
核心问题就是新的权限数据获取到跟新UseableRouteConfig
的时候需要把dynamicRouteConfig从老的UseableRouteConfig
中挑出来,然后更新到新的UseableRouteConfig
上。
- 使用新的权限数据和老的
UseableRouteConfig
计算出一个UseableRouteConfig1
- 使用新的权限数据和
FullRouteConfig
计算出一个新的UseableRouteConfig2
- 将
UseableRouteConfig1
上存在但是UseableRouteConfig2
上不存在的菜单复制到UseableRouteConfig2
中 - 将
UseableRouteConfig2
作为新的UseableRouteConfig
为什么
UseableRouteConfig1
不能作为新的UseableRouteConfig
?因为UseableRouteConfig1
中会缺少新权限数据中放开的路由配置,因为它们被之前过滤掉了,所以还是要以UseableRouteConfig2
为准,将UseableRouteConfig2
中没有UseableRouteConfig1
中有的复制到UseableRouteConfig2
即可。
其他更好的方案:
- 动态路由不要更新到
UseableRouteConfig
而是更新到FullRouteConfig
这也是合理的,因为DynamicRouteConfig是从FullRouteConfig
中拆分出去的。等FullRouteConfig被完善后触发一次重新计算UseableRouteConfig
即可 - 不要将DynamicRouteConfig更新到
UseableRouteConfig
而是在对应的组件下直接渲染,这样就是真正的响应式,而不是饶了一大圈,先更新UseableRouteConfig
然后再根据UseableRouteConfig
重新渲染试图,导致数据被冲掉。