如何设计并实现一个所谓"全网最快的路由数据加载 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)
相关推荐
昱禹10 分钟前
关于CSS Grid布局
前端·javascript·css
啊QQQQQ17 分钟前
HTML:相关概念以及标签
前端·html
就叫飞六吧1 小时前
vue2和vue3全面对比
前端·javascript·vue.js
Justinc.1 小时前
CSS基础-盒子模型(三)
前端·css
敲代码不忘补水1 小时前
Docker 启动 PostgreSQL 主从架构:实现数据同步的高效部署指南
docker·postgresql·架构·数据库架构
qq_2518364571 小时前
基于ssm vue uniapp实现的爱心小屋公益机构智慧管理系统
前端·vue.js·uni-app
._Ha!n.1 小时前
Vue基础(二)
前端·javascript·vue.js
tekin2 小时前
x86 架构下一些常用的汇编指令英文全称与功能简述
汇编·架构·汇编指令·汇编语言指令·汇编指令英文全称
风清扬_jd2 小时前
Chromium 硬件加速开关c++
java·前端·c++
谢尔登3 小时前
【React】事件机制
前端·javascript·react.js