中间件形式,koa源码阅读

By admin in 4858美高梅 on 2019年4月4日

接上次挖的坑,对koa2.x有关的源码举办辨析 第一篇。
只得说,koa是三个很轻量、很优雅的http框架,尤其是在二.x过后移除了co的引入,使其代码变得进一步显然。

koa源码阅读[2]-koa-router

其3篇,有关koa生态中比较首要的3当中间件:koa-router

第一篇:koa源码阅读-0
第二篇:koa源码阅读-1-koa与koa-compose

koa源码阅读[0]

Node.js也是写了两三年的日子了,刚初始攻读Node的时候,hello world不怕创制二个HttpServer,后来在工作中也是经历过ExpressKoa1.xKoa2.x以及新近还在探讨的构成着TypeScriptrouting-controllers(驱动依旧是ExpressKoa)。
用的可比多的或然Koa本子,也是对它的洋葱模型比较感兴趣,所以近期抽出时间来读书其源码,正好近期可能会对二个Express品种实行重构,将其重构为koa2.x本子的,所以,阅读其源码对于重构也是1种有效的救助。

中间件在 Node.js
中被广大应用,它泛指壹种特定的设计形式、一名目繁多的处理单元、过滤器和处理程序,以函数的款式存在,连接在1起,形成1个异步队列,来成功对任何数据的预处理和后处理。

expresskoa同为一群人展开开发,与express相比,koa来得十分小巧。
因为express是一个大而全的http框架,内置了接近router等等的中间件实行拍卖。
而在koa中,则将接近意义的中间件全体摘了出去,早期koa其间是停放了koa-compose的,而前天也是将其分了出来。
koa只保留1个概括的中间件的构成,http恳请的处理,作为一个效用性的中间件框架来存在,本身仅有少量的逻辑。
koa-compose则是当做整合中间件最为根本的一个工具、洋葱模型的切实落到实处,所以要将五头放在壹起来看。

koa-router是什么

第叁,因为koa是三个管制中间件的平台,而注册贰在那之中间件使用use来执行。
不论什么请求,都会将持有的中间件执行一回(假如未有中途甘休的话)
就此,那就会让开发者很麻烦,假设我们要做路由该怎么写逻辑?

app.use(ctx => {
  switch (ctx.url) {
    case '/':
    case '/index':
      ctx.body = 'index'
      break
    case 'list':
      ctx.body = 'list'
      break
    default:
      ctx.body = 'not found'
  }
})

 

真正,那样是2个简短的主意,不过一定不适用于大型项目,数十二个接口通过三个switch来决定未免太繁琐了。
再说请求只怕只补助get或者post,以及那种方法并无法很好的援助URAV四L中带有参数的乞求/info/:uid
express中是不会有那般的题目标,本人已经提供了getpost等等等的与METHOD同名的函数用来注册回调:
express

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('hi there.')
})

 

但是koa做了广大的洗练,将广大逻辑都拆分出来作为独立的中间件来存在。
从而造成恒河沙数express品种搬迁为koa时,必要至极的装置一些中间件,koa-router应当就是最常用的3个。
所以在koa中则必要额外的设置koa-router来完成类似的路由功能:
koa

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())

 

看起来代码确实多了1些,毕竟将过多逻辑都从框架之中间转播移到了中间件中来拍卖。
也总算为了维持三个简易的koa框架所取舍的有的事物吧。
koa-router的逻辑确实要比koa的纷纷一些,能够将koa想象为三个市场,而koa-router则是在那之中2个摊点
koa仅要求确认保证集镇的平稳运行,而真的和消费者打交道真的是在其间摆摊的koa-router

Koa是怎么来的

率先须求分明,Koa是何许。
别的三个框架的面世都以为了缓解难题,而Koa则是为着更方便的构建http服务而产出的。
能够归纳的领悟为一个HTTP服务的中间件框架。

它的独到之处在于 灵活性
:使用中间件大家用极少的操作就能收获一个插件,用最简单易行的情势就能将新的过滤器和处理程序增添到现有的种类上。

koa基本构造

.
├── application.js
├── request.js
├── response.js
└── context.js

 

关于koa全套框架的兑现,也只是简单的拆分为了多个文本。

就象在上一篇笔记中效仿的那样,创设了叁个对象用来注册中间件,监听http劳务,这么些正是application.js在做的作业。
而框架的意思呢,正是在框架内,我们要遵守框架的本分来做政工,同样的,框架也会提须要我们有的更易用的不二等秘书籍来让大家做到必要。
针对http.createServer回调的五个参数requestresponse实行的一回封装,简化壹些常用的操作。
诸如大家对Header的1些操作,在原生http模块中也许要这么写:

// 获取Content-Type
request.getHeader('Content-Type')

// 设置Content-Type
response.setHeader('Content-Type', 'application/json')
response.setHeader('Content-Length', '18')
// 或者,忽略前边的statusCode,设置多个Header
response.writeHead(200, {
  'Content-Type': 'application/json',
  'Content-Length': '18'
})

 

而在koa中得以那样处理:

// 获取Content-Type
context.request.get('Content-Type')

// 设置Content-Type
context.response.set({
  'Content-Type': 'application/json',
  'Content-Length': '18'
})

 

简化了有些针对性requestresponse的操作,将那一个封装在了request.jsresponse.js文件中。
但与此同时那会拉动3个选拔上的麻烦,那样封装未来实际取得大概设置header变得层级更深,须求通过context找到requestresponse,然后才能进行操作。
所以,koa使用了node-delegates来更为简化那么些步骤,将request.getresponse.set统统代理到context上。
也等于说,代理后的操作是那样子的:

context.get('Content-Type')

// 设置Content-Type
context.set({
  'Content-Type': 'application/json',
  'Content-Length': '18'
})

 

那样就变得很清楚了,获取Header,设置Header再也不会担心写成request.setHeader,一挥而就,通过context.js来整合request.jsresponse.js的行为。
同时context.js也会提供部分别样的工具函数,例如Cookie等等的操作。

application引入contextcontext中又结合了requestresponse的职能,多少个文本的功效早已很清楚了:

file desc
applicaiton 中间件的管理、http.createServer的回调处理,生成Context作为本次请求的参数,并调用中间件
request 针对http.createServer -> request功能上的封装
response 针对http.createServer -> response功能上的封装
context 整合requestresponse的部分功能,并提供一些额外的功能

而在代码结构上,唯有application对外的koa是选取的Class的艺术,别的七个公文均是抛出多少个常见的Object

koa-router的大约结构

koa-router的结构并不是很复杂,也就分了多少个公文:

.
├── layer.js
└── router.ja

 

layer主假如针对性部分信息的包装,首要路基由router提供:

