strick

  在上一篇中,主要分析了package.json和application.js文件,本文会分析剩下的几个文件。

一、context.js

  在context.js中,会处理错误,cookie,JSON格式化等。

1)cookie

  在处理cookie时,创建了一个Symbol类型的key,注意Symbol具有唯一性的特点,即 Symbol('context#cookies') === Symbol('context#cookies') 得到的是 false。

const Cookies = require('cookies')
const COOKIES = Symbol('context#cookies')

const proto = module.exports = {
  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
  }
}

  get在读取cookie,会初始化Cookies实例。

2)错误

  在默认的错误处理函数中,会配置头信息,触发错误事件,配置响应码等。

  onerror (err) {
    // 可以绕过KOA的错误处理
    if (err == null) return

    // 在处理跨全局变量时,正常的“instanceof”检查无法正常工作
    // See https://github.com/koajs/koa/issues/1466
    // 一旦jest修复,可能会删除它 https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err))

    let headerSent = false
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true
    }

    // 触发error事件,在application.js中创建过监听器
    this.app.emit('error', err, this)

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return
    }

    const { res } = this

    // 首先取消设置所有header
    /* istanbul ignore else */
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name))
    } else {
      res._headers = {} // Node < 7.7
    }

    // 然后设置那些指定的
    this.set(err.headers)

    // 强制 text/plain
    this.type = 'text'

    let statusCode = err.status || err.statusCode

    // ENOENT support
    if (err.code === 'ENOENT') statusCode = 404

    // default to 500
    if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500

    // 响应数据
    const code = statuses[statusCode]
    const msg = err.expose ? err.message : code
    this.status = err.status = statusCode
    this.length = Buffer.byteLength(msg)
    res.end(msg)
  },

3)属性委托

  在package.json中依赖了一个名为delegates的库,看下面这个示例。

  request是context的一个属性,在调delegate(context, 'request')函数后,就能直接context.querystring这么调用了。

const delegate = require('delegates')
const context = {
  request: {
    querystring: 'a=1&b=2'
  }
}
delegate(context, 'request')
  .access('querystring')
console.log(context.querystring)// a=1&b=2

  在KOA中,ctx可以直接调用request与response的属性和方法就是通过delegates实现的。

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  ...

二、request.js和response.js

  request.js和response.js就是为Node原生的req和res做一层封装。在request.js中都是些HTTP首部、IP、URL、缓存等。

  /**
   * 获取 WHATWG 解析的 URL,并缓存起来
   */
  get URL () {
    /* istanbul ignore else */
    if (!this.memoizedURL) {
      const originalUrl = this.originalUrl || '' // avoid undefined in template string
      try {
        this.memoizedURL = new URL(`${this.origin}${originalUrl}`)
      } catch (err) {
        this.memoizedURL = Object.create(null)
      }
    }
    return this.memoizedURL
  },
  /**
   * 检查请求是否新鲜(有缓存),也就是
   * If-Modified-Since/Last-Modified 和 If-None-Match/ETag 是否仍然匹配
   */
  get fresh () {
    const method = this.method
    const s = this.ctx.status

    // GET or HEAD for weak freshness validation only
    if (method !== 'GET' && method !== 'HEAD') return false

    // 2xx or 304 as per rfc2616 14.26
    if ((s >= 200 && s < 300) || s === 304) {
      return fresh(this.header, this.response.header)
    }

    return false
  },

  response.js要复杂一点,会配置状态码、响应正文、读取解析的响应内容长度、302重定向等。

  /**
   * 302 重定向
   * Examples:
   *    this.redirect('back');
   *    this.redirect('back', '/index.html');
   *    this.redirect('/login');
   *    this.redirect('http://google.com');
   */
  redirect (url, alt) {
    // location
    if (url === 'back') url = this.ctx.get('Referrer') || alt || '/'
    this.set('Location', encodeUrl(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}.`
  },
  /**
   * 设置响应正文
   */
  set body (val) {
    const original = this._body
    this._body = val

    // no content
    if (val == null) {
      if (!statuses.empty[this.status]) {
        if (this.type === 'application/json') {
          this._body = 'null'
          return
        }
        this.status = 204
      }
      if (val === null) this._explicitNullBody = true
      this.remove('Content-Type')
      this.remove('Content-Length')
      this.remove('Transfer-Encoding')
      return
    }

    // set the status
    if (!this._explicitStatus) this.status = 200

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type')

    // string
    if (typeof val === 'string') {
      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 (val instanceof Stream) {
      onFinish(this.res, destroy.bind(null, val))
      if (original !== val) {
        val.once('error', err => this.ctx.onerror(err))
        // overwriting
        if (original != null) this.remove('Content-Length')
      }

      if (setType) this.type = 'bin'
      return
    }

    // json
    this.remove('Content-Length')
    this.type = 'json'
  },

三、单元测试

  KOA的单元测试做的很细致,每个方法和属性都给出了相应的单测,它内部的写法很容易单测,非常值得借鉴。

  采用的单元测试框架是Jest,HTTP请求的测试库是SuperTest。断言使用了Node默认提供的assert模块。

const request = require('supertest')
const assert = require('assert')
const Koa = require('../..')
// request.js
describe('app.request', () => {
  const app1 = new Koa()
  // 声明request的message属性
  app1.request.message = 'hello'

  it('should merge properties', () => {
    app1.use((ctx, next) => {
      // 判断ctx中的request.message是否就是刚刚赋的那个值
      assert.strictEqual(ctx.request.message, 'hello')
      ctx.status = 204
    })
    // 发起GET请求,地址是首页,期待响应码是204
    return request(app1.listen())
      .get('/')
      .expect(204)
  })
})

 

相关文章: