首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >koa-route 源码阅读

koa-route 源码阅读

作者头像
IMWeb前端团队
发布于 2019-12-03 09:59:31
发布于 2019-12-03 09:59:31
49003
代码可运行
举报
文章被收录于专栏:IMWeb前端团队IMWeb前端团队
运行总次数:3
代码可运行

本文作者:IMWeb elvin 原文出处:IMWeb社区 未经同意,禁止转载

周末阅读完了 koa 的源码,其中的关键在于 koa-compose中间件的处理,核心代码只有二十多行,但实现了如下的洋葱模型,赋予了中间件强大的能力,网上有许多相关的文章,强烈建议大家阅读一下。

一句话介绍

今天阅读的模块是 koa-route,当前版本是 3.2.0,虽然周下载量只有 1.8 万(因为很少在生产环境中直接使用),但是该库同样是由 TJ 所写,可以帮助我们很好的理解 koa 中间件的实现与使用。

用法

在不使用中间件的情况下,需要手动通过 switch-case 语句或者 if 语句实现路由的功能:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const Koa = require('koa');
const app = new Koa();

// 通过 switch-case 手撸路由
const route = ctx => {
  switch (ctx.path) {
    case '/name':
      ctx.body = 'elvin';
      return;
    case '/date':
      ctx.body = '2018.09.12';
      return;
    default:
      // koa 抛出 404
      return;
  }
};

app.use(route);

app.listen(3000);

通过 node.js 执行上面的代码,然后在浏览器中访问 http://127.0.0.1:3000/name ,可以看到返回的内容为 elvin;访问 http://127.0.0.1:3000/date ,可以看到返回的内容为 2018.09.12;访问 http://127.0.0.1:3000/hh ,可以看到返回的内容为 Not Found。

这种原生方式十分的不方便,可以通过中间件 koa-route 进行简化:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const Koa = require('koa');
const route = require('koa-route');

const app = new Koa();

const name = ctx => ctx.body = 'elvin';
const date = ctx => ctx.body = '2018.09.11';
const echo = (ctx, param1) => ctx.body = param1;

app.use(route.get('/name', name));
app.use(route.get('/date', date));
app.use(route.get('/echo/:param1', echo));

app.listen(3000);

通过 node.js 执行上面的代码,然后在浏览器中访问 http://127.0.0.1:3000/echo/tencent ,可以看到返回的内容为 tencent ;访问 http://127.0.0.1:3000/echo/cool ,可以看到返回的内容为 cool —— 路由拥有自动解析参数的功能了!

将这两种方式进行对比,可以看出 koa-route 主要有两个优点:

  1. 将不同的路由隔离开来,新增或删除路由更方便。
  2. 拥有自动解析路由参数的功能,避免了手动解析。

源码学习

初始化

在看具体的初始化代码之前,需要先了解 Methods 这个包,它十分简单,导出的内容为 Node.js 支持的 HTTP 方法形成的数组,形如 ['get', 'post', 'delete', 'put', 'options', ...]

那正式看一下 koa-route 初始化的源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 源码 8-1
const methods = require('methods');

methods.forEach(function(method){
  module.exports[method] = create(method);
});

function create(method) {
    return function(path, fn, opts){
        // ...   
        const createRoute = function(routeFunc){
            return function (ctx, next){
                // ...
            };
        };

        return createRoute(fn);
    }
}

上面的代码主要做了一件事情:遍历 Methods 中的每一个方法 method,通过 module.exports[method] 进行了导出,且每一个导出值为 create(method) 的执行结果,即类型为函数。所以我们可以看到 koa-route 模块导出值为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const route = require('koa-route');

console.log(route);
// => {
// =>   get: [Function],
// =>   post: [Function],
// =>   delete: [Function],
// =>   ...
// => }

这里需要重点说一下 create(method) 这个函数,它函数套函数,一共有三个函数,很容易就晕掉了。

