【亚洲必赢app】中间件方式,koa源码阅读

接上次挖的坑,对koa2.x相关的源码举办分析 第一篇。
唯其如此说,koa是二个很轻量、很优雅的http框架,特别是在二.x之后移除了co的引入,使其代码变得越来越清晰。

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

其三篇,有关koa生态中比较关键的贰个中间件:koa-router

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

koa源码阅读[0]

Node.js也是写了两三年的时间了,刚发轫读书Node的时候,hello world哪怕创设三个HttpServer,后来在工作中也是涉世过ExpressKoa1.xKoa2.x以及近日还在研究的组合着TypeScriptrouting-controllers(驱动如故是ExpressKoa)。
用的比较多的要么Koa本子,也是对它的洋葱模型比较感兴趣,所以近年来抽出时间来读书其源码,正好如今只怕会对1个Express花色开始展览重构,将其重构为koa2.x本子的,所以,阅读其源码对于重构也是壹种有效的支持。

中间件在 Node.js
中被普遍利用,它泛指1种特定的设计形式、壹多元的处理单元、过滤器和处理程序,以函数的样式存在,连接在共同,形成多个异步队列,来形成对别的数据的预处理和后甩卖。

expresskoa同为一堆人进行支付,与express相比,koa展示十分的Mini。
因为express是多少个大而全的http框架,内置了就如router等等的中间件实行处理。
而在koa中,则将类似成效的中间件全体摘了出来,早期koa里面是放到了koa-compose的,而以后也是将其分了出去。
koa只保留一个简易的中间件的结合,http恳请的处理,作为二个功能性的中间件框架来存在,自个儿仅有微量的逻辑。
koa-compose则是作为整合中间件最为根本的3个工具、洋葱模型的切实可行完结,所以要将2者放在壹起来看。

koa-router是什么

先是,因为koa是2个管制中间件的阳台,而注册一个中间件使用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'
  }
})

 

诚然,那样是三个简练的格局,可是毫无疑问不适用于大型项目,数十三个接口通过三个switch来控制未免太繁琐了。
再则请求大概只协助get或者post,以及那种办法并不可能很好的支撑UWranglerL中隐含参数的伏乞/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应该算得最常用的三个。
所以在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())

 

看起来代码确实多了壹部分,毕竟将过多逻辑都从框架之中间转播移到了中间件中来处理。
也毕竟为了保持三个简练的koa框架所取舍的一部分东西呢。
koa-router的逻辑确实要比koa的复杂壹些,能够将koa想象为1个市集,而koa-router则是里面3个货摊
koa仅必要确定保证市镇的安定团结运营,而实在和顾客打交道真的是在中间摆摊的koa-router

Koa是怎么来的

先是须要显著,Koa是什么样。
其它一个框架的面世都以为着消除难题,而Koa则是为了更有利于的创设http服务而出现的。
能够省略的知情为1个HTTP服务的中间件框架。

它的优点在于 灵活性
:使用中间件大家用极少的操作就能取得3个插件,用最简便的办法就能将新的过滤器和处理程序扩展到存活的系统上。

koa基本构造

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

 

关于koa整套框架的落到实处,也只是简短的拆分为了多少个公文。

就象在上壹篇笔记中模拟的那么,创立了三个目的用来注册中间件,监听http劳务,这么些便是application.js在做的政工。
而框架的意思呢,正是在框架内,大家要依照框架的本分来做业务,同样的,框架也会提要求大家有的更易用的法子来让大家成功要求。
针对http.createServer回调的四个参数requestresponse开始展览的1次封装,简化1些常用的操作。
譬如说我们对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文件中。
但还要那会推动二个使用上的麻烦,那样封装以往实际取得可能设置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的不2诀窍,别的七个公文均是抛出3个一般性的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'))

 

3个最简单易行的演示,脚本运维后走访http://127.0.0.1:8888即可知到1个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) // 返回数据
}

 

健康中间件情势

拿1个整机的流程来诠释

koa-router的运维流程

能够拿上面所抛出的主导例子来注明koa-router是何等的3个实施流程:

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多的接口。
比方都写在那2个handler里面去,未免太过难以维护。
示范只是简短的针对性贰个变量实行赋值,可是实际的档次不会有如此简单的逻辑存在的。
由此,大家针对handler开始展览二回抽象,让大家能够有利于的管理路径:

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

 

中间件形式中,最基础的组成都部队分正是 中间件管理器
,我们得以用它来组织和执行中间件的函数,如图所示:

创造服务

第3,大家供给创立二个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仅看成1在那之中间件的组成以及呼吁的监听。
为此不会像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
三个用来加载中间件,另2个用来监听端口并运转服务。

