首先区分概念:路由指的是一个过程,就是使用一些定义好的规则,让不同的URI调用不同的处理器(匿名函数或者类中的方法)这样一个过程。通常,很多框架所说的定义路由,就是将这样的规则注册到系统中。Slim的路由使用了FastRoute库。作者写了一个帖子,介绍了自己写这个库的原因(原文链接):前段时间使用正则快速路由库,遇到了Pux路由库的一些问题。它号称比现在的路由库快几个数量级,因为为了达到这个目的,这个库是通过PHP的C扩展来实现的。然而,在粗略地查看Pux的源代码之后,我强烈怀疑这个库优化了路由处理的错误部分,而我可以轻松地获得更好的性能而无需诉诸C扩展。当我查看Pux基准测试时,我的怀疑得到了进一步证实,它只测试了几个非常简单实用的单路由器示例。为了进一步研究这个问题,我编写了一个小型路由库:FastRoute,它实现了接下来描述的调度处理。为了预先说明,我发布了针对Pux库的小型基准测试的结果:1placeholder|Pux(无分机)|Pux(分机)|FastRoute----------------------------------------------------第一条路线|0.17秒|0.13秒|0.14s最后一条路线2.51s|1.20秒|0.49s未知路线|2.34秒|1.10秒|0.34s9占位符|Pux(无分机)|Pux(分机)|FastRoute----------------------------------------------------第一条路线|0.22秒|0.19秒|0.20s最后一条路线|2.65秒|1.78秒|0.59s未知路线|2.50秒|1.49秒|0.40秒该基准测试使用一百条路线并找到最快的路线(最佳示例)、最慢的路线(最差的示例)和一条未知的平均路线。通过设置变量将测试分为两部分,一部分使用1个占位符,另一部分使用9个占位符。整个测试显然循环了数百次。关于路由的一个问题为了确保我们谈论的是同一件事,让我们定义什么是“路由”。在大多数实际形式中,它指的是利用一组类似于以下的路由定义:$r->addRoute('GET','/user/{name}/{id:\d+}','handler0');$r->addRoute('GET','/user/{id:\d+}','handler1');$r->addRoute('GET','/user/{name}','handler2');然后根据他们的URI调度一个进程:$d->dispatch('GET','/user/nikic/42');//=>provides'handler0'and['name'=>'nikic','id'=>'42']为了将这个过程带到更抽象的层次,我们将为路由定义提供HTTP方法和任何特定格式。在这篇文章中,我唯一会考虑的是路由的调度阶段——路由如何被解析或者调度器生成的数据不被覆盖。那么,路由处理最耗时的地方在哪里呢?在一个混乱的、过度设计的系统中,它可能是实例化几十个对象和调用数百个方法的开销。Pux库在减少这种开销方面做得很好。然而,在一个相对原始的系统中,经过一系列数十或数百个路由表达式,然后将它们与提供的URI进行匹配的过程是最耗时的部分。使这个过程更快是本文的主题。组合正则表达式:优化这类问题最基本的方法就是避免将那些正则表达式一个一个匹配,而是将它们组合成一个大的正则表达式,这样你只需要一个匹配就够了。以上一个例子的路由为例。组合正则表达式如下:单个正则表达式:~^/user/([^/]+)/(\d+)$~~^/user/(\d+)$~~^/user/([^/]+)$~组合正则表达式:~^(?:/user/([^/]+)/(\d+)|/user/(\d+)|/user/([^/]+))$~x这种转换非常简单:基本上你只需将那些正则表达式用OR一个接一个地连接起来。在匹配这个合并后的正则表达式时,如何找出匹配的是哪条路由规则呢?为了找出答案,让我们看一下样本的preg_match函数的输出:preg_match($regex,'/user/nikic',$matches);=>["/user/nikic",#fullmatch"","",#groupsfromfirstroute(empty)",#groupsfromsecondroute(empty)"nikic",#groupsfromthirdroute(used!)]然后,在$matches数组中找到第一个非空条目是诀窍(当然不包括第一个完全匹配)。这里我贴出代码供大家测试:$regex="~^(?:/user/([^/]+)/(\d+)|/user/(\d+)|/user/([^/]+))$~x";preg_match($regex,"/user/nikic",$matches);var_dump($matches);(?:regexp)匹配模式但没有得到匹配结果,意思就是这个是一个非获取匹配不存储供以后使用。这在使用“或”字符(|)组合模式的各个部分时很有用。例如,'industr(?:y|ies)是比'industry|industries'更短的表达式。简介为了使用这个结果,你需要一个额外的数据结构来将$matches的索引映射到匹配的路由规则(或者,与该路由规则相关的一些信息)[1=>['handler0',['name','id']],3=>['handler1',['id']],4=>['handler2',['name']],]下面是一个实现整个处理流程的例子:publicfunctiondispatch($uri){if(!preg_match($this->regex,$uri,$matches)){返回[self::NOT_FOUND];}//找到第一个非空匹配(跳过完整匹配)for($i=1;''===$matches[$i];++$i);list($handler,$varNames)=$this->routeData[$i];$变量=[];foreach($varNamesas$varName){$vars[$varName]=$matches[$i++];}return[self::FOUND,$handler,$vars];}找到第一个非空索引后,就可以找到关联的数据了。可以通过遍历$matches数组并将值与变量名称配对来填充占位符变量。那么这种方法的效率如何?这是与Pux的比较结果(使用C扩展):1placeholder|Pux(分机)|GPB-NC------------------------------------第一条路线|0.13秒|0.20s最后一条路线|1.20秒|0.70秒未知路线|1.10秒|0.16s9占位符|Pux(分机)|--------------------------第一条路线|0.19秒|0.41s最后一条路线|1.78秒|4.09未知路线|1.49秒|0.30sGPB-NC表示“基于组位置,非分块”调度。如您所见,这种方法在单个占位符测试用例上提供了良好的性能。当然,它无法击败在最快路线上正确匹配的C扩展实现,但在最差的匹配上它会快一点,如果没有匹配则更快。