以 method 为 get 进行举例说明:

  • koa-route 模块内,module.exports.get 为 create('get') 的执行结果,即 function(path, fn, opts){ ... }
  • 在使用 koa-route 时,如 app.use(route.get('/name', name)); 中,route.get('/name', name) 的执行结果为 function (ctx, next) { ... },即 koa 中间件的标准函数参数形式。
  • 当请求来临时,koa 则会将请求送至上一步中得到的 function (ctx, next) { ... } 进行处理。

路由匹配

作为一个路由中间件,最关键的就是路由的匹配了。当设置了 app.use(route.get('/echo/:param1', echo)) 之后,对于一个形如 http://127.0.0.1:3000/echo/tencent 的请求,路由是怎么匹配的呢?相关代码如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 源码 8-2
const pathToRegexp = require('path-to-regexp');

function create(method) {
  return function(path, fn, opts){
    const re = pathToRegexp(path, opts);

    const createRoute = function(routeFunc){
      return function (ctx, next){
        // 判断请求的 method 是否匹配
        if (!matches(ctx, method)) return next();

        // path
        const m = re.exec(ctx.path);
        if (m) {
            // 路由匹配上了
            // 在这里调用响应函数
        }

        // miss
        return next();
      }
    };

    return createRoute(fn);
  }
}

上面代码的关键在于 path-to-regexp 的使用,它会将字符串 '/echo/:param1' 转化为正则表达式 /^\/echo\/((?:[^\/]+?))(?:\/(?=$))?$/i,然后再调用 re.exec 进行正则匹配,若匹配上了则调用相应的处理函数,否则调用 next() 交给下一个中间件进行处理。

初看这个正则表达式比较复杂(就没见过不复杂的正则表达式?),这里强烈推荐 regexper 这个网站,可以将正则表达式图像化,十分直观。例如 /^\/echo\/((?:[^\/]+?))(?:\/(?=$))?$/i 可以用如下图像表示:

这个生成的正则表达式 /^\/echo\/((?:[^\/]+?))(?:\/(?=$))?$/i 涉及到两个点可以扩展一下:零宽正向先行断言与非捕获性分组。

这个正则表达式其实可以简化为 /^\/echo\/([^\/]+?)\/?$/i,之所以 path-to-regexp 会存在冗余,是因为作为一个模块,需要考虑到各种情况,所以生成冗余的正则表达式也是正常的。

零宽正向先行断言

/^\/echo\/((?:[^\/]+?))(?:\/(?=$))?$/i 末尾的 (?=$) 这种形如 (?=pattern) 的用法叫做零宽正向先行断言(Zero-Length Positive Lookaherad Assertions),即代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配 pattern。这里的零宽即只匹配位置,而不占用字符。来看一下例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 匹配 'Elvin' 且后面需接 ' Peng'
const re1 = /Elvin(?= Peng)/

// 注意这里只会匹配到 'Elvin',而不是匹配 'Elvin Peng'
console.log(re1.exec('Elvin Peng'));
// => [ 'Elvin', index: 0, input: 'Elvin Peng', groups: undefined ]

// 因为 'Elvin' 后面接的是 ' Liu',所以匹配失败
console.log(re1.exec('Elvin Liu'));
// => null

与零宽正向先行断言类似的还有零宽负向先行断言(Zero-Length Negtive Lookaherad Assertions),形如 (?!pattern),代表字符串中的一个位置,紧接该位置之后的字符序列不能够匹配 pattern。来看一下例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 匹配 'Elvin' 且后面接的不能是 ' Liu'
const re2 = /Elvin(?! Liu)/

console.log(re2.exec('Elvin Peng'));
// => [ 'Elvin', index: 0, input: 'Elvin Peng', groups: undefined ]

console.log(re2.exec('Elvin Liu'));
// => null
非捕获性分组