而那三个函数实际上并不曾过多的逻辑,在use中仅仅是判定了流传的参数是或不是为三个function,以及在二.x本子针对Generator函数的一部分奇特处理,将其更换为了Promise花样的函数,并将其push【亚洲必赢app】中间件方式,koa源码阅读。到构造函数中开创的middleware数组中。
本条是从1.x过渡到2.x的一个工具,在3.x本子将直接移除Generator的支持。
其实在koa-convert其间也是引用了cokoa-compose来进展转向,所以也就不再赘述。

而在listen中做的工作就更简便了,只是简单的调用http.createServer来创立服务,并监听对应的端口之类的操作。
有1个细节在于,createServer中传来的是koa实例的另三个方法调用后的再次回到值callback,这些法子才是当真的回调解和处理理,listen只是http模块的贰个急忙格局。
其一是为了局地用socket.iohttps要么部分任何的http模块来拓展利用的。
也就意味着,只倘使足以提供与http模块一致的表现,koa都足以很有益于的过渡。

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

 

创建实例时的某些作业

首先,在koa-router实例化的时候,是能够传递四个配备项参数作为开端化的布署音讯的。
不过那一个布局项在readme中只是不难的被描述为:

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

告诉大家得以加上3个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,但作用上照旧是很简陋的。
设若我们未来有三个急需,要在一些请求的前面添加一些参数的变化,比如一个请求的唯壹ID。
将代码重复编写在大家的handler中肯定是不可取的。
就此大家要对准route的处理进展优化,使其帮忙传入多少个handler

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的中央所在,壹切依靠都经过中间件来拓展加载。

亚洲必赢app 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的代码也正是1两百行,所以精简后的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,所以落成类似那样的一个洋葱模型是很有意思的壹件工作。
首先抛开不变的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在近年来中间件执行到位后会触发回调通告上八在那之中间件,而成功的前提是内部的中间件已经推行到位(resolved)

能够见到在调用koa-compose自此实际会重临3个自实行函数。
在执行函数的开首部分,判断当前中间件的下标来严防在两个中间件中一再调用next
因为一旦反复调用next,就会招致下二当中间件的1再实施,那样就破坏了洋葱模型。

其次正是compose骨子里提供了八个在洋葱模型全部执行落成后的回调,一个可选的参数,实际上效能与调用compose后边的then拍卖未有太大分别。

以及上面提到的,next是跻身下3在那之中间件的钥匙,能够在那2个柯里化函数的使用上看出来:

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

 

将自家绑定了index参数后传出这次中间件,作为调用函数的第3个参数,也正是next,效果就像是调用了dispatch(1),那样就是叁个洋葱模型的贯彻。
fn的调用借使是2个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从不那样的操作。
据此壹旦在中间件中要针对2个数组实行异步操作,一定要手动添加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()来进入下2个环节,整个工艺流程变得很清晰。
但是如故留存部分局限性。
譬如如若我们须求展开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
)

 

无论是哪一种方案,对于原来代码都是一种破坏性的改动,那是不可取的。
因为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来做异步流程的操纵。
所以,随着Node7.陆版本的发生,援助了asyncawait语法,社区也推出了Koa2.x,使用async语法替换在此之前的co+Generator

Koa也将co从依赖中移除(2.x本子接纳koa-convert亚洲必赢app ,将Generator函数转换为promise,在三.x本子司令员直接不辅助Generator
ref: remove generator
supports

鉴于在效劳、使用上Koa的八个版本之间并不曾什么界别,最多便是壹些语法的调整,所以会直接跳过部分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被调用时,第3在这之中间件的then回调才会进行。
那样就贯彻了三个洋葱模型。

就像是我们的log中间件执行的流水生产线:

  1. 收获当前的光阴戳requestTime
  2. 调用next()履行后续的中间件,并监听其回调
  3. 第二当中间件里边大概会调用第四个、第四个、第伍个,但那都不是log所关怀的,log只关心首当中间件曾几何时resolve,而第二个中间件的resolve则借助他后面包车型大巴中间件的resolve
  4. 等到第3个中间件resolve,那就象征后续未有别的的中间件在进行了(全都resolve了),此时log才会继续接二连三代码的进行

从而就如洋葱一样壹层一层的包裹,最外层是最大的,是首先执行的,也是最后执行的。(在二个整机的央浼中,next以前发轫执行,next之后最后执行)。
亚洲必赢app 2

要落实中间件情势,最根本的贯彻细节是:

接过请求,处理重回值

经过下边的代码,一个koa服务业已算是运营起来了,接下去正是访问看成效了。
在接到到二个呼吁后,koa会拿在此之前提到的contextrequestresponse来创制这次请求所选取的上下文。
koa1.x中,上下文是绑定在this上的,而在koa2.x是当做第一个参数字传送入进来的。
民用推断也许是因为Generator不能够使用箭头函数,而async函数能够动用箭头函数导致的吧:) 绝对个人YY

