koa-router-tree 源码解析_koa-tree-router

param 与 catchAll 使用的区别就是:*的区别。*会把路由后面的所有内容赋值给参数key;但 :可以多次使用。比如:/user/:id/:no是合法的,但 /user/*id/:no是非法的,因为 *后面所有内容会赋值给参数 id

wildChild

如果孩子节点是通配符(*或者:),则该字段为 true。

3.2 路由注册

接下来我们需要看的是addRoute这个方法了,方法体比较长。其实大多的逻辑都在处理带参数的节点,真正核心的逻辑其实并不多。我把主要的逻辑都写上了注释应该还是比较容易理解的。如果看不懂其实一步步debug几次也能帮助理解。

  /\*\*
 \* 将path对应的handler添加到路由中
 \* @param {string} path
 \* @param {function[]} handle
 \*/
  addRoute(path, handle) {
    let n = this;
    //记录原始path
    let fullPath = path;
    n.priority++;
    //统计path中包含多少参数 就是判断`:`,`\*`的数量 最多255个
    let numParams = countParams(path);

    // 判断节点是否为空
    if (n.path.length > 0 || n.children.length > 0) {
      walk: while (true) {
        // Find the longest common prefix
        // This also implies that the common prefix contains no ':' or '\*'
        // since the existing key can't contain those chars.
        let i = 0;
        const max = Math.min(path.length, n.path.length); // 找到相同前缀 循环次数 是取 path 和 n.path 长度的小那个长度
        while (i < max && path[i] === n.path[i]) { //循环判断是否字符相同,相同则i++ 直到最后
          i++;
        }

        // Split edge
        //判断是否有前缀相同,如果有相同的则把目前这个节点提取出来作为子节点
        //再把相同前缀的path部分作为 父节点
        //比如n的path = romaned 现在新增路由的path = romanus 相同前缀为 roman
        //步骤为:
        //1. 提取ed 新建一个child节点 把原来n的属性都复制过去
        //2. 把原来的n的path改为相同前缀:roman 为indices添加 子节点的第一个字符:e
        if (i < n.path.length) {
          const child = new Node(
            n.path.slice(i), // 不匹配的部分作为child节点
            n.wildChild,
            STATIC,
            n.indices,
            n.children,
            n.handle,
            n.priority - 1 // 降级成子节点,priority减1
          );

          // 当前节点的子节点变成刚刚分裂的出来的节点
          n.children = [child];
          n.indices = n.path[i];
          n.path = path.slice(0, i);
          n.handle = null;
          n.wildChild = false;
        }

          //原先的节点n现在已经分成2个节点了 结构为:
          //roman 父节点
          // ed 子节点[0]
          //那么现在需要把传入的路由添加到这个父节点中
          //最终结构为
          //roman 父节点
          // ed 子节点[0]
          // us 子节点[1]
          // 其中还有一些情况需要自调用 相当于递归 举例说明:
          //roman
          // ed
          // uie
          //当判断父节点n 本来就有一个uie子节点 这时候uie和us 又有相同前缀u 这个时候需要把这个u再次提取出来作为父节点 所以需要递归调用walk
          //最终结果为 三层结构
          //roman
          // ed
          // u
          // ie
          // s
          //还有一种情况是如果是带有参数的路由 则也会再次调用walk
        // Make new node a child of this node
        if (i < path.length) {
          path = path.slice(i);

          if (n.wildChild) {// 如果是参数节点(包含:或\*)
            n = n.children[0];
            n.priority++;

            numParams--;

            // Check if the wildcard matches, // 例如:/blog/:pp 和 /blog/:ppp,需要检查更长的通配符
            if ( path.length >= n.path.length && n.path === path.slice(0, n.path.length) && (n.path.length >= path.length || path[n.path.length] === "/")) {
              continue walk;
            } else {
              // Wildcard conflict
              let pathSeg = "";
              if (n.type === CATCH\_ALL) {
                pathSeg = path;
              } else {
                pathSeg = path.split("/")[0];
              }
              const prefix = fullPath.slice(0, fullPath.indexOf(pathSeg)) + n.path;
              throw new Error(
                `'${pathSeg}' in new path '${fullPath}' conflicts with existing wildcard '${
 n.path
 }' in existing prefix '${prefix}'`
              );
            }
          }

          const c = path[0];// 首字母,用来与indices做比较

          // Slash after param
          if (n.type === PARAM && c === "/" && n.children.length === 1) {
            n = n.children[0];
            n.priority++;
            continue walk;
          }

          // Check if a child with the next path char exists
          // 判断子节点中是否有和当前path有匹配的,只需要查看子节点path的第一个字母即可,即indices
           // 比如s的子节点现在是earch和upport,indices为eu
           // 如果新来的路由为super,那么就是和upport有匹配的部分u,将继续分类现在的upport节点
          for (let j = 0; j < n.indices.length; j++) {
            if (c === n.indices[j]) {
              j = n.addPriority(j);
              n = n.children[j];
              continue walk;
            }
          }

          // Otherwise insert it
          if (c !== ":" && c !== "\*") {
             // 记录第一个字符,放在indices中
            n.indices += c;
            const child = new Node(
              "",
              false,
              STATIC
            );
            // 增加子节点
            n.children.push(child);
            n.addPriority(n.indices.length - 1);
            n = child;
          }
          n.insertChild(numParams, path, fullPath, handle);
          return;
        } else if (i === path.length) {
          // Make node a (in-path leaf)
          // 路径相同,如果已有handler就报错,没有就赋值
          if (n.handle !== null) {
            throw new Error(
              "A handle is already registered for path '" + fullPath + "'"
            );
          }
          n.handle = handle;
        }
        return;
      }
    } else {
      // 节点为空,直接添加直接添加路由,节点种类是root
      n.insertChild(numParams, path, fullPath, handle);
      n.type = ROOT;
    }
  }
  

