上下文对象

在 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 为例

  1. request.js
const url = require('url')

module.exports = {
  get query() {
    return url.parse(this.request.url, true).query
  }
}
  1. 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 )
  1. 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 并写入浏览器。

你可能感兴趣的:(上下文对象)