Node.js躬行記(20)——KOA源碼分析(下)

  在上一篇中,主要分析了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)
  })
})

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章