3.3 插入子节点

insertChild函数是根据path本身进行分割, 将/分开的部分分别作为节点保存, 形成一棵树结构. 注意参数匹配中的:*的区别, 前者是匹配一个字段, 后者是匹配后面所有的路径

/\*\*
 \* 添加节点函数 主要处理包含参数节点
 \* @param {number} numParams 参数个数
 \* @param {string} path 路径
 \* @param {string} fullPath 完整路径
 \* @param {function[]} handle 处理函数
 \*/
  insertChild(numParams, path, fullPath, handle) {
    let n = this;
    let offset = 0; // Already handled chars of the path

    // 循环查找前缀为':' 或者 '\*' 通配符,只要匹配到wildcard
    for (let i = 0, max = path.length; numParams > 0; i++) {
      const c = path[i];
      if (c !== ":" && c !== "\*") {
        continue;
      }

      // 判断在\*参数之后不能再有\*或者: 否则则报错 除非到了下一个/
      let end = i + 1;
      while (end < max && path[end] !== "/") {
        if (path[end] === ":" || path[end] === "\*") {
          throw new Error(
            "only one wildcard per path segment is allowed, has: '" +
              path.slice(i) +
              "' in path '" +
              fullPath +
              "'"
          );
        } else {
          end++;
        }
      }

      // 检查这个节点是否存在子节点,如果我们在这里插入通配符,子节点将是不可访问的
      if (n.children.length > 0) {
        throw new Error(
          "wildcard route '" +
            path.slice(i, end) +
            "' conflicts with existing children in path '" +
            fullPath +
            "'"
        );
      }

      // check if the wildcard has a name
      if (end - i < 2) {
        throw new Error(
          "wildcards must be named with a non-empty name in path '" +
            fullPath +
            "'"
        );
      }
      
      // 参数类型 相当于注册路由时候带有:
      if (c === ":") {
        // Split path at the beginning of the wildcard
        if (i > 0) {
          n.path = path.slice(offset, i);
          offset = i;
        }

        const child = new Node("", false, PARAM);
        n.children = [child];
        n.wildChild = true;
        n = child;
        n.priority++;
        numParams--;
        if (end < max) {
          n.path = path.slice(offset, end);
          offset = end;

          const staticChild = new Node(
            "",
            false,
            STATIC,
            "",
            [],
            null,
            1
          );
          n.children = [staticChild];
          n = staticChild; // 下次循环这个新的child节点
        }
      } else {
         // 如果是通配符\*
        if (end !== max || numParams > 1) {
          throw new Error(
            "catch-all routes are only allowed at the end of the path in path '" +
              fullPath +
              "'"
          );
        }

        if (n.path.length > 0 && n.path[n.path.length - 1] === "/") {
          throw new Error(
            "catch-all conflicts with existing handle for the path segment root in path '" +
              fullPath +
              "'"
          );
        }

        i--;
        if (path[i] !== "/") {
          throw new Error("no / before catch-all in path '" + fullPath + "'");
        }

        n.path = path.slice(offset, i);

        // first node: catchAll node with empty path
        const catchAllChild = new Node("", true, CATCH\_ALL);
        n.children = [catchAllChild];
        n.indices = path[i];
        n = catchAllChild;
        n.priority++;

        // second node: node holding the variable
        const child = new Node(
          path.slice(i),
          false,
          CATCH\_ALL,
          "",
          [],
          handle,
          1
        );
        n.children = [child];

        return;
      }
    }

    // insert remaining path part and handle to the leaf
    // 插入路由 如果不包含参数节点 offset为0
    n.path = path.slice(offset);
    n.handle = handle;
  }

