如何设计并实现一个所谓"全网最快的路由数据加载 loader"的思路 —— vue/react

如何设计并实现一个所谓"全网最快的路由数据加载 loader"的思路 ------ vue/react

前言

这个概念来自于框架 remix ,所谓全网最快有些夸张,很多国内外的大框架也都是这么喊的,所以我也就这么说了~

理论上来说确实快,实现起来对于常见大多数情况来说其实并不难,应该说是比较麻烦和繁琐而已

本文会从,本着结果相同,过程随意的角度 阐述如何制作生产可用的版本,以及如何和框架集成的思路,由于最终效果需要根据实际封装而定,所以理念和设计会优先大于教学

解决的什么问题

英文好的可以看官网原文,以下是我用些大白话的解释

这东西是用于解决单页面应用,在切换路由时,导致的接口加载瀑布流的问题诞生的

什么是瀑布流呢

瀑布流是当进入页面,或者切换路由时,用到的所有组件的接口请求顺序。组件是存在上下级的,加载顺序一定是由爷爷到孙子顺序加载,上边的不加载完,底下的不出来。进而导致接口请求的顺序会像瀑布一样

loader 要做的就是,在刚进页面/路由切换时,跳过路由校验,第一时间把所有要展示的,与路由树绑定的 loader 函数给执行下。把数据收集起来,当组件被展示在页面上时,数据可能已经好了

js 复制代码
//路由表伪代码
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 的人提供的

整个流程,大致如下

  1. 进入页面
  2. 收集所有路由相关的 loader 函数给运行
  3. 将执行结果全部收集起来,放哪看作者心情
  4. 对外提供访问这些数据的手段
  5. 组件内使用该方式获取数据

实现上要考虑的问题

过程第一眼看上去并不复杂,甚至觉得好像没什么难的,但在如何处理数据上会有坑

  1. 数据竟态

路由切换时,不管旧的有没结果都要取消或者丢弃

这对于 react 挺坑的,如果涉及到权限,路由封装等其他需求,由于相关逻辑一定是放在路由拦截的地方里,所以必定涉及重复进入的问题,那么就要好好想想了

想想怎么保证多次进入时,多次离开时,不会反复执行loader ,想想在多次进入时的前几次,怎么不会把 loader 的结果误删了

  1. 缓存

如果想把路由守卫封装的强大点,那么可能不同的路由会有不同的细致策略,也就是可能会发生路由守卫嵌套的情况,这个时候是必须要做缓存的,因为祖先守卫已经把loader的逻辑执行过了,所以此时的逻辑必定是复用

正常判断是用的路由的useLocation().pathname来做唯一判断,可既然涉及到了缓存,那么就得认真考虑这个复用的值是不是旧的

  1. 对组件内的hooks要提供的数据必须得是最新的
  2. SSR 服务端渲染怎么支持
  3. react 在路由切换时是从父组件开始的,请问怎么拿到屁股后哪些才是要用到的子组件呢,如果不知道就拿不到子组件的 loader
  4. ....

这是几个比较典型的,必须要考虑要解决的问题,react是重灾区,因为人家什么都不提供,路由守卫,拦截,懒加载,...都得自己来,然后在这个基础上把loader的逻辑加上去,感觉没什么问题的代码,刚开始跑起来,要么重复跑、要么某些数据是旧的、要么死循环...,总之意外的问题一堆

你问我Vue?这些问题很多就不存在,难度大幅度降低

如何做设计,如何实现的思路

设计上其实是很灵活的,我先把通用的实现思路列出来(兼容SSR),这里列出,一个简单的,一个复杂的

简单的(client)

  1. 正常的写路由表,要静态,一定要是平铺的!!!,像一般后端管理框架搞的动态的路由后边处理起来会折磨死人
  2. 确保在写的时候,同级的静态路由要放在动态的前边,因为这两个 /动态路由/abc 路由比,一旦前者放前,后者在后续的可能处理中处理不了
  3. 做出两个如下的结构,可以递归第一步的路由表做出来
js 复制代码
//比如可以
const routes = [
  {
    path: "/",
    fullPath: "/",
  },
  {
    path: "about",
    fullPath: "/about"
  }
]
  1. 剩下的就是使用取值之类的,这个通常是放在组件的上下文中,然后内部通过上下文取。按照喜欢的方式自由发挥即可

复杂的(client + SSR)

  1. 正常的写路由表,要静态,像一般后端管理框架搞的动态的路由后边处理起来会折磨死人

  2. 对列表进行排序,因为这两个 /动态路由/abc 路由比,一旦前者放前,后者可能会进不去

  3. 数据结构要支持,要做出以下两个,这里需要递归第一步的路由表制作 -- demo看下边

    1. 一个树形的,每个路由都有属于自己的全路径的,存在父子引用字段的路由表
    2. 一个把树形列表扁平化的列表
  4. 进入路由时

    • 客户端扫描出所有用到的 loader 执行
    • 服务端从routeMap扫出来进的是哪个
  5. 剩下的就是使用取值之类的,这个通常是放在组件的上下文中,然后内部通过上下文取。按照喜欢的方式自由发挥即可