tag desc
layer 信息存储:路径、METHOD、路径对应的正则匹配、路径中的参数、路径对应的中间件
router 主要逻辑:对外暴露注册路由的函数、提供处理路由的中间件,检查请求的URL并调用对应的layer中的路由处理

采用http模块创立http服务

相信大家在攻读Node时,应该都写过类似那样的代码:

const http = require('http')

const serverHandler = (request, response) => {
  response.end('Hello World') // 返回数据
}

http
  .createServer(serverHandler)
  .listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))

 

2个最简便的以身作则,脚本运转后走访http://127.0.0.1:8888即可知到三个Hello World的字符串。
只是那无非是一个简便的示范,因为我们无论访问什么地点(甚至修改请求的Method),都一连会收获到这些字符串:

> curl http://127.0.0.1:8888
> curl http://127.0.0.1:8888/sub
> curl -X POST http://127.0.0.1:8888

 

从而我们大概会在回调中添加逻辑,依照路径、Method来回到给用户对应的数码:

const serverHandler = (request, response) => {
  // default
  let responseData = '404'

  if (request.url === '/') {
    if (request.method === 'GET') {
      responseData = 'Hello World'
    } else if (request.method === 'POST') {
      responseData = 'Hello World With POST'
    }
  } else if (request.url === '/sub') {
    responseData = 'sub page'
  }

  response.end(responseData) // 返回数据
}

 

例行中间件方式

拿三个完好无损的流水生产线来解释

中间件形式,koa源码阅读。koa-router的运作流程

能够拿上面所抛出的中央例子来证实koa-router是何等的二个执行流程:

const router = new Router() // 实例化一个Router对象

// 注册一个路由的监听
router.get('/', async ctx => {
  ctx.body = 'hi there.'
})

app
  .use(router.routes()) // 将该Router对象的中间件注册到Koa实例上,后续请求的主要处理逻辑
  .use(router.allowedMethods()) // 添加针对OPTIONS的响应处理,一些预检请求会先触发 OPTIONS 然后才是真正的请求

 

类似Express的实现

不过尔尔的写法还会带动另一个题材,即便是贰个十分的大的花色,存在N多的接口。
1旦都写在那八个handler其间去,未免太过难以保证。
示范只是简短的指向三个变量进行赋值,可是实际的花色不会有如此简单的逻辑存在的。
因而,我们本着handler进展1次抽象,让我们可以方便的军管路径:

class App {
  constructor() {
    this.handlers = {}

    this.get = this.route.bind(this, 'GET')
    this.post = this.route.bind(this, 'POST')
  }

  route(method, path, handler) {
    let pathInfo = (this.handlers[path] = this.handlers[path] || {})

    // register handler
    pathInfo[method] = handler
  }

  callback() {
    return (request, response) => {
      let { url: path, method } = request

      this.handlers[path] && this.handlers[path][method]
        ? this.handlers[path][method](request, response)
        : response.end('404')
    }
  }
}

 

接下来经超过实际例化一个Router对象进行挂号对应的路子,最后运行服务:

const app = new App()

app.get('/', function (request, response) {
  response.end('Hello World')
})

app.post('/', function (request, response) {
  response.end('Hello World With POST')
})

app.get('/sub', function (request, response) {
  response.end('sub page')
})

http
  .createServer(app.callback())
  .listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))

 

中间件形式中,最基础的组成都部队分正是 中间件管理器
,大家得以用它来公司和举行中间件的函数,如图所示:

创办服务

首先,大家供给创造一个http服务,在koa2.x中开创服务与koa1.x有点有些分化,须要利用实例化的办法来进展创办:

const app = new Koa()

 

而在实例化的长河中,其实koa只做了不难的作业,创造了多少个实例属性。
将引入的contextrequest以及response通过Object.create拷贝的方法放置实例中。

this.middleware = [] // 最关键的一个实例属性

// 用于在收到请求后创建上下文使用
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)

 

在实例化完结后,我们就要举办登记中间件来促成大家的事务逻辑了,上面也波及了,koa仅作为二个中间件的结缘以及呼吁的监听。
就此不会像express那样提供router.getrouter.post等等的操作,仅仅存在三个相比较接近http.createServeruse()
接下去的手续正是注册中间件并监听3个端口号运转服务:

const port = 8000

app.use(async (ctx, next) => {
  console.time('request')
  await next()
  console.timeEnd('request')
})
app.use(async (ctx, next) => {
  await next()
  ctx.body = ctx.body.toUpperCase()
})

app.use(ctx => {
  ctx.body = 'Hello World'
})

app.use(ctx => {
  console.log('never output')
})

app.listen(port, () => console.log(`Server run as http://127.0.0.1:${port}`))

 

在翻看application.js的源码时,能够看看,揭露给外部的方式,常用的大多便是uselisten
三个用来加载中间件,另两个用来监听端口并运行服务。

而这八个函数实际上并未过多的逻辑,在use中仅仅是判断了流传的参数是还是不是为一个function,以及在贰.x本子针对Generator函数的某个越发处理,将其更换为了Promise花样的函数,并将其push到构造函数中开创的middleware数组中。
本条是从1.x过渡到2.x的三个工具,在3.x本子将平昔移除Generator的支持。
其实在koa-convert其间也是援引了cokoa-compose来展开转账,所以也就不再赘述。

而在listen中做的作业就更不难了,只是简单的调用http.createServer来成立服务,并监听对应的端口之类的操作。
有3个细节在于,createServer中流传的是koa实例的另一个措施调用后的重临值callback,这几个艺术才是实在的回调解和处理理,listen只是http模块的2个火速格局。
以此是为着局地用socket.iohttps要么部分其余的http模块来进展应用的。
也就表示,只借使可以提供与http模块1致的行为,koa都足以很便利的接入。

listen(...args) {
  debug('listen')
  const server = http.createServer(this.callback())
  return server.listen(...args)
}

 

开创实例时的1对事情

首先,在koa-router实例化的时候,是足以传递一个布局项参数作为开头化的布署新闻的。
可是这一个布局项在readme中只是简单的被描述为:

Param Type Description
[opts] Object  
[opts.prefix] String prefix router paths(路由的前缀)

报告我们得以增加1个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  

Express中的中间件

这么,就贯彻了贰个代码比较清洁的HttpServer,但功能上仍然是很简陋的。
若是我们未来有一个须要,要在有的请求的先头添加一些参数的生成,比如二个呼吁的唯1ID。
将代码重复编写在我们的handler中势必是不可取的。
因而我们要针对route的拍卖进展优化,使其援救传入七个handler4858美高梅,:

route(method, path, ...handler) {
  let pathInfo = (this.handlers[path] = this.handlers[path] || {})

  // register handler
  pathInfo[method] = handler
}