3.4 路由查找

最后,我们要看下根据path获取router的方法search,这个方法还是比较简单的,注释基本也能明白。匹配每个children的path,最长匹配

/\*\*
 \* 根据path查找路由的方法
 \* @param {string} path
 \*/
  search(path) {
    let handle = null;
    const params = [];
    let n = this;

    walk: while (true) {
      if (path.length > n.path.length) {// 尚未到达path的终点
        if (path.slice(0, n.path.length) === n.path) {// 前面一段需要一致
          path = path.slice(n.path.length);
          // If this node does not have a wildcard child,
          // we can just look up the next child node and continue
          // to walk down the tree
          // 判断如果不是参数节点
          // 那path的第一个字符 循环对比indices中的每个字符查找到子节点
          if (!n.wildChild) {
            const c = path.charCodeAt(0);
            for (let i = 0; i < n.indices.length; i++) {
              if (c === n.indices.charCodeAt(i)) {
                n = n.children[i];
                continue walk;
              }
            }

            // Nothing found.
            return { handle, params };
          }

          // Handle wildcard child
          n = n.children[0];
          switch (n.type) {
            case PARAM:
              // Find param end
              // 如果是普通':'节点, 那么找到/或者path end, 获得参数
              let end = 0;
              while (end < path.length && path.charCodeAt(end) !== 47) {
                end++;
              }

              // Save param value
              params.push({ key: n.path.slice(1), value: path.slice(0, end) });

              // We need to go deeper!
              // 如果参数还没处理完, 继续walk
              if (end < path.length) {
                if (n.children.length > 0) {
                  path = path.slice(end);
                  n = n.children[0];
                  continue walk;
                }

                // ... but we can't
                return { handle, params };
              }

              handle = n.handle;
              // 否则获得handle返回就OK
              return { handle, params };
              
            case CATCH\_ALL:  // 匹配所有参数
              params.push({ key: n.path.slice(2), value: path });

              handle = n.handle;
              return { handle, params };

            default:
              throw new Error("invalid node type");
          }
        }
      } else if (path === n.path) {
        handle = n.handle;
      }

      return { handle, params };
    }
  }

3.5 路由树的构建

再举一个路由树的例子,定义路由如下:

r.GET("/", func1 {})
r.GET("/index", func2{})
r.GET("/inter", func3 {})
r.GET("/go", func4{})
r.GET("/game/:id", func5)

得到的路由树结构图为:

4.性能提升总结

其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

《前端开发四大模块核心知识笔记》

最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值