第三篇,有关koa生态中比较重要的一个中间件:koa-routerjavascript
第一篇:koa源码阅读-0
第二篇:koa源码阅读-1-koa与koa-compose前端
首先,由于koa是一个管理中间件的平台,而注册一个中间件使用use
来执行。
不管是什么请求,都会将全部的中间件执行一遍(若是没有中途结束的话)
因此,这就会让开发者很困扰,若是咱们要作路由该怎么写逻辑?java
app.use(ctx => {
switch (ctx.url) {
case '/':
case '/index':
ctx.body = 'index'
break
case 'list':
ctx.body = 'list'
break
default:
ctx.body = 'not found'
}
})
复制代码
诚然,这样是一个简单的方法,可是必然不适用于大型项目,数十个接口经过一个switch
来控制未免太繁琐了。
更况且请求可能只支持get
或者post
,以及这种方式并不能很好的支持URL中包含参数的请求/info/:uid
。
在express
中是不会有这样的问题的,自身已经提供了get
、post
等之类的与METHOD
同名的函数用来注册回调:
expressnode
const express = require('express')
const app = express()
app.get('/', function (req, res) {
res.send('hi there.')
})
复制代码
可是koa
作了不少的精简,将不少逻辑都拆分出来做为独立的中间件来存在。
因此致使不少express
项目迁移为koa
时,须要额外的安装一些中间件,koa-router
应该说是最经常使用的一个。
因此在koa
中则须要额外的安装koa-router
来实现相似的路由功能:
koagit
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/', async ctx => {
ctx.body = 'hi there.'
})
app.use(router.routes())
.use(router.allowedMethods())
复制代码
看起来代码确实多了一些,毕竟将不少逻辑都从框架内部转移到了中间件中来处理。
也算是为了保持一个简练的koa框架所取舍的一些东西吧。
koa-router的逻辑确实要比koa的复杂一些,能够将koa想象为一个市场,而koa-router则是其中一个摊位
koa仅须要保证市场的稳定运行,而真正和顾客打交道的确是在里边摆摊的koa-routergithub
koa-router
的结构并非很复杂,也就分了两个文件:正则表达式
.
├── layer.js
└── router.ja
复制代码
layer
主要是针对一些信息的封装,主要路基由router
提供:express
File | Description |
---|---|
layer |
信息存储:路径、METHOD、路径对应的正则匹配、路径中的参数、路径对应的中间件 |
router |
主要逻辑:对外暴露注册路由的函数、提供处理路由的中间件,检查请求的URL并调用对应的layer中的路由处理 |
能够拿上边所抛出的基本例子来讲明koa-router
是怎样的一个执行流程:npm
const router = new Router() // 实例化一个Router对象
// 注册一个路由的监听
router.get('/', async ctx => {
ctx.body = 'hi there.'
})
app
.use(router.routes()) // 将该Router对象的中间件注册到Koa实例上,后续请求的主要处理逻辑
.use(router.allowedMethods()) // 添加针对OPTIONS的响应处理,以及一些METHOD不支持的处理
复制代码
首先,在koa-router
实例化的时候,是能够传递一个配置项参数做为初始化的配置信息的。
然而这个配置项在readme
中只是简单的被描述为:数组
Param | Type | Description |
---|---|---|
[opts] |
Object |
|
[opts.prefix] |
String |
prefix router paths(路由的前缀) |
告诉咱们能够添加一个Router
注册时的前缀,也就是说若是按照模块化分,能够没必要在每一个路径匹配的前端都添加巨长的前缀:
const Router = require('koa-router')
const router = new Router({
prefix: '/my/awesome/prefix'
})
router.get('/index', ctx => { ctx.body = 'pong!' })
// curl /my/awesome/prefix/index => pong!
复制代码
P.S. 不过要记住,若是prefix
以/
结尾,则路由的注册就能够省去前缀的/
了,否则会出现/
重复的状况
实例化Router
时的代码:
function Router(opts) {
if (!(this instanceof Router)) {
return new Router(opts)
}
this.opts = opts || {}
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
]
this.params = {}
this.stack = []
}
复制代码
可见的只有一个methods
的赋值,可是在查看了其余源码后,发现除了prefix
还有一些参数是实例化时传递进来的,可是不太清楚为何文档中没有提到:
Param | Type | Default | Description |
---|---|---|---|
sensitive |
Boolean |
false |
是否严格匹配大小写 |
strict |
Boolean |
false |
若是设置为false 则匹配路径后边的/ 是可选的 |
methods |
Array[String] |
['HEAD','OPTIONS','GET','PUT','PATCH','POST','DELETE'] |
设置路由能够支持的METHOD |
routerPath |
String | null |
若是设置了sensitive
,则会以更严格的匹配规则来监听路由,不会忽略URL中的大小写,彻底按照注册时的来匹配:
const Router = require('koa-router')
const router = new Router({
sensitive: true
})
router.get('/index', ctx => { ctx.body = 'pong!' })
// curl /index => pong!
// curl /Index => 404
复制代码
strict
与sensitive
功能相似,也是用来设置让路径的匹配变得更加严格,在默认状况下,路径结尾处的/
是可选的,若是开启该参数之后,若是在注册路由时尾部没有添加/
,则匹配的路由也必定不可以添加/
结尾:
const Router = require('koa-router')
const router = new Router({
strict: true
})
router.get('/index', ctx => { ctx.body = 'pong!' })
// curl /index => pong!
// curl /Index => pong!
// curl /index/ => 404
复制代码
methods
配置项存在的意义在于,若是咱们有一个接口须要同时支持GET
和POST
,router.get
、router.post
这样的写法必然是丑陋的。
因此咱们可能会想到使用router.all
来简化操做:
const Router = require('koa-router')
const router = new Router()
router.all('/ping', ctx => { ctx.body = 'pong!' })
// curl -X GET /index => pong!
// curl -X POST /index => pong!
复制代码
这简直是太完美了,能够很轻松的实现咱们的需求,可是若是再多实验一些其余的methods
之后,尴尬的事情就发生了:
> curl -X DELETE /index => pong!
> curl -X PUT /index => pong!
复制代码
这显然不是符合咱们预期的结果,因此,在这种状况下,基于目前koa-router
须要进行以下修改来实现咱们想要的功能:
const Koa = require('koa')
const Router = require('router')
const app = new Koa()
// 修改处1
const methods = ['GET', 'POST']
const router = new Router({
methods
})
// 修改处2
router.all('/', async (ctx, next) => {
// 理想状况下,这些判断应该交由中间件来完成
if (!~methods.indexOf(ctx.method)) {
return await next()
}
ctx.body = 'pong!'
})
复制代码
这样的两处修改,就能够实现咱们所指望的功能:
> curl -X GET /index => pong!
> curl -X POST /index => pong!
> curl -X DELETE /index => Not Implemented
> curl -X PUT /index => Not Implemented
复制代码
我我的以为这是allowedMethods
实现的一个逻辑问题,不过也许是我没有get到做者的点,allowedMethods
中比较关键的一些源码:
Router.prototype.allowedMethods = function (options) {
options = options || {}
let implemented = this.methods
return function allowedMethods(ctx, next) {
return next().then(function() {
let allowed = {}
// 若是进行了ctx.body赋值,必然不会执行后续的逻辑
// 因此就须要咱们本身在中间件中进行判断
if (!ctx.status || ctx.status === 404) {
if (!~implemented.indexOf(ctx.method)) {
if (options.throw) {
let notImplementedThrowable
if (typeof options.notImplemented === 'function') {
notImplementedThrowable = options.notImplemented() // set whatever the user returns from their function
} else {
notImplementedThrowable = new HttpError.NotImplemented()
}
throw notImplementedThrowable
} else {
ctx.status = 501
ctx.set('Allow', allowedArr.join(', '))
}
} else if (allowedArr.length) {
// ...
}
}
})
}
}
复制代码
首先,allowedMethods
是做为一个后置的中间件存在的,由于在返回的函数中先调用了next
,其次才是针对METHOD
的判断,而这样带来的一个后果就是,若是咱们在路由的回调中进行相似ctx.body = XXX
的操做,实际上会修改本次请求的status
值的,使之并不会成为404
,而没法正确的触发METHOD
检查的逻辑。
想要正确的触发METHOD
逻辑,就须要本身在路由监听中手动判断ctx.method
是否为咱们想要的,而后在跳过当前中间件的执行。
而这一判断的步骤实际上与allowedMethods
中间件中的!~implemented.indexOf(ctx.method)
逻辑彻底是重复的,不太清楚koa-router
为何会这么处理。
固然,allowedMethods
是不可以做为一个前置中间件来存在的,由于一个Koa
中可能会挂在多个Router
,Router
之间的配置可能不尽相同,不能保证全部的Router
都和当前Router
可处理的METHOD
是同样的。
因此,我的感受methods
参数的存在乎义并非很大。。
这个参数的存在。。感受会致使一些很诡异的状况。
这就要说到在注册完中间件之后的router.routes()
的操做了:
Router.prototype.routes = Router.prototype.middleware = function () {
let router = this
let dispatch = function dispatch(ctx, next) {
let path = router.opts.routerPath || ctx.routerPath || ctx.path
let matched = router.match(path, ctx.method)
// 若是匹配到则执行对应的中间件
// 执行后续操做
}
return dispatch
}
复制代码
由于咱们实际上向koa
注册的是这样的一个中间件,在每次请求发送过来时,都会执行dispatch
,而在dispatch
中判断是否命中某个router
时,则会用到这个配置项,这样的一个表达式:router.opts.routerPath || ctx.routerPath || ctx.path
,router
表明当前Router
实例,也就是说,若是咱们在实例化一个Router
的时候,若是填写了routerPath
,这会致使不管任何请求,都会优先使用routerPath
来做为路由检查:
const router = new Router({
routerPath: '/index'
})
router.all('/index', async (ctx, next) => {
ctx.body = 'pong!'
})
app.use(router.routes())
app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))
复制代码
若是有这样的代码,不管请求什么URL,都会认为是/index
来进行匹配:
> curl http://127.0.0.1:8888
pong!
> curl http://127.0.0.1:8888/index
pong!
> curl http://127.0.0.1:8888/whatever/path
pong!
复制代码
一样的,这个短路运算符一共有三个表达式,第二个的ctx
则是当前请求的上下文,也就是说,若是咱们有一个早于routes
执行的中间件,也能够进行赋值来修改路由判断所使用的URL
:
const router = new Router()
router.all('/index', async (ctx, next) => {
ctx.body = 'pong!'
})
app.use((ctx, next) => {
ctx.routerPath = '/index' // 手动改变routerPath
next()
})
app.use(router.routes())
app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))
复制代码
这样的代码也可以实现相同的效果。
实例化中传入的routerPath
让人捉摸不透,可是在中间件中改变routerPath
的这个仍是能够找到合适的场景,这个能够简单的理解为转发的一种实现,转发的过程是对客户端不可见的,在客户端看来依然访问的是最初的URL,可是在中间件中改变ctx.routerPath
能够很轻易的使路由匹配到咱们想转发的地方去
// 老版本的登陆逻辑处理
router.post('/login', ctx => {
ctx.body = 'old login logic!'
})
// 新版本的登陆处理逻辑
router.post('/login-v2', ctx => {
ctx.body = 'new login logic!'
})
app.use((ctx, next) => {
if (ctx.path === '/login') { // 匹配到旧版请求,转发到新版
ctx.routerPath = '/login-v2' // 手动改变routerPath
}
next()
})
app.use(router.routes())
复制代码
这样就实现了一个简易的转发:
> curl -X POST http://127.0.0.1:8888/login
new login logic!
复制代码
上述所有是关于实例化Router
时的一些操做,下面就来讲一下使用最多的,注册路由相关的操做,最熟悉的必然就是router.get
,router.post
这些的操做了。
但实际上这些也只是一个快捷方式罢了,在内部调用了来自Router
的register
方法:
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {}
let router = this
let stack = this.stack
// support array of paths
if (Array.isArray(path)) {
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts)
})
return this
}
// create route
let route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || '',
ignoreCaptures: opts.ignoreCaptures
})
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix)
}
// add parameter middleware
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param])
}, this)
stack.push(route)
return route
}
复制代码
该方法在注释中标为了 private 可是其中的一些参数在代码中各类地方都没有体现出来,鬼知道为何会留着那些参数,但既然存在,就须要了解他是干什么的
这个是路由监听的基础方法,函数签名大体以下:
Param | Type | Default | Description |
---|---|---|---|
path |
String /Array[String] |
- | 一个或者多个的路径 |
methods |
Array[String] |
- | 该路由须要监听哪几个METHOD |
middleware |
Function /Array[Function] |
- | 由函数组成的中间件数组,路由实际调用的回调函数 |
opts |
Object |
{} |
一些注册路由时的配置参数,上边提到的strict 、sensitive 和prefix 在这里都有体现 |
能够看到,函数大体就是实现了这样的流程:
path
是否为数组,若是是,遍历item
进行调用自身Layer
对象,设置一些初始化参数stack
中存储因此在介绍这几个参数以前,简单的描述一下Layer
的构造函数是颇有必要的:
function Layer(path, methods, middleware, opts) {
this.opts = opts || {}
this.name = this.opts.name || null
this.methods = []
this.paramNames = []
this.stack = Array.isArray(middleware) ? middleware : [middleware]
methods.forEach(function(method) {
var l = this.methods.push(method.toUpperCase());
if (this.methods[l-1] === 'GET') {
this.methods.unshift('HEAD')
}
}, this)
// ensure middleware is a function
this.stack.forEach(function(fn) {
var type = (typeof fn)
if (type !== 'function') {
throw new Error(
methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
+ "must be a function, not `" + type + "`"
)
}
}, this)
this.path = path
this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}
复制代码
layer是负责存储路由监听的信息的,每次注册路由时的URL,URL生成的正则表达式,该URL中存在的参数,以及路由对应的中间件。
通通交由Layer
来存储,重点须要关注的是实例化过程当中的那几个数组参数:
methods
存储的是该路由监听对应的有效METHOD
,并会在实例化的过程当中针对METHOD
进行大小写的转换。
paramNames
由于用的插件问题,看起来不那么清晰,实际上在pathToRegExp
内部会对paramNames
这个数组进行push
的操做,这么看可能会舒服一些pathToRegExp(path, &this.paramNames, this.opts)
,在拼接hash
结构的路径参数时会用到这个数组
stack
存储的是该路由监听对应的中间件函数,router.middleware
部分逻辑会依赖于这个数组
在函数头部的处理逻辑,主要是为了支持多路径的同时注册,若是发现第一个path
参数为数组后,则会遍历path
参数进行调用自身。
因此针对多个URL
的相同路由能够这样来处理:
router.register(['/', ['/path1', ['/path2', 'path3']]], ['GET'], ctx => {
ctx.body = 'hi there.'
})
复制代码
这样彻底是一个有效的设置:
> curl http://127.0.0.1:8888/
hi there.
> curl http://127.0.0.1:8888/path1
hi there.
> curl http://127.0.0.1:8888/path3
hi there.
复制代码
而关于methods
参数,则默认认为是一个数组,即便是只监听一个METHOD
也须要传入一个数组做为参数,若是是空数组的话,即便URL
匹配,也会直接跳过,执行下一个中间件,这个在后续的router.routes
中会提到
middleware
则是一次路由真正执行的事情了,依旧是符合koa
标准的中间件,能够有多个,按照洋葱模型的方式来执行。
这也是koa-router
中最重要的地方,可以让咱们的一些中间件只在特定的URL
时执行。
这里写入的多个中间件都是针对该URL
生效的。
P.S. 在koa-router
中,还提供了一个方法,叫作router.use
,这个会注册一个基于router
实例的中间件
opts
则是用来设置一些路由生成的配置规则的,包括以下几个可选的参数:
Param | Type | Default | Description |
---|---|---|---|
name |
String |
- | 设置该路由所对应的name ,命名router |
prefix |
String |
- | 很是鸡肋的参数,彻底没有卵用,看似会设置路由的前缀,实际上没有一点儿用 |
sensitive |
Boolean |
false |
是否严格匹配大小写,覆盖实例化Router 中的配置 |
strict |
Boolean |
false |
是否严格匹配大小写,若是设置为false 则匹配路径后边的/ 是可选的 |
end |
Boolean |
true |
路径匹配是否为完整URL的结尾 |
ignoreCaptures |
Boolean |
- | 是否忽略路由匹配正则结果中的捕获组 |
首先是name
,主要是用于这几个地方:
router.url(<name>)
、router.route(<name>)
获取到对应的router
信息name
会被塞到ctx.routerName
中router.register('/test1', ['GET'], _ => {}, {
name: 'module'
})
router.register('/test2', ['GET'], _ => {}, {
name: 'module'
})
console.log(router.url('module') === '/test1') // true
try {
router.register('/test2', ['GET'], null, {
name: 'error-module'
})
} catch (e) {
console.error(e) // Error: GET `error-module`: `middleware` must be a function, not `object`
}
复制代码
若是多个router
使用相同的命名,则经过router.url
调用返回最早注册的那一个:
// route用来获取命名路由
Router.prototype.route = function (name) {
var routes = this.stack
for (var len = routes.length, i=0; i<len; i++) {
if (routes[i].name && routes[i].name === name) {
return routes[i] // 匹配到第一个就直接返回了
}
}
return false
}
// url获取该路由对应的URL,并使用传入的参数来生成真实的URL
Router.prototype.url = function (name, params) {
var route = this.route(name)
if (route) {
var args = Array.prototype.slice.call(arguments, 1)
return route.url.apply(route, args)
}
return new Error('No route found for name: ' + name)
}
复制代码
若是在项目中,想要针对某些URL
进行跳转,使用router.url
来生成path
则是一个不错的选择:
router.register(
'/list/:id', ['GET'], ctx => {
ctx.body = `Hi ${ctx.params.id}, query: ${ctx.querystring}`
}, {
name: 'list'
}
)
router.register('/', ['GET'], ctx => {
// /list/1?name=Niko
ctx.redirect(
router.url('list', { id: 1 }, { query: { name: 'Niko' } })
)
})
// curl -L http://127.0.0.1:8888 => Hi 1, query: name=Niko
复制代码
能够看到,router.url
实际上调用的是Layer
实例的url
方法,该方法主要是用来处理生成时传入的一些参数。
源码地址:layer.js#L116
函数接收两个参数,params
和options
,由于自己Layer
实例是存储了对应的path
之类的信息,因此params
就是存储的在路径中的一些参数的替换,options
在目前的代码中,仅仅存在一个query
字段,用来拼接search
后边的数据:
const Layer = require('koa-router/lib/layer')
const layer = new Layer('/list/:id/info/:name', [], [_ => {}])
console.log(layer.url({ id: 123, name: 'Niko' }))
console.log(layer.url([123, 'Niko']))
console.log(layer.url(123, 'Niko'))
console.log(
layer.url(123, 'Niko', {
query: {
arg1: 1,
arg2: 2
}
})
)
复制代码
上述的调用方式都是有效的,在源码中有对应的处理,首先是针对多参数的判断,若是params
不是一个object
,则会认为是经过layer.url(参数, 参数, 参数, opts)
这种方式来调用的。
将其转换为layer.url([参数, 参数], opts)
形式的。
这时候的逻辑仅须要处理三种状况了:
hash
形式的参数替换这个参数替换指的是,一个URL
会经过一个第三方的库用来处理连接中的参数部分,也就是/:XXX
的这一部分,而后传入一个hash
实现相似模版替换的操做:
// 能够简单的认为是这样的操做:
let hash = { id: 123, name: 'Niko' }
'/list/:id/:name'.replace(/(?:\/:)(\w+)/g, (_, $1) => `/${hash[$1]}`)
复制代码
而后layer.url
的处理就是为了将各类参数生成相似hash
这样的结构,最终替换hash
获取完整的URL
。
上边实例化Layer
的过程当中看似是opts.prefix
的权重更高,可是紧接着在下边就有了一个判断逻辑进行调用setPrefix
从新赋值,在翻遍了整个的源码后发现,这样惟一的一个区别就在于,会有一条debug
应用的是注册router
时传入的prefix
,而其余地方都会被实例化Router
时的prefix
所覆盖。
并且若是想要路由正确的应用prefix
,则须要调用setPrefix
,由于在Layer
实例化的过程当中关于path
的存储就是来自远传入的path
参数。
而应用prefix
前缀则须要手动触发setPrefix
:
// Layer实例化的操做
function Layer(path, methods, middleware, opts) {
// 省略不相干操做
this.path = path
this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}
// 只有调用setPrefix才会应用前缀
Layer.prototype.setPrefix = function (prefix) {
if (this.path) {
this.path = prefix + this.path
this.paramNames = []
this.regexp = pathToRegExp(this.path, this.paramNames, this.opts)
}
return this
}
复制代码
这个在暴露给使用者的几个方法中都有体现,相似的get
、set
以及use
。
固然在文档中也提供了能够直接设置全部router
前缀的方法,router.prefix
: 文档中就这样简单的告诉你能够设置前缀,prefix
在内部会循环调用全部的layer.setPrefix
:
router.prefix('/things/:thing_id')
复制代码
可是在翻看了layer.setPrefix
源码后才发现这里实际上是含有一个暗坑的。
由于setPrefix
的实现是拿到prefix
参数,拼接到当前path
的头部。
这样就会带来一个问题,若是咱们屡次调用setPrefix
会致使屡次prefix
叠加,而非替换:
router.register('/index', ['GET'], ctx => {
ctx.body = 'hi there.'
})
router.prefix('/path1')
router.prefix('/path2')
// > curl http://127.0.0.1:8888/path2/path1/index
// hi there.
复制代码
prefix方法会叠加前缀,而不是覆盖前缀
这俩参数没啥好说的,就是会覆盖实例化Router
时所传递的那俩参数,效果都一致。
end
是一个颇有趣的参数,这个在koa-router
中引用的其余模块中有体现到,path-to-regexp:
if (end) {
if (!strict) route += '(?:' + delimiter + ')?'
route += endsWith === '$' ? '$' : '(?=' + endsWith + ')'
} else {
if (!strict) route += '(?:' + delimiter + '(?=' + endsWith + '))?'
if (!isEndDelimited) route += '(?=' + delimiter + '|' + endsWith + ')'
}
return new RegExp('^' + route, flags(options))
复制代码
endWith
能够简单地理解为是正则中的$
,也就是匹配的结尾。
看代码的逻辑,大体就是,若是设置了end: true
,则不管任何状况都会在最后添加$
表示匹配的结尾。
而若是end: false
,则只有在同时设置了strict: false
或者isEndDelimited: false
时才会触发。
因此咱们能够经过这两个参数来实现URL的模糊匹配:
router.register(
'/list', ['GET'], ctx => {
ctx.body = 'hi there.'
}, {
end: false,
strict: true
}
)
复制代码
也就是说上述代码最后生成的用于匹配路由的正则表达式大概是这样的:
/^\/list(?=\/|$)/i
// 能够经过下述代码获取到正则
require('path-to-regexp').tokensToRegExp('/list/', {end: false, strict: true})
复制代码
结尾的$
是可选的,这就会致使,咱们只要发送任何开头为/list
的请求都会被这个中间件所获取到。
ignoreCaptures
参数用来设置是否须要返回URL
中匹配的路径参数给中间件。
而若是设置了ignoreCaptures
之后这两个参数就会变为空对象:
router.register('/list/:id', ['GET'], ctx => {
console.log(ctx.captures, ctx.params)
// ['1'], { id: '1' }
})
// > curl /list/1
router.register('/list/:id', ['GET'], ctx => {
console.log(ctx.captures, ctx.params)
// [ ], { }
}, {
ignoreCaptures: true
})
// > curl /list/1
复制代码
这个是在中间件执行期间调用了来自layer
的两个方法获取的。
首先调用captures
获取全部的参数,若是设置了ignoreCaptures
则会致使直接返回空数组。
而后调用params
将注册路由时所生成的全部参数以及参数们实际的值传了进去,而后生成一个完整的hash
注入到ctx
对象中:
// 中间件的逻辑
ctx.captures = layer.captures(path, ctx.captures)
ctx.params = layer.params(path, ctx.captures, ctx.params)
ctx.routerName = layer.name
return next()
// 中间件的逻辑 end
// layer提供的方法
Layer.prototype.captures = function (path) {
if (this.opts.ignoreCaptures) return []
return path.match(this.regexp).slice(1)
}
Layer.prototype.params = function (path, captures, existingParams) {
var params = existingParams || {}
for (var len = captures.length, i=0; i<len; i++) {
if (this.paramNames[i]) {
var c = captures[i]
params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c
}
}
return params
}
// 所作的事情大体以下:
// [18, 'Niko'] + ['age', 'name']
// =>
// { age: 18, name: 'Niko' }
复制代码
上述是关于注册路由时的一些参数描述,能够看到在register
中实例化Layer
对象后并无直接将其放入stack
中,而是执行了这样的一个操做之后才将其推入stack
:
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param])
}, this)
stack.push(route) // 装载
复制代码
这里是用做添加针对某个URL
参数的中间件处理的,与router.param
二者关联性很强:
Router.prototype.param = function (param, middleware) {
this.params[param] = middleware
this.stack.forEach(function (route) {
route.param(param, middleware)
})
return this
}
复制代码
二者操做相似,前者用于对新增的路由监听添加全部的param
中间件,然后者用于针对现有的全部路由添加param
中间件。
由于在router.param
中有着this.params[param] = XXX
的赋值操做。
这样在后续的新增路由监听中,直接循环this.params
就能够拿到全部的中间件了。
router.param
的操做在文档中也有介绍,文档地址
大体就是能够用来作一些参数校验之类的操做,不过由于在layer.param
中有了一些特殊的处理,因此咱们没必要担忧param
的执行顺序,layer
会保证param
必定是早于依赖这个参数的中间件执行的:
router.register('/list/:id', ['GET'], (ctx, next) => {
ctx.body = `hello: ${ctx.name}`
})
router.param('id', (param, ctx, next) => {
console.log(`got id: ${param}`)
ctx.name = 'Niko'
next()
})
router.param('id', (param, ctx, next) => {
console.log('param2')
next()
})
// > curl /list/1
// got id: 1
// param2
// hello: Niko
复制代码
以及说完了上边的基础方法register
,咱们能够来看下暴露给开发者的几个router.verb
方法:
// get|put|post|patch|delete|del
// 循环注册多个METHOD的快捷方式
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
let middleware
if (typeof path === 'string' || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments, 2)
} else {
middleware = Array.prototype.slice.call(arguments, 1)
path = name
name = null
}
this.register(path, [method], middleware, {
name: name
})
return this
}
})
Router.prototype.del = Router.prototype['delete'] // 以及最后的一个别名处理,由于del并非有效的METHOD
复制代码
使人失望的是,verb
方法将大量的opts
参数都砍掉了,默认只留下了一个name
字段。
只是很简单的处理了一下命名name
路由相关的逻辑,而后进行调用register
完成操做。
以及上文中也提到的router.use
,能够用来注册一个中间件,使用use
注册中间件分为两种状况:
router
实例做为中间件传入这里是use
方法的关键代码:
Router.prototype.use = function () {
var router = this
middleware.forEach(function (m) {
if (m.router) { // 这里是经过`router.routes()`传递进来的
m.router.stack.forEach(function (nestedLayer) {
if (path) nestedLayer.setPrefix(path)
if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix) // 调用`use`的Router实例的`prefix`
router.stack.push(nestedLayer)
})
if (router.params) {
Object.keys(router.params).forEach(function (key) {
m.router.param(key, router.params[key])
})
}
} else { // 普通的中间件注册
router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath })
}
})
}
// 在routes方法有这样的一步操做
Router.prototype.routes = Router.prototype.middleware = function () {
function dispatch() {
// ...
}
dispatch.router = this // 将router实例赋值给了返回的函数
return dispatch
}
复制代码
第一种是比较常规的方式,传入一个函数,一个可选的path
,来进行注册中间件。
不过有一点要注意的是,.use('path')
这样的用法,中间件不能独立存在,必需要有一个能够与之路径相匹配的路由监听存在:
router.use('/list', ctx => {
// 若是只有这么一个中间件,不管如何也不会执行的
})
// 必需要存在相同路径的`register`回调
router.get('/list', ctx => { })
app.use(router.routes())
复制代码
缘由是这样的:
.use
和.get
都是基于.register
来实现的,可是.use
在methods
参数中传递的是一个空数组methods
,若是length !== 0
则会对当前匹配组标记一个flag
flag
,若是没有则说明该路径全部的中间件都没有设置METHOD
,则会直接跳过进入其余流程(好比allowedMethod)Router.prototype.match = function (path, method) {
var layers = this.stack
var layer
var matched = {
path: [],
pathAndMethod: [],
route: false
}
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i]
if (layer.match(path)) {
matched.path.push(layer)
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer)
// 只有在发现不为空的`methods`之后才会设置`flag`
if (layer.methods.length) matched.route = true
}
}
}
return matched
}
// 以及在`routes`中有这样的操做
Router.prototype.routes = Router.prototype.middleware = function () {
function dispatch(ctx, next) {
// 若是没有`flag`,直接跳过
if (!matched.route) return next()
}
return dispatch
}
复制代码
能够看到,若是选择了router.routes()
来方式来复用中间件,会遍历该实例的全部路由,而后设置prefix
。
并将修改完的layer
推出到当前的router
中。
那么如今就要注意了,在上边其实已经提到了,Layer
的setPrefix
是拼接的,而不是覆盖的。
而use
是会操做layer
对象的,因此这样的用法会致使以前的中间件路径也被修改。
并且若是传入use
的中间件已经注册在了koa
中就会致使相同的中间件会执行两次(若是有调用next
的话):
const middlewareRouter = new Router()
const routerPage1 = new Router({
prefix: '/page1'
})
const routerPage2 = new Router({
prefix: '/page2'
})
middlewareRouter.get('/list/:id', async (ctx, next) => {
console.log('trigger middleware')
ctx.body = `hi there.`
await next()
})
routerPage1.use(middlewareRouter.routes())
routerPage2.use(middlewareRouter.routes())
app.use(middlewareRouter.routes())
app.use(routerPage1.routes())
app.use(routerPage2.routes())
复制代码
就像上述代码,实际上会有两个问题:
/page2/page1/list/1
,由于prefix
会拼接而非覆盖next
之后,console.log
会连续输出三次,由于全部的routes
都是动态的,实际上prefix
都被修改成了/page2/page1
必定要当心使用,不要认为这样的方式能够用来实现路由的复用
以及,终于来到了最后一步,当一个请求来了之后,Router
是怎样处理的。
一个Router
实例能够抛出两个中间件注册到koa
上:
app.use(router.routes())
app.use(router.allowedMethods())
复制代码
routes
负责主要的逻辑。
allowedMethods
负责提供一个后置的METHOD
检查中间件。
allowedMethods
没什么好说的,就是根据当前请求的method
进行的一些校验,并返回一些错误信息。
而上边介绍的不少方法其实都是为了最终的routes
服务:
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this
var dispatch = function dispatch(ctx, next) {
var path = router.opts.routerPath || ctx.routerPath || ctx.path
var matched = router.match(path, ctx.method)
var layerChain, layer, i
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path)
} else {
ctx.matched = matched.path
}
ctx.router = router
if (!matched.route) return next()
var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name
}
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures)
ctx.params = layer.params(path, ctx.captures, ctx.params)
ctx.routerName = layer.name
return next()
})
return memo.concat(layer.stack)
}, [])
return compose(layerChain)(ctx, next)
};
dispatch.router = this
return dispatch
}
复制代码
首先能够看到,koa-router
同时还提供了一个别名middleware
来实现相同的功能。
以及函数的调用最终会返回一个中间件函数,这个函数才是真正被挂在到koa
上的。
koa
的中间件是纯粹的中间件,无论什么请求都会执行所包含的中间件。
因此不建议为了使用prefix
而建立多个Router
实例,这会致使在koa
上挂载多个dispatch
用来检查URL是否符合规则
进入中间件之后会进行URL的判断,就是咱们上边提到的能够用来作foraward
实现的地方。
匹配调用的是router.match
方法,虽然说看似赋值是matched.path
,而实际上在match
方法的实现中,里边所有是匹配到的Layer
实例:
Router.prototype.match = function (path, method) {
var layers = this.stack // 这个就是获取的Router实例中全部的中间件对应的layer对象
var layer
var matched = {
path: [],
pathAndMethod: [],
route: false
}
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i]
if (layer.match(path)) { // 这里就是一个简单的正则匹配
matched.path.push(layer)
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
// 将有效的中间件推入
matched.pathAndMethod.push(layer)
// 判断是否存在METHOD
if (layer.methods.length) matched.route = true
}
}
}
return matched
}
// 一个简单的正则匹配
Layer.prototype.match = function (path) {
return this.regexp.test(path)
}
复制代码
而之因此会存在说判断是否有ctx.matched
来进行处理,而不是直接对这个属性进行赋值。
这是由于上边也提到过的,一个koa
实例可能会注册多个koa-router
实例。
这就致使一个router
实例的中间件执行完毕后,后续可能还会有其余的router
实例也命中了某个URL
,可是这样会保证matched
始终是在累加的,而非每次都会覆盖。
path
与pathAndMethod
都是match
返回的两个数组,二者的区别在于path
返回的是匹配URL成功的数据,而pathAndMethod
则是匹配URL且匹配到METHOD的数据
const router1 = new Router()
const router2 = new Router()
router1.post('/', _ => {})
router1.get('/', async (ctx, next) => {
ctx.redirectBody = 'hi'
console.log(`trigger router1, matched length: ${ctx.matched.length}`)
await next()
})
router2.get('/', async (ctx, next) => {
ctx.redirectBody = 'hi'
console.log(`trigger router2, matched length: ${ctx.matched.length}`)
await next()
})
app.use(router1.routes())
app.use(router2.routes())
// > curl http://127.0.0.1:8888/
// => trigger router1, matched length: 2
// => trigger router2, matched length: 3
复制代码
关于中间件的执行,在koa-router
中也使用了koa-compose
来合并洋葱:
var matchedLayers = matched.pathAndMethod
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures)
ctx.params = layer.params(path, ctx.captures, ctx.params)
ctx.routerName = layer.name
return next()
})
return memo.concat(layer.stack)
}, [])
return compose(layerChain)(ctx, next)
复制代码
这坨代码会在全部匹配到的中间件以前添加一个ctx
属性赋值的中间件操做,也就是说reduce
的执行会让洋葱模型对应的中间件函数数量至少X2
。
layer中可能包含多个中间件,不要忘了middleware
,这就是为何会在reduce
中使用concat
而非push
由于要在每个中间件执行以前,修改ctx
为本次中间件触发时的一些信息。
包括匹配到的URL参数,以及当前中间件的name
之类的信息。
[
layer1[0], // 第一个register中对应的中间件1
layer1[1], // 第一个register中对应的中间件2
layer2[0] // 第二个register中对应的中间件1
]
// =>
[
(ctx, next) => {
ctx.params = layer1.params // 第一个register对应信息的赋值
return next()
},
layer1[0], // 第一个register中对应的中间件1
layer1[1], // 第一个register中对应的中间件2
(ctx, next) => {
ctx.params = layer2.params // 第二个register对应信息的赋值
return next()
},
layer2[0] // 第二个register中对应的中间件1
]
复制代码
在routes
最后,会调用koa-compose
来合并reduce
所生成的中间件数组,以及用到了以前在koa-compose
中提到了的第二个可选的参数,用来作洋葱执行完成后最终的回调处理。
至此,koa-router
的使命就已经完成了,实现了路由的注册,以及路由的监听处理。
在阅读koa-router
的源码过程当中感到很迷惑:
两个最简单的举证:
ctx.routerPath
来实现forward
功能,可是在文档中不会告诉你router.register(path, ['GET', 'POST'])
来快速的监听多个METHOD
,可是register
被标记为了@private
参考资料:
示例代码在仓库中的位置:learning-koa-router