在 Web 应用中,Koa 的上下文对象 ctx 是一次完整的 HTTP 请求上下文,贯穿这个请求的生命周期。请求会经过 N(N>0)层中间件的拦截,唯一共享的就是这个上下问对象。
koa v2 的上下文是以参数形式存在的 ctx 对象。代码如下:
app.use(function * () {
let ctx = this // 上下文对象
this.request // request 对象
this.response // response 对象
})
每个请求至少贯穿一个中间件,ctx 在整个中间件流转过程中一直存在。
ctx 的生命周期是贯穿整个 HTTP 请求过程的。在 ctx 上绑定内容并不是一个好的做法,但适当的在 ctx 上下文中绑定某些内容是必要的,这能能够更方便的实现业务逻辑,常见的有日志中间件和 ctx.render() 函数 等。
源码分析
ctx 上常用的对象有 request,response,res,req 等,其中 request 和 response 是 Koa 内置的对象,是对 HTTP 的使用扩展;而 req 和 res 是在 http.createServer 回调函数里注入的,即未经加工的原生内置对象。
根据前面的介绍我们知道,Koa 提供的中间件机制是对 http.createServer 回调进行抽象,看下面的例子:
const http = require('http')
const Koa = require('koa')
// 响应
app.use(async ctx => {
ctx.body = 'hello world'
})
const server = http.createServer(app.callback())
server.listen(3000)
app 里被注入了 http.createServer 的回调函数,核心是 app.callback() 实现。下面是 callback 的实现:
callback() {
const fn = compose(this.middleware)
if(!this.listeners('error').length) {
this.on('error', this.onerror)
}
const handleRequest = (req,res) => {
res.statusCode = '404'
const ctx = this,createContext(req,res)
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
onFinished(res, onerror)
return fn(ctx).then(handleResponse).catch(onerror)
}
reurn handleRequest;
}
其中 const ctx = this.createContext(req,res) 表示进行了绑定,看下面的示例:
createContext(req,res){
const context = Object.create(this.context)
const request = context.request = Object.create(this.request)
const response = context.response = Object.create(this.response)
context.app = request.app = response.app = this
context.res = request.res = response.res = res
context.req = request.req = response.req = req
request.ctx = response.ctx = context
request.response = response
response.request = reuest
context.originalUrl = request.originalUrl = req.url
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
})
request.ip = request.ips[0] || req.socket.remoteAddress || ''
context.accpet = request.accpet = accpets(req)
context.state = {}
return context
}
上面代码中的要点如下:
- 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 = {} 约定了一个中间件的公用储存空间,可以储存一些数据。比如用户数据。
request 对象和 response 对象
在 Koa 应用里,最常用的就是 request 对象和 response 对象。为了使用方便,许多上下文属性和方法都被委托到了 Koa 的 ctx.request 或 ctx.response 上。按照职责划分,与请求相关的方法都被放到 ctx.request 中,与响应相关的方法都被挂载在 ctx.response 上。
req 和 res 的继承关系如下
req => IncomingMesage
res => ServerResponse => OutgoingMessage
基于以上关系,想要对 Stream 进行扩展就非常简单。
对于 request.js ,采用 Koa 扩展方法为例;对于 response.js 以 Express 为例
- request.js
const url = require('url')
module.exports = {
get query() {
return url.parse(this.request.url, true).query
}
}
- response.js
requestListener 上没有 res.json 方法,但是为了方便,Express 和 Koa 均提供了 json 方法来快速返回 JSON API。
下面以 Express 为例,
const http = require('http')
let res = Object.create(http.ServerResponse.prototype)
res.json = function json(obj) {
let body = JSON.stringify(obj)
this.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'application/json'
})
return this.end(body)
}
module.exports = res
原始的 req 和 res
对于 ctx.request 和 ctx.response 没有实现的功能,可以通过扩展 ctx.req 和 ctx.res 来实现。(参考 koa-bigpipe)
与浏览器端交互
Koa 框架与浏览器交互的方式主要是让服务器对浏览器进行响应,可用方法如下:
- ctx.body (Koa 内置)
- ctx.redirect ( Koa 内置 )
- ctx.render ( 外部中间件 koa-views )
- ctx.body
ctx.body 能够以最精简的代码实现最多的功能。
- 返回文本:ctx.body = 'hello world'
- 返回 HTML : ctx.body = '
hello world
' - 返回 JSON,代码如下:
ctx.body = {a:1}
ctx.body 的工作原理是根据赋值类型的不同 Content-type 的处理,处理过程分为以下两步。
- 根据 body 的类型设置对应的 Content-type。
- 根据 Content-type 调用 res.write 或者 res.end,将数据写入浏览器。
下面是 Koa 源码里的实现:
set body(val) {
const original = this._body;
this._body = val;
if (this.res.headrsSend) return;
// Content-type 为空的情况
if(null == val) {
if(!statuses.empty(this.status)) this.status = 204
this.remove('Content-type')
this.remove('Content-Length')
this.remove('Transfer-Encoding')
return ;
}
// 设置状态码
if(!this._eplicitStatus) this.status = 200
// 设置 Content-type
const setType = !this.header['content-type']
// 判断是否为字符串
if('string' === typeof val) {
if (setType) this.type = /^\s* this.ctx.onerror(err))
if(null != original && original != val) this.remove('Content-Length')
if(setType) this.type = 'bin'
return ;
}
// Content-type 默认为 JSON 对象
this.remove('Content-Length')
this.type = 'json'
}
上面的代码要点如下:
- 判断 Content-Type 是否为空的,如果是,则不返回任何结果
- 判断 Content-Type 是否为字符串,字符串又分为 Content-Type = 'text/html' 和 Content-Type = 'text/plain' 两种类型,对应 Content-Type 不一样。
- 判断 Content-Type 是否是 Buffer 或 Stream 类型。
- 如果 Content-Type 不是以上任何类型,那么就是 JSON 对象。
2. ctx.redirect
浏览器重定向只有两种情况,向前重定向和向后重定向,代码如下:
// 向后重定向
ctx.redirect('back')
ctx.redirect('back', './index.html')
// 向前重定向
ctx.redirect('/login')
ctx.redirect('http://google.com')
3. ctx.render
ctx.render 是渲染模板使用的方法,示例如下:
router.get('/', async(ctx, next) => {
await ctx.render('index', {
title: 'hello world'
})
})
ctx.render 有两个参数:模板和数据。该方法主要用于将模板编译成 HTML 并写入浏览器。