在React中利用递归和迭代手撕一个useRoute和useMatched的功能

131 阅读2分钟

前言

在vue3中有现成的useRoute,可以获取当前路由的信息(包括元数据),源码通过依赖注入实现,react虽然也可以通过一些hooks获取路由信息,但是如果要获取自己定义的meta元数据就必须要手动实现了,vue3中还有一个route.matched可以获取当前路由之前的所有父辈路由信息,多用于面包屑和鉴权(如果父级别路由需要token则当前路由默认需要token,就不需要给每个路由都携带requiresAuth了)

!!!这里只是实现useRoute和useMatched的功能,但是不能命名为useRoute和useMatched,因为react会识别成hooks然后添加到hook的链表中,因为hooks链表必须以顺序运行但我们需要用到递归,所以如果作为自定义hooks会造成栈溢出无限递归

下面先利用迭代+递归实现一个useRoute获取当前路由元信息

路由表本质其实是一个树形结构,找到节点需要使用一些数据结构的知识来实现

先直接放上代码

export const getRoute = (path: string, routes: RouteObject[] = [], prefix = ''): RouteObject => {
  let result: RouteObject = {}
  //递归遍历
  for (let item of routes) {
    //迭代每次的path
    const currentPath = prefix + item.path
    if (prefix + item.path === path) {
      //如果找到了path一样,继续判断子路由有没有index,如果有index返回index的路由,否则返回当前路由
      if (item.children) {
        for (let child of item.children) if (child.index) return child
      }
      return item
    }
    //判断当前节点是否是目标节点的父辈,如果不是就跳出去,大大节省递归到底层才能找到的时间复杂度
    if (item.children && new RegExp(`^${currentPath}`).test(path)) {
      const res = getRoute(path, item.children, currentPath + '/')
      if (Object.keys(res).length) result = res
    }
  }
  return result
}

思路讲解:
1. 递归函数先要设置一个出口,在这里也就是找到目标节点(currentPath==path)就返回,这里的currentPath表示当前节点的fullpath,也就是prefix+item.path,先介绍一下为什么要用迭代:比如我有一个父路由,path:'/father',但在子节点命名path的时候不需要写完整的path,所以直接写path:'son',但子路由的fullpath其实是/father/son,但是在递归遍历的时候获取item.path只能获取到对象中的path:'son',不能根据这个判断是否是目标路由,所以必须用迭代拼接之前的路由路径,迭代的实现利用传参把当前拼接好的路径传给子函数作为子函数的prefix即可

2.递归的时候传入一个result,这就是到时候要返回的路由对象,如果找到了就赋值这个对象,然后上级函数判断对象是否为空,为空表示没找到,如果有就层层向上递交返回的result

3. 在判断是否有子节点的时候匹配一次正则,如果符合就继续往下找,比如我的目标path是/b/c/d,但是当前节点是/a很明显不是目标节点的父元素,所以无论这个节点是否有子节点都直接跳出这一层的递归,否则就直接跳出,减少不必要的递归,大大提高递归效率

下面实现一个useMatched获取当前路由之前的所有父辈路由信息

export const getMatched = (
  path: string,
  //当前层级的路由
  routes: RouteObject[] = [],
  //迭代的前缀
  prefix = '',
  //当前路由的matched
  matched: RouteObject[] = [],
  //引用类型,使得在递归时外层函数能在子函数查到数据后改变flag时也能通过这个引用类型获取已经找到这个信号量
  flag = { isFound: false }
): RouteObject[] => {
  //递归遍历
  for (let item of routes) {
    //开局先添加
    matched.push(item)
    //迭代每次的path
    const currentPath = prefix + item.path
    if (currentPath === path) {
      //如果找到了path一样,继续判断子路由有没有index,如果有index返回index的路由,否则返回当前路由
      if (item.children) {
        for (let child of item.children) {
          if (child.index) {
            flag.isFound = true
            //如果是主页(主页没有path,但也算一个节点),所以也追加这个节点到matched
            matched.push(child)
            return matched
          }
        }
      }
      //找到了,把引用类型改变,告诉外部函数我已经找到了不用继续了
      flag.isFound = true
      return matched
    }
    //判断当前节点是否是目标节点的父辈,如果不是就跳出去,大大节省递归到底层才能找到的时间复杂度
    if (item.children && new RegExp(`^${currentPath}`).test(path)) {
      getMatched(path, item.children, currentPath + '/', matched, flag)
      if (flag.isFound) return matched
    }
    //一轮过后还没有找到就删除
    matched.pop()
  }
  return matched
}

思路讲解:
1. useMatched和useRoute的基础是一样的,在useRoute的基础上实现一个记录路径,每次进入循环先push当前的节点,等到找到目标节点返回这个数组即可,如果到最里层还没有找到就回退我push的当前节点

2. 但是判断是否找到的依据和useRoute不同,useRoute通过判断result对象是否为空,useMatched需要递归一个引用类型层层传递给子函数,使得子函数和父函数可以共享一个引用类型, 如果子函数找到了则修改这个引用类型为true,父函数就能捕捉到这个信号量然后返回最终的数组

在高阶路由守卫组件中利用封装的getMatched实现权限校验

//遍历这个父辈路由的信息,如果其中一个父级需要token则默认当前路由也需要token,优化了如果一个路由需要token,但他的每个子路由也都要写requiresAuth的缺点
  if (!getMatched(pathname, rootRouter).some(item => item.meta?.requiresAuth)) return props.children

和vue中一模一样,只不过route.matched官方提供了api,react需要手动实现