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