/^\/echo\/((?:[^\/]+?))(?:\/(?=$))?$/i 中的 (?:[^\/]+?) 和 (?:\/(?=$)) 这种形如 (?:pattern) 的正则用法叫做非捕获性分组,其和形如 (pattern)捕获性分组区别在于:非捕获性分组仅作为匹配的校验,而不会作为子匹配返回。来看一下例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 捕获性分组
const r3 = /Elvin (\w+)/;
console.log(r3.exec('Elvin Peng'));
// => [ 'Elvin Peng',
// =>   'Peng',
// =>   index: 0,
// =>   input: 'Elvin Peng' ]

// 非捕获性分组
const r4 = /Elvin (?:\w+)/;
console.log(r4.exec('Elvin Peng'));
// => [ 'Elvin Peng',
// =>     index: 0,
// =>    input: 'Elvin Peng']

参数解析

路由匹配后需要对路由中的参数进行解析,在上一节的源码 8-2 中故意隐藏了这一部分,完整代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 源码 8-3
const createRoute = function(routeFunc){
    return function (ctx, next){
        // 判断请求的 method 是否匹配
        if (!matches(ctx, method)) return next();

        // path
        const m = re.exec(ctx.path);
        if (m) {
            // 此处进行参数解析
            const args = m.slice(1).map(decode);
            ctx.routePath = path;
            args.unshift(ctx);
            args.push(next);
            return Promise.resolve(routeFunc.apply(ctx, args));
        }

        // miss
        return next();
    };
};

function decode(val) {
  if (val) return decodeURIComponent(val);
}

以 re 为 /^\/echo\/((?:[^\/]+?))(?:\/(?=$))?$/i, 访问链接http://127.0.0.1:3000/echo/你好 为例,上述代码主要做了五件事情:

  1. 通过 re.exec(ctx.path) 进行路由匹配,得到 m 值为 ['/echo/%E4%BD%A0%E5%A5%BD', '%E4%BD%A0%E5%A5%BD']。这里之所以会出现 %E4%BD%A0%E5%A5%BD 是因为 URL中的中文会被浏览器自动编码:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log(encodeURIComponent('你好'));
 // => '%E4%BD%A0%E5%A5%BD'
  1. m.slice(1) 获取全部的匹配参数形成的数组 ['%E4%BD%A0%E5%A5%BD']
  2. 调用 .map(decode) 对每一个参数进行解码得到 ['你好']
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log(decodeURIComponent('%E4%BD%A0%E5%A5%BD'));
// => '你好'
  1. 对中间件函数的参数进行组装:因为 koa 中间件的函数参数一般为 (ctx, next) ,所以源码 8-3 中通过 args.unshift(ctx); args.push(next); 将参数组装为 [ctx, '你好', next],即将参数放在 ctxnext 之间
  2. 通过 return Promise.resolve(routeFunc.apply(ctx, args)); 返回一个新生成的中间件处理函数。这里通过 Promise.resolve(fn) 的方式生成了一个异步的函数

这里补充一下 encodeURIencodeURIComponent 的区别,虽然它们两者都是对链接进行编码,但还是存在一些细微的区别:

  • encodeURI 用于直接对 URI 编码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
encodeURI("http://www.example.org/a file with spaces.html")
// => 'http://www.example.org/a%20file%20with%20spaces.html'
  • encodeURIComponent 用于对 URI 中的请求参数进行编码,若对完整的 URI 进行编码则会存储问题
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
encodeURIComponent("http://www.example.org/a file with spaces.html")
// => 'http%3A%2F%2Fwww.example.org%2Fa%20file%20with%20spaces.html'
// 上面的链接不会被浏览器识别,所以不能直接对 URI 编码

const URI = `http://127.0.0.1:3000/echo/${encodeURIComponent('你好')}`
// => 'http://127.0.0.1:3000/echo/%E4%BD%A0%E5%A5%BD'

其实核心的区别在于 encodeURIComponent 会比 encodeURI 多编码 11 个字符:

关于这两者的区别也可以参考 stackoverflow - When are you supposed to use escape instead of encodeURI / encodeURIComponent?

存在的问题