js 复制代码
//比如可以
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 之类的方法来匹配

路由库匹配用到个路由的本质是,把fullPath经过排序后,在转成正则表达式,然后挨个匹配。所以我们需要递归路由表,自己把每个层级的fullPath全路径自己拼出来,然后传给库的xx match方法去,让库的内部给编译成正则,用它们自己的方法来匹配,如果返回 true 就代表这个是入口

我们拿到的入口一定是从最终的子路由开始的,所以需要用到parent字段递归的收集所有的loader字段,然后一并执行

这里还得考虑怎么做区分,比如父组件拿到的一定是父loader的数据,子组件拿到的一定是子loader的数据,不做区分就混了,当然,觉得不需要区分这步就可以跳过了,主要是它们可能混了可能出现数据覆盖

要考虑的细节代表性的就这么多了,小坑自己踩吧,如果不知道怎么做,就看简单版本的,因为简单版本的会跳过许多步骤

  • 所有层级拉平,就不需要收集loader列表了
  • 如果是 vue,路由守卫能直接知道进到哪个子路由了。如果是react,请确保最顶层的路由,除了 404 不能出现任何动态路由,然后直接 === 就可以做到了
  • 由于层级拉平了,所以不需要parent / children / fullPath 字段了,也不需要递归,不需要做一个单独的 routeMap
  • 对于react,请确保只在最顶层加路由守卫相关的逻辑,这样就不需要处理缓存,集成,字段是不是最新的...乱七八糟的破情况
  • 不知道怎么做数据共享和传递,可以直接搞个全局变量,记得不要挂载windows上,在文件内创建变量然后导出去即可,我们代码打包后都是在闭包中执行,你不给外边提供访问渠道外边是拿不到的
  • ssr 不支持是因为这需要框架一边的协助,因为获取到的数据需要注入到 html 中不然客户端水合会报错,这个逻辑不能单纯写到中间件里,需要写到 ssr 渲染的函数中,在最后返回 html 时动态给混进去(肯定是组件渲染完才知道都有哪些数据,所以只能在最后混入),不过也无所谓,毕竟大多数业务还是后台管理系统,这已经够用了

总结

我只是给出一个思路,实现上来说可以复杂可以简单,代码并不通用所以我也就没必要给出具体代码了,大家可以尝试按照这个思路写一次就心中有谱了

个人觉得这个功能其实更适合由框架来做,而不是单一的某个库,比如umijs/remixjs之类的,react-router的数据路由其实已经提供了这个机制了,可是我用起来并不舒服,有些复杂,没中文,它其实是remixjs团队成员做的,所以用了还得学它的框架思想(只用 loader 很多是没必要学的),不光有理解负担,打包的代码会倍增 ,但如果用我的方式做出来再怎么扩展撑死都没1kb

之所以了解,是因为做了个小框架,初版用react单独做出来感觉还行,但和权限校验,懒加载,ssr 结合起来各种 bug 就层出不穷,真正做到代码5分钟,调试1小时

扯远了,相关的东西和心得以后都会陆陆续续公布出来,喜欢的可以求个点赞,收藏,关注么~

往期文章

  1. React 期许的未来(RSC)可不能并不是国内前端想要的未来
  2. 再谈前后端分离与不分离的技术利弊
  3. 如何使用 ESM 构建更加现代的 nodejs 应用(tsc + nodejs + nestjs)
相关推荐
云宏信息14 分钟前
运维效率提升实战:如何用轻量化云管平台统一纳管与自动化日常资源操作
运维·服务器·网络·架构·云计算
hour_go18 分钟前
微服务架构的故障演练数字化:方法解析与实践优势
微服务·云原生·架构
韩曙亮22 分钟前
【Web APIs】元素滚动 scroll 系列属性 ② ( 右侧固定侧边栏 )
前端·javascript·bom·window·web apis·pageyoffset
珑墨24 分钟前
【浏览器】页面加载原理详解
前端·javascript·c++·node.js·edge浏览器
LYFlied1 小时前
在AI时代,前端开发者如何构建全栈开发视野与核心竞争力
前端·人工智能·后端·ai·全栈
用户47949283569151 小时前
我只是给Typescript提个 typo PR,为什么还要签协议?
前端·后端·开源
天天进步20151 小时前
【Cradle 源码解析一】架构总览与通用计算机控制 (GCC) 的实现思路
架构
Surpass余sheng军1 小时前
AI 时代下的网关技术选型
人工智能·经验分享·分布式·后端·学习·架构
程序员爱钓鱼1 小时前
Next.js SSR 项目生产部署全攻略
前端·next.js·trae
程序员爱钓鱼1 小时前
使用Git 实现Hugo热更新部署方案(零停机、自动上线)
前端·next.js·trae