总的说来,大家因此上面提到的八个模块创设了二个呼吁所需的上下文,基本上是1通儿赋值,代码就不贴了,没有太多逻辑,正是有二个小细节相比较有趣:

request.response = response
response.request = request

 

让两者之间产生了3个引用关系,既能够由此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扭转的洋葱中去。
因为无论是何种景况,洋葱肯定会回去结果的(出错与否),所以大家还索要在终极有1个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用来关闭流

任何的访问器基本上正是部分广泛操作的包裹,例如针对querystring的封装。
在行使原生http模块的场所下,处理UKugaL中的参数,是索要协调引入额外的包进行处理的,最广大的是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个中有咬定,若是未有二个可用的Cookie实例,才会暗中同意去实例化。

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. 各此中间件都可以告壹段落数据的愈加处理,只须要简单地不调用它的毁掉函数或许将错误传递给回调函数。当产生错误时,常常会触发执行另贰个特意处理错误的中间件。

洋葱模型执行到位后的局地操作

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的片段甩卖,以及一些私下认可的失实呈现:

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是三个第2方模块,包含各个http
code的新闻: statuses

建议在最外层的中间件都友好做丰裕处理,因为私下认可的不当提醒有个别太不要脸了(纯文本),本身处理跳转到极度处理页面会好1些,以及幸免有个别接口因为暗中同意的可怜新闻导致解析失败。

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中相比关键的1些源码:

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是当做1个前置的中间件存在的,因为在回去的函数中先调用了next,其次才是针对METHOD的判断,而那般带动的三个后果正是,假使大家在路由的回调中展开类似ctx.body = XXX的操作,实际上会修改此番请求的status值的,使之并不会变成404,而望洋兴叹正确的接触METHOD反省的逻辑。
想要正确的接触METHOD逻辑,就要求自个儿在路由监听中手动判断ctx.method是还是不是为我们想要的,然后在跳过当前中间件的实行。
而这壹论断的手续实际上与allowedMethods中间件中的!~implemented.indexOf(ctx.method)逻辑完全是再度的,不太驾驭koa-router缘何会如此处理。

当然,allowedMethods是不可见作为1个放到中间件来存在的,因为3个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注册的是这么的1当中间件,在每一遍请求发送过来时,都会实施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'))

 

若是有诸如此类的代码,无论请求什么ULANDL,都会以为是/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是3个很有意思的框架,在翻阅源码的长河中,其实也发觉了一些小标题:

  1. 三人搭档保险一份代码,确实可以看出各人都有例外的编码风格,例如typeof val !== 'string''number' == typeof code,很显眼的二种风格。233叁
  2. delegate的调用格局在质量越来越多的时候并不是很为难,一大长串的链式调用,假使换来循环会更雅观一下

但是,koa依旧是一个很棒的框架,很符合阅读源码来进展学习,这么些都以1些小细节,无伤大雅。

总括一下koakoa-compose的作用:

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

巧用routerPath实现转载功能

同等的,那些短路运算符1共有四个表明式,第二个的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的那一个还是能找到适当的气象,那么些能够省略的敞亮为转载的1种完结,转载的经过是对客户端不可知的,在客户端看来照旧访问的是初期的U汉兰达L,不过在中间件中改变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())

 

那样就得以实现了1个简约的转会:

> 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. 实例化2个Layer目的,设置有些起始化参数
  3. 设置针对1些参数的中间件处理(假若有的话)
  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是承担储存路由监听的新闻的,每回注册路由时的U帕杰罗L,U景逸SUVL生成的正则表达式,该URAV4L中设有的参数,以及路由对应的中间件。
统统交由Layer来储存,重点要求关心的是实例化进度中的那2个数组参数:

  • methods
  • paramNames
  • stack

methods仓库储存的是该路由监听对应的有效性METHOD,并会在实例化的经过中针对METHOD拓展高低写的更换。
paramNames因为用的插件难题,看起来不那么清楚,实际上在pathToRegExp其间会对paramNames以此数组实行push的操作,这么看或许会清爽一下pathToRegExp(path, &this.paramNames, this.opts),在拼接hash布局的不贰诀窍参数时会用到这些数组
stack积存的是该路由监听对应的中间件函数,router.middleware一对逻辑会依赖于这么些数组

举1个出自于 《Node.js 设计情势 第2版》 的贰个为音信传递库完毕中间件管理器 的例子:

path

在函数底部的拍卖逻辑,主假如为了帮忙多路径的还要登记,倘诺发现第2个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参数,则默许认为是一个数组,固然是只监听3个METHOD也亟需传入八个数组作为参数,如若是空数组的话,即便URL拾分,也会一贯跳过,执行下八其中间件,这一个在接二连三的router.routes中会提到