koa-route 虽然是很好的源码阅读材料,但是由于它将每一个路由都化为了一个中间件函数,所以哪怕其中一个路由匹配了,请求仍然会经过其它路由中间件函数,从而造成性能损失。例如下面的代码,模拟了 1000 个路由,通过 console.log(app.middleware.length); 可以打印中间件的个数,运行 node test-1.js 后可以看到输出为 1000,即有 1000 个中间件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// test-1.js
const Koa = require('koa');
const route = require('koa-route');

const app = new Koa();

for (let i = 0; i < 1000; i++) {
  app.use(route.get(`/get${i}`, async (ctx, next) => {
    ctx.body = `middleware ${i}`
    next();
  }));
}

console.log(app.middleware.length);

app.listen(3000);

另外通过 ab -n 12000 -c 60 http://127.0.0.1:3000/get123 进行总数为 12000,并发数为 60 的压力测试的话,得到的结果如下,可以看到请求的平均用时为 27ms,而且波动较大。

同时,我们可以写一个同样功能的原路由进行对比,其只会有一个中间件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// test-2.js
const Koa = require('koa');
const route = require('koa-route');

const app = new Koa();

app.use(async (ctx, next) => {
  const path = ctx.path;
  for (let i = 0; i < 1000; i++) {
    if (path === `/get${i}`) {
      ctx.body = `middleware ${i}`;
      break;
    }
  }
  next();
})

console.log(app.middleware.length);

app.listen(3000);

通过 node test-2.js,再用 ab -n 12000 -c 60 http://127.0.0.1:3000/get123 进行总数为 12000,并发数为 60 的压力测试,可以得到如下的结果,可以看到平均用时仅为 19ms,减小了约 30%:

