如何设计并实现一个所谓"全网最快的路由数据加载 loader"的思路 —— vue/react
前言
这个概念来自于框架 remix ,所谓全网最快有些夸张,很多国内外的大框架也都是这么喊的,所以我也就这么说了~
理论上来说确实快,实现起来对于常见大多数情况来说其实并不难,应该说是比较麻烦和繁琐而已
本文会从,本着结果相同,过程随意的角度阐述如何制作生产可用的版本,以及如何和框架集成的思路,由于最终效果需要根据实际封装而定,所以理念和设计会优先大于教学
解决的什么问题
英文好的可以看官网原文,以下是我用些大白话的解释
这东西是用于解决单页面应用,在切换路由时,导致的接口加载瀑布流的问题诞生的
什么是瀑布流呢
瀑布流是当进入页面,或者切换路由时,用到的所有组件的接口请求顺序。组件是存在上下级的,加载顺序一定是由爷爷到孙子顺序加载,上边的不加载完,底下的不出来。进而导致接口请求的顺序会像瀑布一样
而 loader
要做的就是,在刚进页面/路由切换时,跳过路由校验,第一时间把所有要展示的,与路由树绑定的 loader 函数给执行下。把数据收集起来,当组件被展示在页面上时,数据可能已经好了
//路由表伪代码
const routes = [
{
path: "/", loader: homeLoader,
children: [
{ path: "about", loader: aboutLoader },
{ path: "login", loader: loginLoader },
]
},
{ path: "xxx", loader: xxxLoader }
]
比如这个例子,如果进入了 /about
页面,那么就会命中 homeLoader, aboutLoader
,它们两个会被立即执行,其他的则不会管
loader
通常存在于路由表中,从实际使用角度上来说,完全可以认为它就是用来返回数据的函数,主要是放请求接口的函数,比如 async function loader(){ return {msg: "loader"} }
这个简单的函数就可以说它是个 loader
函数
loader
函数需要结合单页面的组件路由来使用,比如 vue-router react-router-dom
等等
在组件中使用时需要通过一个 hooks
来接收,这东西是封装 loader
的人提供的
整个流程,大致如下
- 进入页面
- 收集所有路由相关的
loader
函数给运行 - 将执行结果全部收集起来,放哪看作者心情
- 对外提供访问这些数据的手段
- 组件内使用该方式获取数据
实现上要考虑的问题
过程第一眼看上去并不复杂,甚至觉得好像没什么难的,但在如何处理数据上会有坑
- 数据竟态
路由切换时,不管旧的有没结果都要取消或者丢弃
这对于 react
挺坑的,如果涉及到权限,路由封装等其他需求,由于相关逻辑一定是放在路由拦截的地方里,所以必定涉及重复进入的问题,那么就要好好想想了
想想怎么保证多次进入时,多次离开时,不会反复执行loader
,想想在多次进入时的前几次,怎么不会把 loader
的结果误删了
- 缓存
如果想把路由守卫封装的强大点,那么可能不同的路由会有不同的细致策略,也就是可能会发生路由守卫嵌套的情况,这个时候是必须要做缓存的,因为祖先守卫已经把loader
的逻辑执行过了,所以此时的逻辑必定是复用
正常判断是用的路由的useLocation().pathname
来做唯一判断,可既然涉及到了缓存,那么就得认真考虑这个复用的值是不是旧的
- 对组件内的
hooks
要提供的数据必须得是最新的 - SSR 服务端渲染怎么支持
- react 在路由切换时是从父组件开始的,请问怎么拿到屁股后哪些才是要用到的子组件呢,如果不知道就拿不到子组件的 loader
- ....
这是几个比较典型的,必须要考虑要解决的问题,react
是重灾区,因为人家什么都不提供,路由守卫,拦截,懒加载,...都得自己来,然后在这个基础上把loader
的逻辑加上去,感觉没什么问题的代码,刚开始跑起来,要么重复跑、要么某些数据是旧的、要么死循环...,总之意外的问题一堆
你问我Vue
?这些问题很多就不存在,难度大幅度降低
如何做设计的思路
设计上其实是很灵活的,这里直接给出通用的实现思路,这里列出,一个简单的,一个复杂的
简单的(client,不支持 ssr)
- 正常的写路由表,要静态,一定要是平铺的!!!,像一般后端管理框架搞的动态的路由后边处理起来会折磨死人
- 确保在写的时候,同级的静态路由要放在动态的前边,因为这两个
/动态路由
和/abc
路由比,一旦前者放前,后者在后续的可能处理中处理不了
//比如可以
const routes = [
{
path: "/",
fullPath: "/",
},
{
path: "about",
fullPath: "/about"
}
]
- 剩下的就是使用取值之类的,这个通常是放在组件的上下文中,然后内部通过上下文取。按照喜欢的方式自由发挥即可
复杂的(client + SSR)
-
正常的写路由表,要静态,像一般后端管理框架搞的动态的路由后边处理起来会折磨死人
-
对列表进行排序,因为这两个
/动态路由
和/abc
路由比,一旦前者放前,后者可能会进不去(对于框架来需要程序实现排序,对于开发者来说,手动控制就不需要排了) -
数据结构要支持,要做出以下两个,这里需要递归第一步的路由表制作 -- demo看下边
- 一个树形的,每个路由都有属于自己的全路径的,存在父子引用字段的路由表
- 一个把树形列表扁平化的列表
-
进入路由时
- 客户端扫描出所有用到的
loader
执行 - 服务端从
routeMap
扫出来进的是哪个
- 客户端扫描出所有用到的
-
剩下的就是使用取值之类的,这个通常是放在组件的上下文中,然后内部通过上下文取。按照喜欢的方式自由发挥即可
//比如可以
const routes = [
{
parent: null,
path: "/",
fullPath: "/",
children: [
{
parent: //自己的上一级
path: "about",
fullPath: "/about"
}
]
}
]
const routeMap = [
{ parent: null, path: "", fullPath: "/" },
{ parent: "", path: "", fullPath: "/about" }
]
分别解释下为什么这么做
因为我们得能在路由进入的第一时间知道,有哪些路由会被用到,为了以最快的速度执行全部loader
函数,所以不允许抱着等组件加载到了就知道了的想法(vue 一开始就能知道,但 react 做不到)
fullPath
字段是用来做匹配用的,因为存在动态路由,所以不能通过===
的方式匹配,需要用到各个路由库提供的useMatch/matchPath
之类的方法来匹配(vue 是收集好的,react得自己手动筛选)
//vue-router
const appRouter = createRouter(/*...*/)
appRouter.beforeEach((to) => {
console.log( to.matched ) // 能拿到所有匹配到的路由
})
//react-router-dom
//这里是自己写的路由守卫函数内部,仅仅用来演示怎么匹配
import {useLocation, matchPath} from "react-router-dom"
function GuardRoute() {
const { pathname } = useLocation()
//简单版的,直接用 写好的路由表 即可
const matched = routes.find(r => {
return matchPath(r.fullPath, pathname)
})
//复杂版的用上边制作的,扁平的 map
const matched = routeMap.find(r => {
return matchPath(r.fullPath, pathname)
})
}
路由库匹配用到个路由的本质是,把fullPath
经过排序后,在转成正则表达式,然后挨个匹配。所以我们需要递归路由表,自己把每个层级的fullPath
全路径自己拼出来,然后传给库的xx match
方法去,让库的内部给编译成正则,用它们自己的方法来匹配,如果返回 true 就代表这个是入口
复杂版的parent
字段是因为,我们拿到的入口一定是从最终的子路由开始的,所以需要用到parent
字段递归的收集所有的loader
字段,然后一并执行(如果像 vue 这样直接能取到所有,就不需要了)
这里看情况得考虑怎么做区分,比如想要做到,让父组件拿到的是父loader
的数据,子组件拿到的一定是子loader
的数据,不做区分就混了
当然,觉得不需要区分这步就可以跳过了,主要是它们可能混了可能出现数据覆盖
要考虑的细节代表性的就这么多了,小坑自己踩吧,如果不知道怎么做,就按照简单版本的尝试写写,因为简单版本的会跳过许多步骤,然后在过渡到复杂版本的
简单版之所简单是因为
- 所有层级拉平,可以很轻松的通过一次遍历收集到所有
loader
列表 - 只有一层的情况下,加上自己手动控制排序,就不需要程序来控制了(我目前想到的规则后边会给出,但不保证0BUG,缺乏大规模的验证)
- 由于层级拉平了,所以不需要
parent / children / fullPath
字段了,也不需要递归,不需要做一个单独的routeMap
- 对于
react
,请确保只在最顶层加路由守卫相关的逻辑,这样就不需要处理缓存,集成,字段是不是最新的...乱七八糟的破情况 - 不知道怎么做数据共享和传递,可以直接搞个全局变量,记得不要挂载
windows
上,在文件内创建变量然后导出去即可,我们代码打包后都是在闭包中执行,你不给外边提供访问渠道外边是拿不到的 - ssr 不支持是因为这需要框架一边的协助,因为获取到的数据需要注入到 html 中不然客户端水合会报错,这个逻辑不能单纯写到中间件里,需要写到 ssr 渲染的函数中,在最后返回 html 时动态给混进去(肯定是组件渲染完才知道都有哪些数据,所以只能在最后混入),不过也无所谓,毕竟大多数业务还是后台管理系统,这已经够用了
路由表排序规则实现原理
因为我们动态寻找进入到个路由时,肯定是通过顺序遍历查找的,所以需要做出一个只有一层的列表出来,不然就得深度递归
为了做匹配,路径必定是要被编译成正则的,有能力的可以自己编译成自己搞正则匹配,否则就老老实实用框架提供的方法来完成
这是两个类型的路由,它们编译成正则时的样子
- 静态路由
/home/about -> /^\/home\/about\/?$/i
- 动态路由
/home/:id -> /^\/home\/(?:([^\/]+?))\/?$/i
(正则可以自己粘贴到控制台试试)既然是正则匹配,我们还是顺序遍历,那么试想,如果当前进入的路由是 /home/about
会发现以上两个正则都返回 true
,所以为了确保静态路由不会被动态的吃掉,所以同级别的路由,动态必须放在静态的后边
知道谁前谁后的准则后,就可以开始写代码了
- 循环或递归循环路由表,从下标 1 开始
- 可以写一个方法,做到每次拿两个路由项中的
path
进行对比(此时还不是全路径) - 对两个
path
字符串按照下表从 0 开始对比- 找到动态的标识(一般是
:
)的放后边 - 都是静态无所谓前后
- 两者都是动态就继续匹配,因为多个动态符是允许的
- 找到动态的标识(一般是
- 最后全部结束时,一定要把
404
路由单独跳出来放在最后,不然404
路由会吃了所有
总结
我只是给出一个思路,实现上来说可以复杂可以简单,由于每个作者的想法不同,代码也不通用所以我也就没必要给出具体代码了,大家可以尝试按照这个思路写一次就心中有谱了
个人觉得这个功能其实更适合由框架来做,而不是单一的某个库,比如umijs/remixjs
之类的,react-router
的数据路由其实已经提供了这个机制了,可是我用起来并不舒服,有些复杂,没中文,它其实是remixjs
团队成员做的,所以用了还得学它的框架思想(只用 loader 很多是没必要学的),不光有理解负担,打包的代码会倍增,但如果用我的方式做出来再怎么扩展撑死都没1kb
之所以了解,是因为做了个小框架,初版用react
单独做出来感觉还行,但和权限校验,懒加载,ssr 结合起来各种 bug 就层出不穷,真正做到代码5
分钟,调试1小时
扯远了,相关的东西和心得以后都会陆陆续续公布出来,喜欢的可以求个点赞,收藏,关注么~
往期文章