callback() {
  return (request, response) => {
    let { url: path, method } = request

    let handlers = this.handlers[path] && this.handlers[path][method]

    if (handlers) {
      let context = {}
      function next(handlers, index = 0) {
        handlers[index] &&
          handlers[index].call(context, request, response, () =>
            next(handlers, index + 1)
          )
      }

      next(handlers)
    } else {
      response.end('404')
    }
  }
}

 

下一场针对上面的门径监听添加别的的handler:

function generatorId(request, response, next) {
  this.id = 123
  next()
}

app.get('/', generatorId, function(request, response) {
  response.end(`Hello World ${this.id}`)
})

 

那样在走访接口时,就足以见到Hello World 123的字样了。
其一就足以总结的觉得是在Express中贯彻的 中间件
中间件是ExpressKoa的骨干所在,壹切依靠都因其中间件来拓展加载。

4858美高梅 1

应用koa-compose合并中间件

故此大家就来探望callback的实现:

callback() {
  const fn = compose(this.middleware)

  if (!this.listenerCount('error')) this.on('error', this.onerror)

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res)
    return this.handleRequest(ctx, fn)
  }

  return handleRequest
}

 

在函数内部的率先步,正是要处理中间件,将一个数组中的中间件转换为大家想要的洋葱模型格式的。
此处就用到了相比较基本的koa-compose

实质上它的意义上与co好像,只然则把co处理Generator函数那有个别逻辑全部去掉了,本人co的代码也正是壹两百行,所以精简后的koa-compose代码仅有4八行。

大家知道,async函数实际上剥开它的语法糖今后是长那些样子的:

async function func () {
  return 123
}

// ==>

function func () {
  return Promise.resolve(123)
}
// or
function func () {
  return new Promise(resolve => resolve(123))
}

 

据此拿上述use的代码举例,实际上koa-compose获得的是那样的参数:

[
  function (ctx, next) {
    return new Promise(resolve => {
      console.time('request')
      next().then(() => {
        console.timeEnd('request')
        resolve()
      })
    })
  },
  function (ctx, next) {
    return new Promise(resolve => {
      next().then(() => {
        ctx.body = ctx.body.toUpperCase()
        resolve()
      })
    })
  },
  function (ctx, next) {
    return new Promise(resolve => {
      ctx.body = 'Hello World'
      resolve()
    })
  },
  function (ctx, next) {
    return new Promise(resolve => {
      console.log('never output')
      resolve()
    })
  }
]

 

就像在第6个函数中输出表示的那样,第两在那之中间件不会被执行,因为第肆在那之中间件并不曾调用next,所以达成类似那样的二个洋葱模型是很有意思的1件事情。
先是抛开不变的ctx不谈,洋葱模型的贯彻核心在于next的处理。
因为next是您进来下壹层中间件的钥匙,唯有手动触发现在才会进去下一层中间件。
接下来大家还亟需确定保证next要在中间件执行完成后开始展览resolve,重返到上1层中间件:

return function (context, next) {
  // last called middleware #
  let index = -1
  return dispatch(0)
  function dispatch (i) {
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i
    let fn = middleware[i]
    if (i === middleware.length) fn = next
    if (!fn) return Promise.resolve()
    try {
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
    } catch (err) {
      return Promise.reject(err)
    }
  }
}

 

故而肯定了那两点今后,上面的代码就会变得很清楚:

  1. next用来进入下一个中间件
  2. next在当前中间件执行到位后会触发回调通知上6当中间件,而成功的前提是内部的中间件已经履行到位(resolved)

能够看看在调用koa-compose此后实际会再次来到四个自推行函数。
在执行函数的始发部分,判断当前中间件的下标来防止在三当中间件中反复调用next
因为只要反复调用next,就会招致下3个中间件的往往实施,那样就磨损了洋葱模型。

帮助就是compose事实上提供了1个在洋葱模型全部实施达成后的回调,三个可选的参数,实际上功能与调用compose后边的then处理未有太大分别。

以及上边提到的,next是跻身下一当中间件的钥匙,能够在那叁个柯里化函数的应用上看出来:

Promise.resolve(fn(context, dispatch.bind(null, i + 1)))

 

将我绑定了index参数后传出此次中间件,作为调用函数的第3个参数,也正是next,效果就如调用了dispatch(1),那样便是2个洋葱模型的落到实处。
fn的调用假如是八个async function,那么外层的Promise.resolve会等到内部的async执行resolve尔后才会接触resolve,例如这样:

Promise.resolve(new Promise(resolve => setTimeout(resolve, 500))).then(console.log) // 500ms以后才会触发 console.log

 

P.S.
一个从koa1.x切换到koa2.x的暗坑,co会对数组进行出格处理,使用Promise.all展开包装,但是koa2.x未有这么的操作。
故此倘使在中间件中要针对性三个数组进行异步操作,一定要手动添加Promise.all,也许说等草案中的await*

// koa1.x
yield [Promise.resolve(1), Promise.resolve(2)]              // [1, 2]

// koa2.x
await [Promise.resolve(1), Promise.resolve(2)]              // [<Promise>, <Promise>]

// ==>
await Promise.all([Promise.resolve(1), Promise.resolve(2)]) // [1, 2]
await* [Promise.resolve(1), Promise.resolve(2)]             // [1, 2]

 

sensitive

壹旦设置了sensitive,则会以更严厉的匹配规则来监听路由,不会忽略U路虎极光L中的大小写,完全依据注册时的来同盟:

