
2.1.2 上下文对象
在Web应用中,Koa的上下文对象ctx是一次完整的HTTP请求的上下文,贯穿这个请求的生命周期。请求会经过N(N>0)层中间件的拦截,唯一共享的就是这个上下文对象。它主要封装了request对象与response对象,并提供了一些帮助开发者编写业务逻辑的方法,我们可以在ctx.request和ctx.response中很方便地访问这些方法。
在控制业务逻辑的中间件中,Koa v1的上下文被寄存在this对象中,Koa v2的上下文是以参数形式存在的ctx对象,这里统一把上下文定为ctx(后面不会再区分是基于Koa v1还是基于Koa v2),代码如下。

每个请求至少贯穿一个中间件,ctx在整个中间件流转过程中是一直存在的,示例如下。

可以看到,首先ctx.a=1,然后在第二个中间件里ctx.a=2,由于第二个中间件里没有next命令,所以结束响应,返回第一个中间件next()后面。此时ctx.a=2,此处涉及Koa中间件原理,后面会详细介绍。
ctx的生命周期是贯穿整个HTTP请求过程的。在ctx上绑定内容不是一个好的做法,但适当地在ctx上下文中绑定某些内容是必要的,这样做能够让我们更方便地实现业务逻辑,常见的日志中间件、ctx.render()函数,就是非常典型的例子。
➘ 源码分析
ctx上常用的对象有request、response、req、res等,其中,request和response是Koa内置的对象,是对HTTP的实用扩展;而req和res是在http.createServer回调函数里注入的,即未经加工的原生内置对象。
第1章讲过,Koa提供的中间件机制是对http.createServer回调进行抽象,下面看一个简单的例子。

也就是说,app里被注入了http.createServer的回调函数,核心是app.callback()实现。下面我们来看一下callback实现代码(在Koa源码application.js里)。

其中const ctx=this.createContext(req,res)表示进行了绑定。我们继续看一个示例。

上面代码中的要点如下。
○ context.request=Object.create(this.request)是Koa内置的request对象。
○ context.response=Object.create(this.response)是Koa内置的response对象。
○ context.app=request.app=response.app=this是app自身。
○ context.req是原始HTTP回调函数里的req对象。
○ context.res是原始HTTP回调函数里的res对象。
○ context.originalUrl是最初的URL地址。
○ context.cookies是浏览器Cookie封装。
○ context.state={} 约定了一个中间件的公用存储空间,可以储存一些数据,比如用户数据。另外,类似koa-views这些视图层的中间件也会默认把ctx.state里面的属性作为视图的上下文并用于渲染。ctx.state和Express里的res.locals是一样的。
这里给读者留一个问题:能否使用前端状态管理模块,比如Redux、Dva、Vuex,来管理服务器端状态?请大家思考。
➘ request对象和response对象
在Koa应用里,最常用的就是request对象和response对象。为了使用方便,许多上下文属性和方法都被委托代理到了Koa的ctx.request或ctx.response上。按照职责来分,与请求相关的方法都被放到了ctx.request中,与响应相关的方法则被挂载在ctx.response上。比如ctx.type和ctx.length是实现response对象访问的便捷方法,ctx.path和ctx.method是实现request对象访问的便捷方法。
前面已经讲了req和res的继承关系,如下。

基于以上关系,想要对Stream进行扩展就非常简单了。对于request.js,我们以Koa的扩展方法为例;对于response.js,我们以Express的扩展方法为例,向读者展示Stream的扩展方法。
1.request.js
在http://extend/request.js里增加query方法,用于获取QueryString,代码如下。

2.response.js
requestListener上没有res.json方法,但为了使用方便,Express和Koa均提供了json方法来快速返回JSON API。下面以Express为例向http/extend/response.js文件里增加json方法,代码如下。

通过继承http.ServerResponse,可以提供更多扩展响应的方式,所以在使用时可以直接使用res.json方法。res.json方法主要完成了4项操作,具体如下。
○ 将JSON对象转成字符串body。
○ 设置statusCode。
○ 将Content-Type设置为application/json。
○ 将字符串body通过res.end方法发送给浏览器。
➘ 原始的req和res
对于ctx.request和ctx.response没有实现的功能,可以通过扩展ctx.req和ctx.res来实现。ctx.req和ctx.res是更低级别的API,是http.createServer的回调函数传进来的参数,通过它们可以实现更多功能。比如koa-bigpipe中间件就是通过原始的res.write来实现BigPipe分块写入功能的,代码如下。

http模块的res.write()默认是支持分块传输的,所以,遵循Koa中间件约定并提供ctx.write和ctx.end方法,能够更好地以Koa的方式来编写BigPipe,代码如下。

➘ 与浏览器端交互
Koa框架与浏览器交互的方式主要是让服务器对浏览器进行响应,可用方法如下。
○ ctx.body(Koa内置)
○ ctx.redirect(Koa内置)
○ ctx.render(外部中间件)
1.ctx.body
ctx.body能够以最精简的代码实现最多的功能。
○ 返回文本:ctx.body='Hello Koa 2!'。
○ 返回HTML付出:ctx.body='<h1>Hello Koa 2!<h1>'。
○ 返回JSON,代码如下。

ctx.body的工作原理是根据赋值类型来进行不同Content-Type的处理,处理过程分为如下两步。
○ 根据body的类型设置对应的Content-Type。
○ 根据Content-Type调用res.write或res.end,将数据写入浏览器。
在Koa源码lib/response.js文件里,body的赋值方法如下。


上面代码的要点如下。
○ 判断Content-Type是否为空,如果是,则不返回任何结果。
○ 判断Content-Type是否为字符串,字符串又分为Content-Type=—text/html”和Content-Type=—text/plain”两种类型,对应的Content-Type也不一样。
○ 判断Content-Type是否是Buffer或Stream类型。
○ 如果Content-Type不是以上任何类型,那么就应该是JSON对象。
这种通过typeof方法来判定Content-Type的类型,进而决定具体操作的技巧是非常实用的,笔者在进行apie模块约定的时候就曾这样做。另外对于编写脚手架来识别JSON字段类型,也可以按照这种方法来实现。
2.ctx.redirect
浏览器重定向只有两种情况,向前重定向和向后重定向,代码如下。

3.ctx.render
ctx.render是渲染模板使用的方法,示例如下。

ctx.render有两个参数:模板和数据。该方法主要用于将模板编译成HTML并写入浏览器,后面会详细介绍。