
1.2.5 路由
Koa本身只提供中间件机制,所以在一个Web应用里,我们只能在app.js内不断地叠加中间件,然后在中间件内通过ctx.path来判断请求路径,处理各种业务逻辑,这样就会出现app.js过于臃肿、可读性极差的情况。为了解决这个问题,各种Web框架对会请求处理进行抽象,将请求路径(request.path)和具体的实现抽取到独立文件中形成独立的路由。
简单地讲,路由就是根据特定请求路径进行特定处理的中间件,是比较简单、初级的解耦方式。
由于Koa没有绑定任何中间件,所以我们需要使用koa-router中间件来完成对路由的支持。该中间件兼容Express路由风格,功能强大,是目前非常好的路由选型。按照express-generator的约定,我们一般将路由文件放到routes目录下,下面是router/index.js的内容示例。

在app.js里进行挂载,代码如下。

router.use的第一个参数(/)表示基准路径,它会根据具体路由文件里子路由的路径合成最终的路由。router.use的第二个参数index.routes()是router对象上挂载的所有中间件,每个中间件其实都是一个子路由。假设我们要在同一个router上挂载两个子路由,示例如下,这里子路由的路径分别是'/'和'/1'。

对于子路由代码,其中的要点如下。
○ get方法支持所有的HTTP动词,比如我们常用的GET、POST、PUT、DELETE等。
○ 第一个参数是'/1',是相对于基准路径的相对路径,比如,这里的相对路径是'/1',它的基准路径是/index,那么它最终的路径就是/index/1。
○ 第二个参数是Koa中间件,后面会详细讲解。
➘ 区分路径问题
只要是通过koa-router写的路由都可以加载,加载方式和Express里一样,示例如下。

一定要区分路径的差异。Koa路由里的路径默认是带有“/”前缀的。

如果你的路由里也加入了“/”前缀,那么访问时具体的路径中就有两个“/”前缀。这是koa-router与Express里的路由稍有不同的地方,需要注意。

➘ 路由前缀写法
一个路由文件里会定义多个路由,我们可以为多个路由定义同样的路由前缀。比如routes/users.js的代码如下。

在app.js里,app.use()方法没有指定请求路径,这和Express内置的路由是不一样的。也就是说,我们必须在路由内部自己指定请求路径,否则请求不会被处理。如果查看Koa源码里的lib/application.js文件里use方法的实现,就会明白use的参数只允许两种类型:函数中间件和Generator中间件。use方法的实现如下。

既然无法避免,那么就只能使用router.prefix('/users');来指定前缀了,但是这种琐碎的写法很烦人,有没有更好的写法呢?可以自己观察路由文件、定义、前缀写法,找出共性特征,具体内容将在后面的章节中介绍。
➘ 路由实现原理
由于Koa是一个微内核框架,它自身是不带任何中间件的,包括路由(路由最终也会转成中间件),所以Koa的路由有很多种实现方式,其中最常见的是通过Alex Mingoia编写的koa-router模块实现。该模块最好的一点是,会尽可能地保持与Express的路由用法一致,让已有的Express经验得以复用。
下面看一下koa-router的模块配置文件package.json依赖的模块。

这里的path-to-regexp模块就是路由实现的核心,它的简介是Express-style path to regexp (Express风格的路由实现),其实就是从Express里分离出来的核心路由规则匹配模块。
下面来看一下在rest里经常会用到的具名参数的写法。

正则路由的写法如下。

可以说path-to-regexp模块的功能非常强大,正则路由非常复杂,匹配效率非常高。我们可能会思考一个问题:还有没有更高效的写法呢?比如,Go语言里常用Radix Tree(基数树)实现,Fastify中的find-my-way也基于基数树实现。不得不说,目前在Node.js里这种实现还比较少。
如果想了解性能测试相关内容,可以去看看trek-router的基准测试,该基准测试对比了5种路由:route-recognizer、path-to-regexp、route-trie、routington、trek-router,它们的性能差异不大,这里就不进行比较了。
path-to-regexp模块的源码里包含了.d.ts文件,提供了对TypeScript的支持,这种方式在Node.js模块里可谓越来越流行。