const Router = require('koa-router')
const router = new Router({
  sensitive: true
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /index => pong!
// curl /Index => 404

 

更灵活的中间件方案-洋葱模型

上述方案的确能够令人很便利的施用部分中间件,在工艺流程序控制制中调用next()来进入下1个环节,整个流程变得很清晰。
可是仍然存在壹些局限性。
比如说若是大家供给展开一些接口的耗时计算,在Express有这么三种能够实现的方案:

function beforeRequest(request, response, next) {
  this.requestTime = new Date().valueOf()

  next()
}

// 方案1. 修改原handler处理逻辑,进行耗时的统计,然后end发送数据
app.get('/a', beforeRequest, function(request, response) {
  // 请求耗时的统计
  console.log(
    `${request.url} duration: ${new Date().valueOf() - this.requestTime}`
  )

  response.end('XXX')
})

// 方案2. 将输出数据的逻辑挪到一个后置的中间件中
function afterRequest(request, response, next) {
  // 请求耗时的统计
  console.log(
    `${request.url} duration: ${new Date().valueOf() - this.requestTime}`
  )

  response.end(this.body)
}

app.get(
  '/b',
  beforeRequest,
  function(request, response, next) {
    this.body = 'XXX'

    next() // 记得调用,不然中间件在这里就终止了
  },
  afterRequest
)

 

无论是哪1种方案,对于本来代码都是一种破坏性的修改,这是不可取的。
因为Express采用了response.end()的章程来向接口请求方再次回到数据,调用后即会终止后续代码的实施。
并且因为当风尚无七个很好的方案去等待有个别中间件中的异步函数的履行。

function a(_, _, next) {
  console.log('before a')
  let results = next()
  console.log('after a')
}

function b(_, _, next) {
  console.log('before b')
  setTimeout(_ => {
    this.body = 123456
    next()
  }, 1000)
}

function c(_, response) {
  console.log('before c')
  response.end(this.body)
}

app.get('/', a, b, c)

 

就像是上述的示范,实际上log的输出顺序为:

before a
before b
after a
before c

 

那明明不切合大家的预料,所以在Express中获取next()的重回值是尚未意义的。

就此就有了Koa拉动的洋葱模型,在Koa1.x并发的日子,正好赶上了Node支持了新的语法,Generator函数及Promise的定义。
据此才有了co诸如此类令人惊讶的库,而当大家的中间件使用了Promise之后,前二当中间件就可以很随意的在此起彼伏代码执行完结后再处理本人的事体。
但是,Generator自个儿的效能并不是用来支持我们更自在的应用Promise来做异步流程的操纵。
就此,随着Node柒.6版本的发出,协助了asyncawait语法,社区也生产了Koa2.x,使用async语法替换从前的co+Generator

Koa也将co从信赖中移除(二.x版本接纳koa-convert将Generator函数转换为promise,在3.x本子上将直接不辅助Generator
ref: remove generator
supports

出于在效益、使用上Koa的三个本子之间并未有怎么不相同,最多正是1对语法的调动,所以会一向跳过一些Koa1.x有关的东西,直奔大旨。

Koa中,能够运用如下的方法来定义中间件并行使:

async function log(ctx, next) {
  let requestTime = new Date().valueOf()
  await next()

  console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
}

router.get('/', log, ctx => {
  // do something...
})

 

因为一些语法糖的存在,遮盖了代码实际运行的长河,所以,大家选取Promise来还原一下上述代码:

function log() {
  return new Promise((resolve, reject) => {
    let requestTime = new Date().valueOf()
    next().then(_ => {
      console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
    }).then(resolve)
  })
}

 

大致代码是这么的,也正是说,调用next会给咱们重返一个Promise对象,而Promise何时会resolve就是Koa在那之中做的拍卖。
能够简单的完毕一下(关于上面实现的App类,仅仅需求修改callback即可):

callback() {
  return (request, response) => {
    let { url: path, method } = request

    let handlers = this.handlers[path] && this.handlers[path][method]

    if (handlers) {
      let context = { url: request.url }
      function next(handlers, index = 0) {
        return new Promise((resolve, reject) => {
          if (!handlers[index]) return resolve()

          handlers[index](context, () => next(handlers, index + 1)).then(
            resolve,
            reject
          )
        })
      }

      next(handlers).then(_ => {
        // 结束请求
        response.end(context.body || '404')
      })
    } else {
      response.end('404')
    }
  }
}

 

老是调用中间件时就监听then,并将眼下Promiseresolvereject处理传入Promise的回调中。
也正是说,唯有当首此中间件的resolve被调用时,第2在那之中间件的then回调才会实施。
那样就兑现了二个洋葱模型。

就像大家的log中间件执行的流水生产线:

  1. 取妥当前的时光戳requestTime
  2. 调用next()实践后续的中间件,并监听其回调
  3. 其次当中间件里边大概会调用第多个、第拾个、第5个,但那都不是log所关心的,log只关怀第一个中间件曾几何时resolve,而第3当中间件的resolve则凭借他前面包车型客车中间件的resolve
  4. 等到第2其中间件resolve,那就表示后续未有别的的中间件在推行了(全都resolve了),此时log才会持续持续代码的实施

之所以就像是洋葱一样1层一层的卷入,最外层是最大的,是首先执行的,也是最终执行的。(在一个完整的请求中,next事先先河执行,next自此最后执行)。
4858美高梅 2

要贯彻中间件形式,最根本的兑现细节是:

吸收请求,处理重回值

经过上边的代码,二个koa劳动已经算是运维起来了,接下去正是访问看功能了。
在接到到三个请求后,koa会拿在此以前提到的contextrequestresponse来成立此番请求所采取的上下文。
koa1.x中,上下文是绑定在this上的,而在koa2.x是当做第二个参数字传送入进来的。
民用猜度或许是因为Generator无法采用箭头函数,而async函数能够动用箭头函数导致的啊:) 纯属个人YY

简单来讲,大家通过上边提到的几个模块成立了3个呼吁所需的上下文,基本上是一通儿赋值,代码就不贴了,未有太多逻辑,正是有八个小细节相比有意思:

request.response = response
response.request = request

 

让两者之间产生了贰个引用关系,既可以通过request获取到response,也能够经过response获取到request
还要那是二个递归的引用,类似那样的操作:

let obj = {}

obj.obj = obj

obj.obj.obj.obj === obj // true

 

同时如上文提到的,在context创办的进度中,将一大批判的requestresponse的属性、方法代理到了自作者,有趣味的能够友善翻看源码(看着有点晕):koa.js
|
context.js
这个delegate的兑现也终归相比不难,通过取出原始的品质,然后存三个引用,在自小编的性质被触发时调用对应的引用,类似三个民间版的Proxy呢,期待后续能够运用Proxy代替它。

然后大家会将生成好的context作为参数传入koa-compose变化的洋葱中去。
因为随便何种境况,洋葱肯定会回到结果的(出错与否),所以大家还亟需在最终有三个finished的处理,做1些接近将ctx.body更换为多少进行输出之类的操作。

koa运用了多量的getset访问器来落到实处际效果益,例如最常用的ctx.body = 'XXX',它是来自responseset body
那应该是requestresponse中逻辑最复杂的三个主意了。
其间要拍卖很多东西,例如在body剧情为空时协理您改改请求的status code为20四,并移除无用的headers
以及一旦未有手动钦定status code,会暗中同意钦定为200
居然还会依照近日传到的参数来判断content-type应该是html大概普通的text

// string
if ('string' == typeof val) {
  if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
  this.length = Buffer.byteLength(val)
  return
}

 

以及还蕴藏针对流(Stream)的奇特处理,例如如若要用koa兑现静态财富下载的成效,也是足以平昔调用ctx.body拓展赋值的,所有的东西都早已在response.js中帮你处理好了:

// stream
if ('function' == typeof val.pipe) {
  onFinish(this.res, destroy.bind(null, val))
  ensureErrorHandler(val, err => this.ctx.onerror(err))

  // overwriting
  if (null != original && original != val) this.remove('Content-Length')

  if (setType) this.type = 'bin'
  return
}

// 可以理解为是这样的代码
let stream = fs.createReadStream('package.json')
ctx.body = stream

// set body中的处理
onFinish(res, () => {
  destory(stream)
})

stream.pipe(res) // 使response接收流是在洋葱模型完全执行完以后再进行的

 

onFinish用来监听流是还是不是得了、destory用来关闭流

其他的访问器基本上正是1些科学普及操作的包裹,例如针对querystring的封装。
在使用原生http模块的气象下,处理U大切诺基L中的参数,是急需协调引入额外的包实行拍卖的,最广大的是querystring
koa也是在中间引入的该模块。
于是对外抛出的query大约是其一样子的:

get query() {
  let query = parse(this.req).query
  return qs.parse(query)
}

// use
let { id, name } = ctx.query // 因为 get query也被代理到了context上,所以可以直接引用

 

parse为parseurl库,用来从request中提出query参数

亦大概针对cookies的卷入,也是放置了最风靡的cookies
在率先次接触get cookies时才去实例化Cookie对象,将这一个繁琐的操作挡在用户看不到的地点:

get cookies() {
  if (!this[COOKIES]) {
    this[COOKIES] = new Cookies(this.req, this.res, {
      keys: this.app.keys,
      secure: this.request.secure
    })
  }
  return this[COOKIES]
}

set cookies(_cookies) {
  this[COOKIES] = _cookies
}

 

所以在koa中使用Cookie就如这么就能够了:

this.cookies.get('uid')

this.cookies.set('name', 'Niko')

// 如果不想用cookies模块,完全可以自己赋值为自己想用的cookie
this.cookies = CustomeCookie

this.cookies.mget(['uid', 'name'])

 

那是因为在get cookies中间有咬定,就算未有二个可用的库克ie实例,才会暗中认可去实例化。

strict

strictsensitive成效类似,也是用来设置让路径的相当变得更其严酷,在私下认可意况下,路径结尾处的/是可选的,假设翻开该参数未来,假设在注册路由时尾部未有拉长/,则匹配的路由也必定不可见添加/结尾:

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

 

小记

新近抽时间将Koa相关的源码翻看一波,看得挺激动的,想要将它们记录下来。
应当会拆分为几段来,不1篇全写了,上次写了个装饰器的,太长,看得和谐都困了。
先占多少个坑:

  • 着力模块 koa与koa-compose
  • 热门中间件 koa-router与koa-views
  • 凌乱的轮子 koa-bodyparser/multer/better-body/static

演示代码仓库地址
源码阅读仓库地址

  1. 能够经过调用use()函数来注册新的中间件,平日,新的中间件只好被添加到高压包带的背后,但不是严谨供给这么做;
  2. 当接过到要求处理的新数据时,注册的中间件在意不履行流程中被依次调用。各类中间件都领受上贰在那之中间件的实施结果作为输入值;
  3. 每在那之中间件都可以告1段落数据的越来越处理,只须求简单地不调用它的毁掉函数只怕将错误传递给回调函数。当发生错误时,日常会触发执行另二个专门处理错误的中间件。

洋葱模型执行到位后的一些操作

koa的三个伸手流程是如此的,先实施洋葱里边的具有中间件,在执行到位现在,还会有八个回调函数。
该回调用来依据中间件执行进程中所做的政工来控制重返给客户端什么数据。
拿到ctx.bodyctx.status那么些参数举办处理。
席卷后面提到的流(Stream)的处理都在此间:

if (body instanceof Stream) return body.pipe(res) // 等到这里结束后才会调用我们上边`set body`中对应的`onFinish`的处理

 

同时上边还有二个非同小可的拍卖,假设为false则不做此外处理,直接重临:

if (!ctx.writable) return

 

骨子里这么些也是response提供的三个访问器,那里边用来判定当前恳请是或不是曾经调用过end给客户端再次来到了数码,假如已经接触了response.end()以后,则response.finished会被置为true,也正是说,此番请求已经实现了,同时访问器中还处理了二个bug,请求已经回到结果了,不过仍旧未有关闭套接字:

get writable() {
  // can't write any more after response finished
  if (this.res.finished) return false

  const socket = this.res.socket
  // There are already pending outgoing res, but still writable
  // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
  if (!socket) return true
  return socket.writable
}

 

那边就有几个koaexpress绝对而言的劣势了,因为koa利用的是一个洋葱模型,对于重回值,如若是使用ctx.body = 'XXX'来拓展赋值,那会促成最终调用response.end时在洋葱全体推行到位后再开始展览的,也便是上边所讲述的回调中,而express固然在中间件中就足以轻易支配曾几何时归来数据:

// express.js
router.get('/', function (req, res) {
  res.send('hello world')

  // 在发送数据后做一些其他处理
  appendLog()
})

// koa.js
app.use(ctx => {
  ctx.body = 'hello world'

  // 然而依然发生在发送数据之前
  appendLog()
})

 

唯独辛亏仍是能够通过一直调用原生的response指标来开始展览发送数据的,当大家手动调用了response.end以后(response.finished === true),就代表最后的回调会直接跳过,不做别的处理。

app.use(ctx => {
  ctx.res.end('hello world')

  // 在发送数据后做一些其他处理
  appendLog()
})

 

异常处理

koa的万事请求,实际上依旧四个Promise,所以在洋葱模型前边的监听不仅仅有resolve,对reject也壹如既往是有处理的。
中间任何1环出bug都会促成持续的中间件以及前面等待回调的中间件终止,直接跳转到近年来的二个非凡处理模块。
就此,借使有接近接口耗费时间总计的中间件,一定要记得在try-catch中执行next的操作:

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (e) {
    console.error(e)
    ctx.body = 'error' // 因为内部的中间件并没有catch 捕获异常,所以抛出到了这里
  }
})

