koa源码浏览[1]-koa与koa-compose

接上次挖的坑,对koa2.x相干的源码举行剖析 第一篇
不得不说,koa是一个很轻量、很文雅的http框架,尤其是在2.x今后移除了co的引入,使其代码变得更加清楚。

expresskoa同为一批人举行开辟,与express比拟,koa显得非常的迷你。
由于express是一个大而全的http框架,内置了相似router之类的中间件举行处置惩罚。
而在koa中,则将相似功用的中间件悉数摘了出来,初期koa里边是内置了koa-compose的,而如今也是将其分了出来。
koa只保存一个简朴的中间件的整合,http请求的处置惩罚,作为一个功用性的中间件框架来存在,自身唯一少许的逻辑。
koa-compose则是作为整合中间件最为症结的一个东西、洋葱模子的详细完成,所以要将二者放在一同来看。

koa基础组织

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

关于koa全部框架的完成,也只是简朴的拆分为了四个文件。

就象在上一篇笔记中模仿的那样,建立了一个对象用来注册中间件,监听http效劳,这个就是application.js在做的事变。
而框架的意义呢,就是在框架内,我们要依据框架的礼貌来做事变,一样的,框架也会供应给我们一些更易用的体式格局来让我们完成需求。
针对http.createServer回调的两个参数requestresponse举行的一次封装,简化一些经常运用的操纵。
比方我们对Header的一些操纵,在原生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的功用,四个文件的作用已很清楚了:

filedesc
applicaiton中间件的治理、http.createServer的回调处置惩罚,天生Context作为本次请求的参数,并挪用中间件
request针对http.createServer -> request功用上的封装
response针对http.createServer -> response功用上的封装
context整合requestresponse的部份功用,并供应一些分外的功用

而在代码组织上,只要application对外的koa是采纳的Class的体式格局,其他三个文件均是抛出一个一般的Object

拿一个完整的流程来诠释

建立效劳

起首,我们须要建立一个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()
接下来的步骤就是注册中间件并监听一个端口号启动效劳:

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,以及在2.x版本针对Generator函数的一些迥殊处置惩罚,将其转换为了Promise情势的函数,并将其push到组织函数中建立的middleware数组中。
这个是从1.x过渡到2.x的一个东西,在3.x版本将直接移除Generator的支撑。
实在在koa-convert内部也是援用了cokoa-compose来举行转化,所以也就不再赘述。

而在listen中做的事变就更简朴了,只是简朴的挪用http.createServer来建立效劳,并监听对应的端口之类的操纵。
有一个细节在于,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-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代码唯一48行。

我们晓得,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()
    })
  }
]

就像在第四个函数中输出示意的那样,第四个中间件不会被实行,由于第三个中间件并没有挪用next,所以完成相似如许的一个洋葱模子是很有意义的一件事变。
起首抛开稳定的ctx不谈,洋葱模子的完成中心在于next的处置惩罚。
由于next是你进入下一层中间件的钥匙,只要手动触发今后才会进入下一层中间件。
然后我们还须要保证next要在中间件实行终了后举行resolve,返回到上一层中间件:

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今后现实上会返回一个自实行函数。
在实行函数的开首部份,推断当前中间件的下标来防备在一个中间件中屡次挪用next
由于假如屡次挪用next,就会致使下一个中间件的屡次实行,如许就破坏了洋葱模子。

其次就是compose现实上供应了一个在洋葱模子悉数实行终了后的回调,一个可选的参数,现实上作用与挪用compose后边的then处置惩罚没有太大区分。

以及上边提到的,next是进入下一个中间件的钥匙,可以在这一个柯里化函数的应用上看出来:

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

将自身绑定了index参数后传入本次中间件,作为挪用函数的第二个参数,也就是next,效果就像挪用了dispatch(1),如许就是一个洋葱模子的完成。
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]

吸收请求,处置惩罚返回值

经由上边的代码,一个koa效劳已算是运转起来了,接下来就是接见看效果了。
在吸收到一个请求后,koa会拿之条件到的contextrequestresponse来建立本次请求所运用的上下文。
koa1.x中,上下文是绑定在this上的,而在koa2.x是作为第一个参数传入进来的。
个人猜想可以是由于Generator不能运用箭头函数,而async函数可以运用箭头函数致使的吧:) 纯属个人YY

总之,我们经由历程上边提到的三个模块建立了一个请求所需的上下文,基础上是一通儿赋值,代码就不贴了,没有太多逻辑,就是有一个小细节比较有意义:

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的处置惩罚,做一些相似将ctx.body转换为数据举行输出之类的操纵。

koa运用了大批的getset接见器来完成功用,比方最经常运用的ctx.body = 'XXX',它是来自responseset body
这应该是requestresponse中逻辑最庞杂的一个要领了。
里边要处置惩罚许多东西,比方在body内容为空时协助你修正请求的status code为204,并移除无用的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模块的状况下,处置惩罚URL中的参数,是须要本身引入分外的包举行处置惩罚的,最罕见的是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实例,才会默许去实例化。

洋葱模子实行完成后的一些操纵

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也一样是有处置惩罚的。
时期任何一环出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是一个第三方模块,包含种种http code的信息: statuses
发起在最外层的中间件都本身做非常处置惩罚,由于默许的毛病提醒有点儿太难看了(纯文本),本身处置惩罚跳转到非常处置惩罚页面会好一些,以及防止一些接口由于默许的非常信息致使剖析失利。

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失效 
})

小记

koa是一个很好玩的框架,在浏览源码的历程当中,实在也发现了一些小题目:

  1. 多人协作保护一份代码,确切可以看出大家都有差别的编码作风,比方typeof val !== 'string''number' == typeof code,很显然的两种作风。2333
  2. delegate的挪用体式格局在属性迥殊多的时刻并非很悦目,一大长串的链式挪用,假如换成轮回会更悦目一下

然则,koa依旧是一个很棒的框架,很合适浏览源码来举行进修,这些都是一些小细节,无伤大雅。

总结一下koakoa-compose的作用:

  • koa 注册中间件、注册http效劳、天生请求上下文挪用中间件、处置惩罚中间件对上下文对象的操纵、返回数据完毕请求
  • koa-compose 将数组中的中间件鸠合转换为串行挪用,并供应钥匙(next)用来跳转下一个中间件,以及监听next猎取内部中间件实行完毕的关照

招人,招人

我司如今大批招人咯,前端、Node方向都有HC
公司名:Blued,坐标帝都旭日双井
重要手艺栈是React,也会有机会玩ReactNative和Electron
Node方向8.x版本+koa 新项目会以TS为主
有兴致的小伙伴可以私聊我,或许:
email: jiashunming@blued.com
wechat: github_jiasm

    原文作者:贾顺名
    原文地址: https://segmentfault.com/a/1190000015810835
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