接下去只供给创建中间件,分别在 inbound 和 outbound
中写入中间件函数,然后实施达成调用 next() 就好了。比如: 

middleware

middleware则是1次路由真正执行的政工了,如故是符合koa行业内部的中间件,能够有多少个,根据洋葱模型的点子来施行。
这也是koa-router中最根本的地方,能够让大家的一对中间件只在特定的URL时执行。
那里写入的陆在那之中间件都是对准该URL生效的。

P.S.
koa-router中,还提供了1个形式,叫做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 所推广的 中间件 概念就与之类似,2个 Express
中间件一般是如此的:

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则是1个没有错的选料:

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艺术,该办法首假使用来处理生成时传回的1对参数。
源码地址:layer.js#L116
函数接收五个参数,paramsoptions,因为作者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)形式的。
那儿的逻辑仅须求处理三种情形了:

  1. 数组情势的参数替换
  2. hash方式的参数替换
  3. 无参数

本条参数替换指的是,二个URL会经过2个其叁方的库用来处理链接中的参数部分,也正是/:XXX的那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再也赋值,在翻遍了百分百的源码后发觉,那样绝无仅有的贰个分裂就在于,会有一条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方法会叠加前缀,而不是覆盖前缀

前面体现的中间件模型使用回调函数达成的,但是未来有1个相比较流行的 Node.js
框架 Koa2 的中间件落成情势与前边描述的有部分不太壹致。 Koa2中的中间件情势移除了一方始采用 ES二〇一六中的生成器完结的不二诀窍,包容了回调函数、 convert 后的生成器以及 async 和
await 。

sensitive与strict

那俩参数没啥好说的,正是会覆盖实例化Router时所传递的那俩参数,效果都同样。

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

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能够省略地了然为是正则中的$,也便是合营的末梢。
看代码的逻辑,大概正是,倘诺设置了end: true,则不管任何情状都会在结尾添加$代表10分的结尾。
而如果end: false,则唯有在同时设置了strict: false或者isEndDelimited: false时才会触发。
因此大家得以经过这两个参数来兑现UTiguanL的模糊匹配:

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的央求都会被那么些中间件所得到到。

亚洲必赢app 3

ignoreCaptures

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' }

 

从图中大家得以看来,先进入 inbound 的中间件函数在 outbound
中被内置了背后推行,那么毕竟是为啥吗?带着那么些标题大家去读一下 Koa2的源码。

router.param的作用

上述是关于注册路由时的1对参数描述,能够见见在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参数都砍掉了,暗中同意只留下了贰个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实例作为中间件传入

和地点壹样,在 Koa2 中也是用 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
}

 

先是种是比较健康的章程,传入1个函数,贰个可选的path,来拓展挂号中间件。
只是有一些要留意的是,.use('path')那般的用法,中间件不可能独立存在,必须求有1个足以与之途径相匹配的路由监听存在:

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

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

app.use(router.routes())

 

由来是如此的:

  1. .use.get都是遵照.register来促成的,可是.usemethods参数中传递的是叁个空数组
  2. 在三个路径被匹配到时,会将装有匹配到的中游件取出来,然后检核查应的methods,如果length !== 0则会对日前匹配组标记3个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会再三再四输出一回,因为拥有的routes都以动态的,实际上prefix都被涂改为了/page2/page1

自然要小心使用,不要觉得这么的方法得以用来促成路由的复用

跟着大家看框架对端口监听进行了3个简便的卷入:

呼吁的处理

以及,终于来临了最后一步,当三个伸手来通晓后,Router是怎么着处理的。
一个Router实例能够抛出两当中间件注册到koa上:

app.use(router.routes())
app.use(router.allowedMethods())

 

routes肩负重大的逻辑。
allowedMethods担负提供3个前置的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
}

 

第3能够看出,koa-router而且还提供了二个外号middleware来促成平等的职能。
以及函数的调用最后会回到3当中间件函数,那几个函数才是实在被挂在到koa上的。
koa的中间件是彻头彻尾的中间件,不管什么样请求都会履行所包含的中间件。
为此不提议为了接纳prefix而创立两个Router实例,那会招致在koa上挂载三个dispatch用来检查U库罗德L是不是吻合规则

进入中间件今后会进展UKoleosL的判定,正是大家上面提到的能够用来做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回到的是匹配URubiconL成功的数据,而pathAndMethod则是匹配UPRADOL且匹配到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为本次中间件触发时的部分新闻。
归纳匹配到的U汉兰达L参数,以及当前中间件的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中提到了的首个可选的参数,用来做洋葱执行到位后最终的回调解和处理理。


// 封装之前 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 通过递归对中间件队列举行了 反序遍历 ,生成了三个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地图