app.use(async (ctx, next) => {
  let startTime = new Date()
  try {
    await next()
  } finally {
    let endTime = new Date() // 抛出异常,但是不影响这里的正常输出
  }
})

app.use(ctx => Promise.reject(new Error('test')))

 

P.S. 假设那么些被捕获,则会继续执行后续的response

app.use(async (ctx, next) => {
  try {
    throw new Error('test')
  } catch (e) {
    await next()
  }
})

app.use(ctx => {
  ctx.body = 'hello'
})

// curl 127.0.0.1 
// > hello

 

一经自身的中间件未有捕获卓殊,就会走到暗中同意的格外处理模块中。
在默许的十二分模块中,基本上是针对性statusCode的有的拍卖,以及1些默许的荒谬彰显:

const code = statuses[err.status]
const msg = err.expose ? err.message : code
this.status = err.status
this.length = Buffer.byteLength(msg)
this.res.end(msg)

 

statuses是贰个第二方模块,包罗各个http
code的音信: statuses

建议在最外层的中间件都本身做越发处理,因为私下认可的错误提醒某些太掉价了(纯文本),自个儿处理跳转到很是处理页面会好有的,以及防止有个别接口因为暗中同意的11分音讯导致解析退步。

methods

methods安插项存在的含义在于,若是大家有三个接口要求同时帮衬GETPOSTrouter.getrouter.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的判断,而那般带动的1个后果便是,假设咱们在路由的回调中展开类似ctx.body = XXX的操作,实际上会修改此番请求的status值的,使之并不会变成404,而非常小概正确的触及METHOD自作者批评的逻辑。
想要正确的触及METHOD逻辑,就须要协调在路由监听中手动判断ctx.method是还是不是为大家想要的,然后在跳过当前中间件的实践。
而那一论断的手续实际上与allowedMethods中间件中的!~implemented.indexOf(ctx.method)逻辑完全是重复的,不太驾驭koa-router何以会那样处理。

