在 Web 應(yīng)用中,Koa 的上下文對(duì)象 ctx 是一次完整的 HTTP 請(qǐng)求上下文,貫穿這個(gè)請(qǐng)求的生命周期。請(qǐng)求會(huì)經(jīng)過(guò) N(N>0)層中間件的攔截,唯一共享的就是這個(gè)上下問(wèn)對(duì)象。
koa v2 的上下文是以參數(shù)形式存在的 ctx 對(duì)象。代碼如下:
app.use(function * () {
let ctx = this // 上下文對(duì)象
this.request // request 對(duì)象
this.response // response 對(duì)象
})
每個(gè)請(qǐng)求至少貫穿一個(gè)中間件,ctx 在整個(gè)中間件流轉(zhuǎn)過(guò)程中一直存在。
ctx 的生命周期是貫穿整個(gè) HTTP 請(qǐng)求過(guò)程的。在 ctx 上綁定內(nèi)容并不是一個(gè)好的做法,但適當(dāng)?shù)脑?ctx 上下文中綁定某些內(nèi)容是必要的,這能能夠更方便的實(shí)現(xiàn)業(yè)務(wù)邏輯,常見(jiàn)的有日志中間件和 ctx.render() 函數(shù) 等。
源碼分析
ctx 上常用的對(duì)象有 request,response,res,req 等,其中 request 和 response 是 Koa 內(nèi)置的對(duì)象,是對(duì) HTTP 的使用擴(kuò)展;而 req 和 res 是在 http.createServer 回調(diào)函數(shù)里注入的,即未經(jīng)加工的原生內(nèi)置對(duì)象。
根據(jù)前面的介紹我們知道,Koa 提供的中間件機(jī)制是對(duì) http.createServer 回調(diào)進(jìn)行抽象,看下面的例子:
const http = require('http')
const Koa = require('koa')
// 響應(yīng)
app.use(async ctx => {
ctx.body = 'hello world'
})
const server = http.createServer(app.callback())
server.listen(3000)
app 里被注入了 http.createServer 的回調(diào)函數(shù),核心是 app.callback() 實(shí)現(xiàn)。下面是 callback 的實(shí)現(xiàn):
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) 表示進(jìn)行了綁定,看下面的示例:
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
}
上面代碼中的要點(diǎn)如下:
- context.request = Object.create(this.request) 是 Koa 內(nèi)置的 request 對(duì)象
- context.response = Object.create(this.response) 是 Koa 內(nèi)置的 response 對(duì)象
- context.app = request.app = response.app = this 是 app 自身。
- context.req 是原始 HTTP 回調(diào)函數(shù)里的 req 對(duì)象
- context.res 是原始 HTTP 回調(diào)函數(shù)里的 res 對(duì)象
- context.originalUrl 是最初的 URL 地址
- context.cookies 是瀏覽器 Cookie 封裝
- context.state = {} 約定了一個(gè)中間件的公用儲(chǔ)存空間,可以儲(chǔ)存一些數(shù)據(jù)。比如用戶數(shù)據(jù)。
request 對(duì)象和 response 對(duì)象
在 Koa 應(yīng)用里,最常用的就是 request 對(duì)象和 response 對(duì)象。為了使用方便,許多上下文屬性和方法都被委托到了 Koa 的 ctx.request 或 ctx.response 上。按照職責(zé)劃分,與請(qǐng)求相關(guān)的方法都被放到 ctx.request 中,與響應(yīng)相關(guān)的方法都被掛載在 ctx.response 上。
req 和 res 的繼承關(guān)系如下
req => IncomingMesage
res => ServerResponse => OutgoingMessage
基于以上關(guān)系,想要對(duì) Stream 進(jìn)行擴(kuò)展就非常簡(jiǎn)單。
對(duì)于 request.js ,采用 Koa 擴(kuò)展方法為例;對(duì)于 response.js 以 Express 為例
- request.js
const url = require('url')
module.exports = {
get query() {
return url.parse(this.request.url, true).query
}
}
- response.js
requestListener 上沒(méi)有 res.json 方法,但是為了方便,Express 和 Koa 均提供了 json 方法來(lái)快速返回 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
對(duì)于 ctx.request 和 ctx.response 沒(méi)有實(shí)現(xiàn)的功能,可以通過(guò)擴(kuò)展 ctx.req 和 ctx.res 來(lái)實(shí)現(xiàn)。(參考 koa-bigpipe)
與瀏覽器端交互
Koa 框架與瀏覽器交互的方式主要是讓服務(wù)器對(duì)瀏覽器進(jìn)行響應(yīng),可用方法如下:
- ctx.body (Koa 內(nèi)置)
- ctx.redirect ( Koa 內(nèi)置 )
- ctx.render ( 外部中間件 koa-views )
- ctx.body
ctx.body 能夠以最精簡(jiǎn)的代碼實(shí)現(xiàn)最多的功能。
- 返回文本:ctx.body = 'hello world'
- 返回 HTML : ctx.body = '<h1>hello world</h1>'
- 返回 JSON,代碼如下:
ctx.body = {a:1}
ctx.body 的工作原理是根據(jù)賦值類型的不同 Content-type 的處理,處理過(guò)程分為以下兩步。
- 根據(jù) body 的類型設(shè)置對(duì)應(yīng)的 Content-type。
- 根據(jù) Content-type 調(diào)用 res.write 或者 res.end,將數(shù)據(jù)寫入瀏覽器。
下面是 Koa 源碼里的實(shí)現(xiàn):
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 ;
}
// 設(shè)置狀態(tài)碼
if(!this._eplicitStatus) this.status = 200
// 設(shè)置 Content-type
const setType = !this.header['content-type']
// 判斷是否為字符串
if('string' === typeof val) {
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
this.length = Buffer.byteLength(val)
return
}
// 判斷是否為 Buffer
if(Buffer.isBuffer(val)) {
if(setType) this.type = 'bin'
this.length = val.length
return
}
// 判斷是否是 Stream
if('function' == typeof val.pipe) {
onFinish(this.res, destory.bind(null, val))
ensureErrorHandle(val, err => this.ctx.onerror(err))
if(null != original && original != val) this.remove('Content-Length')
if(setType) this.type = 'bin'
return ;
}
// Content-type 默認(rèn)為 JSON 對(duì)象
this.remove('Content-Length')
this.type = 'json'
}
上面的代碼要點(diǎn)如下:
- 判斷 Content-Type 是否為空的,如果是,則不返回任何結(jié)果
- 判斷 Content-Type 是否為字符串,字符串又分為 Content-Type = 'text/html' 和 Content-Type = 'text/plain' 兩種類型,對(duì)應(yīng) Content-Type 不一樣。
- 判斷 Content-Type 是否是 Buffer 或 Stream 類型。
- 如果 Content-Type 不是以上任何類型,那么就是 JSON 對(duì)象。
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 有兩個(gè)參數(shù):模板和數(shù)據(jù)。該方法主要用于將模板編譯成 HTML 并寫入瀏覽器。