其实在生产环境中,一般选择使用 koa-router,不仅符合 Express 的路由风格,而且功能更强大。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-10-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Erasure-Code-擦除码-2-实现篇
本文链接: [https://blog.openacid.com/storage/ec-2/]
drdrxp
2022/04/28
7500
Erasure-Code-擦除码-2-实现篇
Erasure-Code-擦除码-3-极限篇
本文链接: [https://blog.openacid.com/storage/ec-3/]
drdrxp
2022/04/28
8220
Erasure-Code-擦除码-3-极限篇
分布式系统下的纠删码技术(一) — Erasure Code (EC)
近几个月主要参与一个分布式存储系统的纠删码部分(用于数据容错),纠删码在学术界出现比较早,现在ceph,微软的存储系统,Hadoop 3.0等都用了EC。文章会分为多篇,主要将Erasure Code,LRC, 以及相关的数学基础,作为学习总结。
全栈程序员站长
2022/11/17
3.5K0
分布式系统下的纠删码技术(一) — Erasure Code (EC)
Hadoop3.0时代,怎么能不懂EC纠删码技术?
根据云存储服务商Backblaze发布的2021年硬盘“质量报告”,现有存储硬件设备的可靠性无法完全保证,我们需要在软件层面通过一些机制来实现可靠存储。一个分布式软件的常用设计原则就是面向失效的设计。
个推
2022/05/27
1.6K0
Hadoop3.0时代,怎么能不懂EC纠删码技术?
有趣的纠删码(erasure code)
RAID 是 "Redundant Array of Independent Disk" 的缩写,中文意思是独立冗余磁盘阵列 是一种古老的磁盘冗余备份技术,也许你从未了解其中的原理,但肯定也听说过它的大名。简单地解释,就是将N台硬盘通过RAID Controller(分Hardware,Software)结合成虚拟单台大容量的硬盘使用,其特色是N台硬盘同时读取速度加快及提供容错性.
王磊-字节跳动
2021/05/30
12.4K0
RS 纠删码为什么可以提高分布式存储可靠性?| 原力计划
Erasure Code(EC),即纠删码,是一种前向错误纠正技术(Forward Error Correction,FEC,说明见后附录)。目前很多用在分布式存储来提高存储的可靠性。相比于多副本技术而言,纠删码以最小的数据冗余度获得更高的数据可靠性,但是它的编码方式比较复杂。
区块链大本营
2020/03/24
1.7K0
RS 纠删码为什么可以提高分布式存储可靠性?| 原力计划
什么是HDFS的纠删码
Fayson在前面的文章中介绍过CDH6,参考《Cloudera Enterprise 6正式发布》和《如何在Redhat7.4安装CDH6.0》。CDH6主要集成打包了Hadoop3,包括Hadoop3的一些新特性的官方支持,比如NameNode联邦,纠删码等。纠删码可以将HDFS的存储开销降低约50%,同时与三分本策略一样,还可以保证数据的可用性。本文Fayson主要介绍纠删码的工作原理。
Fayson
2018/11/16
5.6K0
应用AI芯片加速 Hadoop 3.0 纠删码的计算性能
在保证可靠性的前提下如何提高存储利用率已成为当前 DFS 应用的主要问题之一。
ethanzhang
2018/12/30
10.6K1
应用AI芯片加速 Hadoop 3.0 纠删码的计算性能
分布式存储系统纠删码技术分享
海云捷迅云课堂专题,旨在秉承开源理念,为大家提供OpenStack技术原理与实践经验,该专题文章均由海云捷迅工程师理论与实践相结合总结而成,如大家有其他想要了解的信息,可留言给我们,我们会根据问题酌情回复。
海云捷迅
2020/07/08
4.2K0
分布式存储系统纠删码技术分享
【云计算奇妙学习之旅】第六期:各级别RAID详解
在上一期的分享中,我们了解到企业级的存储是什么样子的,它由什么组成的。那么,本期分享我们该如何来使用存储,拿到一台新的存储设备,首先要什么呢?首先要做的是给存储上电开机然后做RAID,才能使用存储提供的空间。这个时候就要知道RAID是什么,我们该给存储选择配置什么样的RAID呢?
誉天小鹿
2020/05/08
8830
【云计算奇妙学习之旅】第六期:各级别RAID详解
华为分布式存储的EC冗余模式还有人不知道是什么?
作者站在ICT项目集成角度,不定期更新ICT项目集成类文章,技术方向涉及数通、安防,安全、云计算等;管理方向涉及项目管理,经验分享,IT新闻等。致力于普及时下最新的、最实在、最艳的ICT项目集成干货,阿祥志愿和您共同成长。
ICT系统集成阿祥
2025/06/08
2660
华为分布式存储的EC冗余模式还有人不知道是什么?
详解HDFS3.x新特性-纠删码
EC(纠删码)是一种编码技术,在HDFS之前,这种编码技术在廉价磁盘冗余阵列(RAID)中应用最广泛(RAID介绍:大数据预备知识-存储磁盘、磁盘冗余阵列RAID介绍),RAID通过条带化技术实现EC,条带化技术就是一种自动将 I/O 的负载均衡到多个物理磁盘上的技术,原理就是将一块连续的数据分成很多小部分并把他们分别存储到不同磁盘上去,这就能使多个进程同时访问数据的多个不同部分而不会造成磁盘冲突(当多个进程同时访问一个磁盘时,可能会出现磁盘冲突),而且在需要对这种数据进行顺序访问的时候可以获得最大程度上的 I/O 并行能力,从而获得非常好的性能。在HDFS中,把连续的数据分成很多的小部分称为条带化单元,对于原始数据单元的每个条带单元,都会计算并存储一定数量的奇偶检验单元,计算的过程称为编码,可以通过基于剩余数据和奇偶校验单元的解码计算来恢复任何条带化单元上的错误。
五分钟学大数据
2021/01/15
1.7K0
可靠分布式系统-paxos的直观解释
另外一个经常被提及的分布式算法是[raft], raft的贡献在于把一致性算法落地. 因为 [Leslie Lamport] 的理论很抽象, 要想把他的理论应用到现实中, 还需要工程师完全掌握他的理论再添加工程必要的环节才能跑起来.
drdrxp
2022/04/28
2820
可靠分布式系统-paxos的直观解释
纯干货 | 深入剖析 HDFS 3.x 新特性-纠删码
HDFS是一个高吞吐、高容错的分布式文件系统,但是HDFS在保证高容错的同时也带来了高昂的存储成本,比如有5T的数据存储在HDFS上,按照HDFS的默认3副本机制,将会占用15T的存储空间。那么有没有一种能达到和副本机制相同的容错能力但是能大幅度降低存储成本的机制呢,有,就是在HDFS 3.x 版本引入的纠删码机制。
五分钟学大数据
2021/04/01
1.9K0
计算机存储系统之磁盘阵列技术
所谓磁盘阵列,它是由多台磁盘存储器组成,是快速、大容量、且高可靠的外存子系统,现在常见的独立冗余磁盘列阵(RAID)就是一种由多块独立磁盘构成的冗余列阵,
灰小猿
2020/09/23
6590
Seagate:HAMR与MACH-2,HDD存储的革新之路
在数据爆炸的时代,存储技术的革新成为推动数字化进程的核心动力。Seagate作为全球存储领域的领军者,正通过突破性技术重新定义HDD的未来。本文聚焦其三大核心技术:HAMR热辅助磁记录(突破存储密度极限)、MACH-2双执行器架构(提升性能与能效),以及Reman Build自愈机制(优化数据恢复效率)。这些创新不仅将单碟容量推向新高度,更通过智能资源虚拟化与数据迁移策略,显著降低数据中心的总拥有成本(TCO)。在AI与云计算需求激增的背景下,Seagate的解决方案为大规模存储提供了兼具高性能、低能耗与高可靠性的路径,重新定义了HDD在混合存储架构中的核心地位。
数据存储前沿技术
2025/03/13
3020
Seagate:HAMR与MACH-2,HDD存储的革新之路
【RAID磁盘阵列服务器数据恢复】华为OceanStor Dorado存储系统RAID-TP数据丢失数据恢复案例
一:案例描述 客户向我们反馈他们的华为OceanStor Dorado存储系统RAID-TP故障,导致数据丢失,希望能够帮助他们进行数据恢复。
海境超备
2024/08/19
2840
【RAID磁盘阵列服务器数据恢复】华为OceanStor Dorado存储系统RAID-TP数据丢失数据恢复案例
【系统设计】S3 对象存储
在本文中,我们设计了一个类似于 Amazon Simple Storage Service (S3) 的对象存储服务。S3 是 Amazon Web Services (AWS) 提供的一项服务, 它通过基于 RESTful API 的接口提供对象存储。根据亚马逊的报告,到 2021 年,有超过 100 万亿个对象存储在 S3 中。
全球技术精选
2022/09/05
7.3K0
【系统设计】S3 对象存储
磁盘
磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。早期计算机使用的磁盘是软磁盘(Floppy Disk,简称软盘),如今常用的磁盘是硬磁盘(Hard disk,简称硬盘)。--摘自百度百科。
张琳兮
2021/05/24
2.2K0
磁盘
纠删码优势分析
纠删码概述 存储节点或者存储介质失效已经成为经常的事情,提高存储可靠性以及保障数据可用性已经变得非常重要,纠删码具有高存储效率和高容错能力。在体量非常大的存储中纠删码存储方式相比副本方式存在编码开销,又由于其特有的IO访问路径,其改进空间比较大 保障数据可用性的常用方法就是数据冗余,传统的数据冗余方式就是副本和纠删码方式,副本是将每个原始数据分块都镜像复制到其他设备上来保证原始数据丢失或者失效时有副本可恢复;副本方式不涉及数据变换,而纠删码会对数据进行变换和运算,得到支持数据冗余的编码数据,比如k+r(k个
用户4700054
2022/08/17
1.8K0
纠删码优势分析
相关推荐
Erasure-Code-擦除码-2-实现篇
更多 >
LV.0
这个人很懒,什么都没有留下~
目录
  • 一句话介绍
  • 用法
  • 源码学习
    • 初始化
    • 路由匹配
      • 零宽正向先行断言
      • 非捕获性分组
    • 参数解析
    • 存在的问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档