当然,allowedMethods是不可见作为贰个松手中间件来存在的,因为三个Koa中大概会挂在四个RouterRouter里面包车型大巴安顿或许不尽一致,不能够担保拥有的Router都和近期Router可处理的METHOD是如出一辙的。
故此,个人感觉methods参数的存在意义并不是相当的大。。

至于怎么处理传递数据,如今尚未严谨的规则,1般有二种格局

redirect的注意事项

在原生http模块中开始展览302的操作(俗称重定向),须要这么做:

response.writeHead(302, {
  'Location': 'redirect.html'
})
response.end()
// or
response.statusCode = 302
response.setHeader('Location', 'redirect.html')
response.end()

 

而在koa中也有redirect的包裹,能够由此一向调用redirect函数来形成重定向,然而须求小心的是,调用完redirect今后并从未直接接触response.end(),它独自是添加了三个statusCodeLocation而已:

redirect(url, alt) {
  // location
  if ('back' == url) url = this.ctx.get('Referrer') || alt || '/'
  this.set('Location', url)

  // status
  if (!statuses.redirect[this.status]) this.status = 302

  // html
  if (this.ctx.accepts('html')) {
    url = escape(url)
    this.type = 'text/html charset=utf-8'
    this.body = `Redirecting to <a href="${url}">${url}</a>.`
    return
  }

  // text
  this.type = 'text/plain charset=utf-8'
  this.body = `Redirecting to ${url}.`
}

 

继续的代码还会继续执行,所以提出在redirect然后手动截止最近的呼吁,也正是直接return,不然很有非常大大概继续的statusbody赋值很可能会导致有些蹊跷的题材。

app.use(ctx => {
  ctx.redirect('https://baidu.com')

  // 建议直接return

  // 后续的代码还在执行
  ctx.body = 'hello world'
  ctx.status = 200 // statusCode的改变导致redirect失效 
})

 

routerPath

本条参数的留存。。感觉会导致部分很怪异的景况。
这即将谈到在注册完全中学间件未来的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.pathrouter意味着当前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'))

 

只要有那样的代码,无论请求什么U福睿斯L,都会认为是/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!

 

  1. 经过添加属性和艺术来增强;
  2. 使用某种处理的结果来替换 data;
  3. 管教原始要处理的数量不变,永远重临新的副本作为处理的结果。

小记

koa是八个很有意思的框架,在翻阅源码的进程中,其实也发觉了有个别小难题:

  1. 多个人合作保证一份代码,确实能够看出各人都有两样的编码风格,例如typeof val !== 'string''number' == typeof code,很分明的两种风格。2333
  2. delegate的调用方式在性质尤其多的时候并不是很窘迫,一大长串的链式调用,要是换到循环会更赏心悦目一下

但是,koa如故是2个很棒的框架,很吻合阅读源码来展开学习,那几个都以有的小细节,无伤大雅。

小结一下koakoa-compose的作用:

  • koa 注册中间件、注册http劳动、生成请求上下文调用中间件、处理中间件对上下文对象的操作、重返数据停止请求
  • koa-compose 将数组中的中间件集合转换为串行调用,并提供钥匙(next)用来跳转下三个中间件,以及监听next得到内部中间件执行达成的通告

巧用routerPath达成转载功用

同样的,那一个短路运算符一共有八个表明式,第二个的ctx则是时下伏乞的上下文,也等于说,尽管我们有3个早于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的那些还是得以找到确切的情景,那几个能够简单的接头为转载的一种达成,转载的进度是对客户端不可知的,在客户端看来照旧访问的是早期的UPAJEROL,但是在中间件中改变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())

 

那般就兑现了2个不难易行的转速:

> curl -X POST http://127.0.0.1:8888/login
new login logic!

 

而具体的处理格局取决于 中间件管理器
的兑现格局以及中间件本人要到位的天职项目。

注册路由的监听

上述总体是有关实例化Router时的部分操作,下边就来说一下用到最多的,注册路由有关的操作,最熟谙的必定正是router.getrouter.post那个的操作了。
但骨子里这个也只是一个快速情势罢了,在当中调用了来自Routerregister方法:

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 {} 一些注册路由时的配置参数,上边提到的strictsensitiveprefix在这里都有体现

能够观察,函数大致正是贯彻了如此的流水生产线:

  1. 检查path是否为数组,假设是,遍历item进展调用本人
  2. 实例化1个Layer对象,设置有些伊始化参数
  3. 安装针对少数参数的中间件处理(假设某些话)
  4. 将实例化后的对象放入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是背负储存路由监听的消息的,每一遍注册路由时的UBMWX5L,ULacrosseL生成的正则表明式,该ULacrosseL中留存的参数,以及路由对应的中间件。
统统交由Layer来储存,重点需求关心的是实例化进程中的那么些数组参数:

  • methods
  • paramNames
  • stack

methods积存的是该路由监听对应的卓有作用METHOD,并会在实例化的进度中针对METHOD展开高低写的转移。
paramNames因为用的插件难点,看起来不那么清晰,实际上在pathToRegExp中间会对paramNames其一数组进行push的操作,这么看大概会痛快一下pathToRegExp(path, &this.paramNames, this.opts),在拼接hash布局的路子参数时会用到这几个数组
stack储存的是该路由监听对应的中间件函数,router.middleware一对逻辑会依赖于那些数组

举三个源于于 《Node.js 设计形式 第二版》 的3个为音讯传递库达成中间件管理器 的事例:

path

在函数底部的处理逻辑,首倘使为了支持多路径的同时登记,如若发现第一个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.

 

class ZmqMiddlewareManager {
 constructor(socket) {
  this.socket = socket;
  // 两个列表分别保存两类中间件函数:接受到的信息和发送的信息。
  this.inboundMiddleware = [];
  this.outboundMiddleware = [];
  socket.on('message', message => {
   this.executeMiddleware(this.inboundMiddleware, {
    data: message
   });
  });
 }

 send(data) {
  const message = { data };

  this.excuteMiddleware(this.outboundMiddleware, message, () => {
   this.socket.send(message.data);
  });
 }

 use(middleware) {
  if(middleware.inbound) {
   this.inboundMiddleware.push(middleware.inbound);
  }
  if(middleware.outbound) {
   this.outboundMiddleware.push(middleware.outbound);
  }
 }

 exucuteMiddleware(middleware, arg, finish) {
  function iterator(index) {
   if(index === middleware.length) {
    return finish && finish();
   }
   middleware[index].call(this, arg, err => {
    if(err) {
     return console.log('There was an error: ' + err.message);
    }
    iterator.call(this, ++index);
   });
  }
  iterator.call(this, 0);
 }
}

methods

而关于methods参数,则私下认可认为是1个数组,就算是只监听1个METHOD也需求传入1个数组作为参数,倘诺是空数组的话,就算URL匹配,也会平素跳过,执行下2其中间件,那一个在继续的router.routes中会提到

接下去只须要创设中间件,分别在 inbound 和 outbound
中写入中间件函数,然后实施实现调用 next() 就好了。比如: 

middleware

middleware则是二回路由真正进行的事情了,照旧是契合koa标准的中间件,能够有多少个,遵照洋葱模型的艺术来实行。
这也是koa-router中最要害的地点,能够让我们的局地中间件只在特定的URL时执行。
那边写入的陆个中间件都以对准该URL生效的。

P.S.
koa-router中,还提供了一个办法,叫做router.use,那一个会登记一个基于router实例的中间件

const zmqm = new ZmqMiddlewareManager();

zmqm.use({
 inbound: function(message, next) {
  console.log('input message: ', message.data);
  next();
 },
 outbound: function(message, next) {
  console.log('output message: ', message.data);
  next();
 }
});

opts

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 是否忽略路由匹配正则结果中的捕获组

Express 所推广的 中间件 概念就与之接近,三个 Express
中间件1般是这么的:

name

首先是name,首要是用于那多少个地点:

  1. 抛出特别时更方便人民群众的固化
  2. 能够通过router.url(<name>)router.route(<name>)赢获得对应的router信息
  3. 在中间件执行的时候,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)
}

 

function(req, res, next) { ... }
跑题说下router.url的那多少个事儿

假使在档次中,想要针对有些URL实行跳转,使用router.url来生成path则是3个没有错的抉择:

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
函数接收八个参数,paramsoptions,因为本人Layer实例是储存了对应的path等等的消息,所以params哪怕储存的在路径中的1些参数的交替,options在脚下的代码中,仅仅存在1个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)形式的。
这时的逻辑仅须要处理二种处境了:

  1. 数组格局的参数替换
  2. hash花样的参数替换
  3. 无参数

这几个参数替换指的是,多少个URL会透过贰个其3方的库用来拍卖链接中的参数部分,约等于/:XXX的那1部分,然后传入1个hash落到实处类似模版替换的操作:

// 可以简单的认为是这样的操作:
let hash = { id: 123, name: 'Niko' }
'/list/:id/:name'.replace(/(?:\/:)(\w+)/g, (_, $1) => `/${hash[$1]}`)

 

然后layer.url的处理就是为了将各个参数生成类似hash诸如此类的结构,最后替换hash取得完整的URL

Koa二 中使用的中间件

prefix

下边实例化Layer的进程中近乎是opts.prefix的权重更高,不过随后在上边就有了三个判定逻辑进行调用setPrefix再也赋值,在翻遍了全方位的源码后发觉,那样绝无仅有的1个差距就在于,会有一条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
}

 

以此在爆出给使用者的多少个格局中都有反映,类似的getset以及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方法会叠加前缀,而不是覆盖前缀

前边呈现的中间件模型使用回调函数达成的,可是未来有三个比较流行的 Node.js
框架 Koa二 的中间件实现格局与事先描述的有1对不太一样。 Koa2中的中间件方式移除了一初始运用 ES二零一四中的生成器实现的法子,包容了回调函数、 convert 后的生成器以及 async 和
await 。

sensitive与strict

那俩参数没啥好说的,就是会覆盖实例化Router时所传递的那俩参数,效果都如出一辙。

在 Koa二 官方文书档案中付出了3个有关中间件的 洋葱模型 ,如下图所示:

end

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能够简简单单地掌握为是正则中的$,也正是11分的结尾。
看代码的逻辑,大致正是,倘若设置了end: true,则不管任何景况都会在结尾添加$代表格外的结尾。
而如果end: false,则唯有在同时安装了strict: false或者isEndDelimited: false时才会接触。
故而我们能够经过那八个参数来贯彻U大切诺基L的歪曲匹配:

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的央求都会被那在那之中间件所获取到。

4858美高梅 3

ignoreCaptures

ignoreCaptures参数用来安装是或不是须要再次回到URL中匹配的途径参数给中间件。
而1旦设置了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' }

 

从图中大家得以观察,先进入 inbound 的中间件函数在 outbound
中被放置了背后推行,那么到底是怎么吗?带着那个题材大家去读一下 Koa二的源码。

router.param的作用

上述是关于注册路由时的部分参数描述,能够观察在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

 

在 koa/lib/applications.js 中,先看构造函数,别的的都得以不管,关键正是this.middleware ,它是四个 inbound 队列:

最常用的get/post之类的快捷格局

以及说完了下边包车型地铁底子措施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参数都砍掉了,私下认可只留下了3个name字段。
只是很简单的处理了须臾间命名name路由相关的逻辑,然后举行调用register姣好操作。

constructor() {
 super();

 this.proxy = false;
 this.middleware = [];
 this.subdomainOffset = 2;
 this.env = process.env.NODE_ENV || 'development';
 this.context = Object.create(context);
 this.request = Object.create(request);
 this.response = Object.create(response);
}

router.use-Router内部的中间件

以及上文中也关系的router.use,能够用来注册一当中间件,使用use登记中间件分为二种情状:

  1. 见怪不怪的中间件函数
  2. 将现有的router实例作为中间件传入

和地点一样,在 Koa贰 中也是用 use() 来把中间件放入队列中:

普通的use

这里是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
}

 

先是种是相比较正规的方法,传入3个函数,3个可选的path,来展开挂号中间件。
不过有少数要注意的是,.use('path')这么的用法,中间件无法独立存在,必必要有三个能够与之途径相匹配的路由监听存在:

router.use('/list', ctx => {
  // 如果只有这么一个中间件,无论如何也不会执行的
})

// 必须要存在相同路径的`register`回调
router.get('/list', ctx => { })

app.use(router.routes())

 

案由是那般的:

  1. .use.get都以根据.register来完成的,但是.usemethods参数中传递的是三个空数组
  2. 在3个路径被匹配到时,会将兼具匹配到的中级件取出来,然后检核查应的methods,如果length !== 0则会对现阶段匹配组标记一个flag
  3. 在推行中间件此前会先判断有未有那一个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
    }

 

use(fn) {
 if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
 if (isGeneratorFunction(fn)) {
  deprecate('Support for generators will be removed in v3. ' +
    'See the documentation for examples of how to convert old middleware ' +
    'https://github.com/koajs/koa/blob/master/docs/migration.md');
  fn = convert(fn);
 }
 debug('use %s', fn._name || fn.name || '-');
 this.middleware.push(fn);
 return this;
}
将其他router实例传递进入

能够看看,假使选择了router.routes()来格局来复用中间件,会遍历该实例的持有路由,然后设置prefix
并将修改完的layer推出到当下的router中。
那正是说未来快要小心了,在上头其实早就涉嫌了,LayersetPrefix是东拼西凑的,而不是覆盖的。
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())

 

就像上述代码,实际上会有三个难点:

  1. 最终有效的拜会路径为/page2/page1/list/1,因为prefix会拼接而非覆盖
  2. 当我们在中间件中调用next以后,console.log会一而再输出3次,因为兼具的routes都以动态的,实际上prefix都被修改为了/page2/page1

毫无疑问要小心使用,不要认为那样的艺术得以用来兑现路由的复用

跟着我们看框架对端口监听举行了1个简便的包装:

呼吁的拍卖

以及,终于来到了最终一步,当一个请求来了随后,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并且还提供了3个小名middleware来促成平等的功能。
以及函数的调用最后会回来贰个中间件函数,那么些函数才是的确被挂在到koa上的。
koa的中间件是彻头彻尾的中间件,不管如何请求都会履行所包蕴的中间件。
之所以不提出为了利用prefix而创办多个Router实例,那会导致在koa上挂载四个dispatch用来检查U汉兰达L是不是切合规则

进入中间件今后会开始展览UKugaL的判定,就是我们上面提到的能够用来做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一贯是在累加的,而非每一遍都会覆盖。

pathpathAndMethod都是match重回的三个数组,两者的界别在于path回去的是匹配U奇骏L成功的数码,而pathAndMethod则是匹配U本田UR-VL且匹配到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中或然包罗5当中间件,不要忘了middleware,这正是怎么会在reduce中使用concat而非push
因为要在每2当中间件执行在此之前,修改ctx为此次中间件触发时的壹些音讯。
席卷匹配到的URAV4L参数,以及当前中间件的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中关系了的第2个可选的参数,用来做洋葱执行到位后最后的回调解和处理理。


// 封装之前 http.createServer(app.callback()).listen(...)
listen(...args) {
 debug('listen');
 const server = http.createServer(this.callback());
 return server.listen(...args);
}

小记

至此,koa-router的职务就曾经完成了,实现了路由的挂号,以及路由的监听处理。
在阅读koa-router的源码进度中感觉很吸引:

  • 威名昭著代码中一度完毕的职能,为何在文书档案中就从不反映出来呢。
  • 只要文书档案中不写明能够那样来用,为何还要在代码中有照应的兑现啊?

多少个最简易的举例证明:

  1. 能够透过修改ctx.routerPath来实现forward效能,可是在文书档案中不会报告你
  2. 能够透过router.register(path, ['GET', 'POST'])来飞快的监听多少个METHOD,但是register被标记为了@private

参考资料:

  • koa-router |
    docs
  • path-to-regexp |
    docs

演示代码在库房中的地方:learning-koa-router

中间件的管制主要性就在于 this.callback() ,看一下以此措施:

callback() {
 const fn = compose(this.middleware);

 if (!this.listenerCount('error')) this.on('error', this.onerror);

 const handleRequest = (req, res) => {
  const ctx = this.createContext(req, res);
  return this.handleRequest(ctx, fn);
 };

 return handleRequest;
}

此地的 compose 方法其实是 Koa二 的三个基本模块 koa-compose
(),在那个模块中封装了中间件执行的主意:

function compose (middleware) {
 if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
 for (const fn of middleware) {
  if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
 }

  /**
  * @param {Object} context
  * @return {Promise}
  * @api public
  */

 return function (context, next) {
  // last called middleware #
  let index = -1
  return dispatch(0)
  function dispatch (i) {
   if (i <= index) return Promise.reject(new Error('next() called multiple times'))
   index = i
   let fn = middleware[i]
   if (i === middleware.length) fn = next
   if (!fn) return Promise.resolve()
   try {
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
   } catch (err) {
    return Promise.reject(err)
   }
  }
 }
}

可以观望, compose 通过递归对中间件队列进行了 反序遍历 ,生成了1个Promise 链,接下去,只须要调用 Promise 就足以执行中间件函数了:

handleRequest(ctx, fnMiddleware) {
 const res = ctx.res;
 res.statusCode = 404;
 const onerror = err => ctx.onerror(err);
 const handleResponse = () => respond(ctx);
 onFinished(res, onerror);
 return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

从源码中得以窥见, next() 中回到的是贰个 Promise
,所以通用的中间件写法是:

app.use((ctx, next) => {
 const start = new Date();
 return next().then(() => {
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
 });
});

自然要是要用 async 和 await 也行:

app.use((ctx, next) => {
 const start = new Date();
 await next();
 const ms = new Date() - start;
 console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

由于还有众多 Koa一 的种类中间件是依照生成器的,需求动用 koa-convert
来实行平整升级:

const convert = require('koa-convert');

app.use(convert(function *(next) {
 const start = new Date();
 yield next;
 const ms = new Date() - start;
 console.log(`${this.method} ${this.url} - ${ms}ms`);
}));

如上正是本文的全体内容,希望对我们的求学抱有辅助,也可望大家多多帮衬脚本之家。

你也许感兴趣的篇章:

  • node.js中路由,中间件,ge请求和post请求的参数详解
  • node.js中express中间件body-parser的牵线与用法详解
  • node.js
    中间件express-session使用详解
  • node.js cookie-parser
    中间件介绍

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有