在 Koa2 源码解读之 Application 对象 一文中,我们对 Application 对象进行了解读。在Application 对象的构造函数中,通过 Object.create() 创建了一个 context 对象,并挂载到 Application 的 context 属性下:
constructor() {// ...this.context = Object.create(context);// ...}
在中间件执行的时候,还会对这个 context 进行包装,然后把包装后的 context 对象作为中间件函数的第一个函数进行传入,因此我们可以通过中间件函数的第一个参数来调用这个 context 对象。
callback() {// ...// 在 createContext() 函数中对 this.context 进行包装const ctx = this.createContext(req, res);// 在 handleRequest() 函数中把包装后的 context 对象作为中间件函数的第一个函数传入return this.handleRequest(ctx, fn);}
在中间件中使用这个context 对象:
const Koa = require('koa');const app = new Koa();app.use(async (ctx, next) => {// 在中间件中使用 ctx(context 对象)return new Promise((resolve, reject) => {setTimeout(() => {ctx.body = 'hello';resolve();}, 1000);});});app.listen(3000);
在 Application 中的这个 context 对象,就来源于 lib/context.js ,它提供了 toJson、inspect、throw、onerror、get cookies()、set cookies() 等方法,并对 Request 和 Response 对象做了代理访问。
错误处理 onerror
/*** Default error handling.** @param {Error} err* @api private*/onerror(err) {// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if (null == err) return;// When dealing with cross-globals a normal `instanceof` check doesn't work properly.// See https://github.com/koajs/koa/issues/1466// We can probably remove it once jest fixes 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;}// delegate// emit 这个错误,在 Application 中通过 on 监听错误:this.on('error', this.onerror) ,// 这里通过 emit 将错误转交给application的onerror处理。这里可以做到emit、on来发布订阅错误就是因为application继承了Emitter模块。this.app.emit('error', err, this);// nothing we can do here other// than delegate to the app-level// handler and log.if (headerSent) { // 已经发送了一个响应头,则直接return,不再往下执行return;}const { res } = this;// first unset all headers/* istanbul ignore else */if (typeof res.getHeaderNames === 'function') { // headerNames 为 function,则删除 res 上的所有 Headerres.getHeaderNames().forEach(name => res.removeHeader(name));} else {res._headers = {}; // Node < 7.7}// 下面的处理就是对这个错误的ctx进行修改。如header设置成err.headers、statusCode设置成err.status ,msg设置为如err.message等等// then set those specifiedthis.set(err.headers);// force text/plainthis.type = 'text';let statusCode = err.status || err.statusCode;// ENOENT supportif ('ENOENT' === err.code) statusCode = 404;// default to 500if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;// respondconst code = statuses[statusCode];const msg = err.expose ? err.message : code;this.status = err.status = statusCode;this.length = Buffer.byteLength(msg);res.end(msg);},
在 context 中,当发生了 error 时,对挂载在 context 对象上的 response 进行处理,并把 error emit 给 Application 处理。
// emit 这个错误,在 Application 中通过 on 监听错误:this.on('error', this.onerror) ,// 这里通过 emit 将错误转交给application的onerror处理。这里可以做到emit、on来发布订阅错误就是因为application继承了Emitter模块。this.app.emit('error', err, this);
在 Application 的 callback() 中,通过 on 监听 error 事件,并调用了 Application 的 onerror 方法来处理错误:
callback() {// ...if (!this.listenerCount('error')) this.on('error', this.onerror);// ...}
我们看看 Application 是怎么处理错误的:
/*** Default error handler.** @param {Error} err* @api private*/onerror(err) {// When dealing with cross-globals a normal `instanceof` check doesn't work properly.// See https://github.com/koajs/koa/issues/1466// We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.const isNativeError =Object.prototype.toString.call(err) === '[object Error]' ||err instanceof Error;if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));if (404 === err.status || err.expose) return;if (this.silent) return;const msg = err.stack || err.toString();console.error(`\n${msg.replace(/^/gm, ' ')}\n`);}
可以看到,在 onerror 中,只是把 error 打印了出来。但是对于中间件内部的错误,koa 是无法捕捉的(除非转同步),我们的应用如果需要记录这个错误,可以使用 node 的 process 监听:
process.on("unhandledRejection", (err) => {console.log(err);});
处理cookie
在 Koa 中,cookie 的存取是被放在 context 中的:
// 获取 cookies,如果已经有 cookie,则直接返回,如果没有,则生成cookieget cookies() {if (!this[COOKIES]) {this[COOKIES] = new Cookies(this.req, this.res, {keys: this.app.keys,secure: this.request.secure});}return this[COOKIES];},// 设置 cookieset cookies(_cookies) {this[COOKIES] = _cookies;}};
Koa 中的委托模式
为了方便对 Request 和 Response 对象进行操作,Koa 通过 delegate 对 Context 进行了代理访问处理,也就是将 Request 和 Response 挂载到了 context 对象上,使得可以通过 Context 即可操作对应的 Request 和 Response :
/*** Response delegation.*/// 委托响应delegate(proto, 'response').method('attachment').method('redirect').method('remove').method('vary').method('has').method('set').method('append').method('flushHeaders').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/*** Request delegation.*/// 委托请求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').access('query').access('path').access('url').access('accept').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('URL').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');
可以看到,Koa 使用 delegate 这个函数将 原本属于 Request 和 Response 的属性或方法代理到了 context 对象上,下面我们来看看 delegate 的源码。
delegates ,可以帮我们方便快捷地使用设计模式当中的委托模式(Delegation Pattern),即外层暴露的对象将请求委托给内部的其他对象进行处理。
初始化
function Delegator(proto, target) {if (!(this instanceof Delegator)) return new Delegator(proto, target);this.proto = proto;this.target = target;this.methods = [];this.getters = [];this.setters = [];this.fluents = [];}
在初始化函数 Delegate 中,首先判断当前的 this 是否是 Delegate 的实例,如果不是,就调用 new Delegate(proto, target) 进行实例化。通过这种方式,可以避免在调用初始化函数时忘记写 new 造成的问题。因此下面的两种写法是等价的:
let d = new Delegator(proto, target);let d = Delegator(proto, target);
在初始化函数中,还定义了 methods、getters、setters、fluents 等属性,用于记录委托了哪些属性和方法。
method
Delegator.prototype.method = function(name){var proto = this.proto;var target = this.target;this.methods.push(name);proto[name] = function(){return this[target][name].apply(this[target], arguments);};return this;};
在 method 中,将需要代理的方法存入 methods 中记录起来,然后通过 apply 方法将当前代理的方法的 this 对象指向Delegate的内部对象 this[target],从而确保在执行 this[target][name] 时,函数体内的 this 是指向对应的内部对象。
getter
Delegator.prototype.getter = function(name){var proto = this.proto;var target = this.target;this.getters.push(name);proto.__defineGetter__(name, function(){return this[target][name];});return this;};
getter 的实现也十分简单,在 getter 中,也是一样首先将需要代理的属性存储到 getters 中记录起来,然后通过 __defineGetter__ 劫持 proto 的get,转而去访问 target 。__defineGetter__ 可以将一个函数绑定在当前对象的指定属性上,当那个属性的值被调用时,绑定在该属性上的函数就会被调用。该特性已经从 Web 标准中删除,建议通过 Proxy 或 Object.defineProperty 进行劫持。(Vue2 使用Object.defineProperty进行数据劫持,从而实现数据响应式,而Vue3则抛弃了Object.defineProperty,使用 Proxy 劫持数据,实现数据的响应式。)
setter
Delegator.prototype.setter = function(name){var proto = this.proto;var target = this.target;this.setters.push(name);proto.__defineSetter__(name, function(val){return this[target][name] = val;});return this;};
与 getter 的实现不同的是,setter 使用 __defineSetter__ 为已存在的对象添加可读属性。该特性也已经从 Web 标准中删除,建议使用 Proxy 或 Object.defineProperty 实现同样的功能。
access
Delegator.prototype.access = function(name){return this.getter(name).setter(name);};
access 则通过直接调用 Delegate 对象的 getter 和 setter,从而实现对属性的 getter 和 setter 的代理。
Koa 在 context 中将 Request 和 Response 的属性和方法代理到了 context 对象上,我们来看看 request.js 和 response.js 的源码:
request.js
'use strict';/*** Module dependencies.*/const URL = require('url').URL;const net = require('net');const accepts = require('accepts');const contentType = require('content-type');const stringify = require('url').format;const parse = require('parseurl');const qs = require('querystring');const typeis = require('type-is');const fresh = require('fresh');const only = require('only');const util = require('util');const IP = Symbol('context#ip');/*** Prototype.*/module.exports = {/*** Return request header.** @return {Object}* @api public*/get header() {return this.req.headers;},/*** Set request header.** @api public*/set header(val) {this.req.headers = val;},/*** Return request header, alias as request.header** @return {Object}* @api public*/get headers() {return this.req.headers;},/*** Set request header, alias as request.header** @api public*/set headers(val) {this.req.headers = val;},/*** Get request URL.** @return {String}* @api public*/get url() {return this.req.url;},/*** Set request URL.** @api public*/set url(val) {this.req.url = val;},/*** Get origin of URL.** @return {String}* @api public*/get origin() {return `${this.protocol}://${this.host}`;},/*** Get full request URL.** @return {String}* @api public*/get href() {// support: `GET http://example.com/foo`if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;return this.origin + this.originalUrl;},/*** Get request method.** @return {String}* @api public*/get method() {return this.req.method;},/*** Set request method.** @param {String} val* @api public*/set method(val) {this.req.method = val;},/*** Get request pathname.** @return {String}* @api public*/get path() {return parse(this.req).pathname;},/*** Set pathname, retaining the query string when present.** @param {String} path* @api public*/set path(path) {const url = parse(this.req);if (url.pathname === path) return;url.pathname = path;url.path = null;this.url = stringify(url);},/*** Get parsed query string.** @return {Object}* @api public*/get query() {const str = this.querystring;const c = this._querycache = this._querycache || {};return c[str] || (c[str] = qs.parse(str));},/*** Set query string as an object.** @param {Object} obj* @api public*/set query(obj) {this.querystring = qs.stringify(obj);},/*** Get query string.** @return {String}* @api public*/get querystring() {if (!this.req) return '';return parse(this.req).query || '';},/*** Set query string.** @param {String} str* @api public*/set querystring(str) {const url = parse(this.req);if (url.search === `?${str}`) return;url.search = str;url.path = null;this.url = stringify(url);},/*** Get the search string. Same as the query string* except it includes the leading ?.** @return {String}* @api public*/get search() {if (!this.querystring) return '';return `?${this.querystring}`;},/*** Set the search string. Same as* request.querystring= but included for ubiquity.** @param {String} str* @api public*/set search(str) {this.querystring = str;},/*** Parse the "Host" header field host* and support X-Forwarded-Host when a* proxy is enabled.** @return {String} hostname:port* @api public*/get host() {const proxy = this.app.proxy;let host = proxy && this.get('X-Forwarded-Host');if (!host) {if (this.req.httpVersionMajor >= 2) host = this.get(':authority');if (!host) host = this.get('Host');}if (!host) return '';return host.split(/\s*,\s*/, 1)[0];},/*** Parse the "Host" header field hostname* and support X-Forwarded-Host when a* proxy is enabled.** @return {String} hostname* @api public*/get hostname() {const host = this.host;if (!host) return '';if ('[' === host[0]) return this.URL.hostname || ''; // IPv6return host.split(':', 1)[0];},/*** Get WHATWG parsed URL.* Lazily memoized.** @return {URL|Object}* @api public*/get URL() {/* istanbul ignore else */if (!this.memoizedURL) {const originalUrl = this.originalUrl || ''; // avoid undefined in template stringtry {this.memoizedURL = new URL(`${this.origin}${originalUrl}`);} catch (err) {this.memoizedURL = Object.create(null);}}return this.memoizedURL;},/*** Check if the request is fresh, aka* Last-Modified and/or the ETag* still match.** @return {Boolean}* @api public*/get fresh() {const method = this.method;const s = this.ctx.status;// GET or HEAD for weak freshness validation onlyif ('GET' !== method && 'HEAD' !== method) return false;// 2xx or 304 as per rfc2616 14.26if ((s >= 200 && s < 300) || 304 === s) {return fresh(this.header, this.response.header);}return false;},/*** Check if the request is stale, aka* "Last-Modified" and / or the "ETag" for the* resource has changed.** @return {Boolean}* @api public*/get stale() {return !this.fresh;},/*** Check if the request is idempotent.** @return {Boolean}* @api public*/get idempotent() {const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];return !!~methods.indexOf(this.method);},/*** Return the request socket.** @return {Connection}* @api public*/get socket() {return this.req.socket;},/*** Get the charset when present or undefined.** @return {String}* @api public*/get charset() {try {const { parameters } = contentType.parse(this.req);return parameters.charset || '';} catch (e) {return '';}},/*** Return parsed Content-Length when present.** @return {Number}* @api public*/get length() {const len = this.get('Content-Length');if (len === '') return;return ~~len;},/*** Return the protocol string "http" or "https"* when requested with TLS. When the proxy setting* is enabled the "X-Forwarded-Proto" header* field will be trusted. If you're running behind* a reverse proxy that supplies https for you this* may be enabled.** @return {String}* @api public*/get protocol() {if (this.socket.encrypted) return 'https';if (!this.app.proxy) return 'http';const proto = this.get('X-Forwarded-Proto');return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http';},/*** Shorthand for:** this.protocol == 'https'** @return {Boolean}* @api public*/get secure() {return 'https' === this.protocol;},/*** When `app.proxy` is `true`, parse* the "X-Forwarded-For" ip address list.** For example if the value was "client, proxy1, proxy2"* you would receive the array `["client", "proxy1", "proxy2"]`* where "proxy2" is the furthest down-stream.** @return {Array}* @api public*/get ips() {const proxy = this.app.proxy;const val = this.get(this.app.proxyIpHeader);let ips = proxy && val? val.split(/\s*,\s*/): [];if (this.app.maxIpsCount > 0) {ips = ips.slice(-this.app.maxIpsCount);}return ips;},/*** Return request's remote address* When `app.proxy` is `true`, parse* the "X-Forwarded-For" ip address list and return the first one** @return {String}* @api public*/get ip() {if (!this[IP]) {this[IP] = this.ips[0] || this.socket.remoteAddress || '';}return this[IP];},set ip(_ip) {this[IP] = _ip;},/*** Return subdomains as an array.** Subdomains are the dot-separated parts of the host before the main domain* of the app. By default, the domain of the app is assumed to be the last two* parts of the host. This can be changed by setting `app.subdomainOffset`.** For example, if the domain is "tobi.ferrets.example.com":* If `app.subdomainOffset` is not set, this.subdomains is* `["ferrets", "tobi"]`.* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.** @return {Array}* @api public*/get subdomains() {const offset = this.app.subdomainOffset;const hostname = this.hostname;if (net.isIP(hostname)) return [];return hostname.split('.').reverse().slice(offset);},/*** Get accept object.* Lazily memoized.** @return {Object}* @api private*/get accept() {return this._accept || (this._accept = accepts(this.req));},/*** Set accept object.** @param {Object}* @api private*/set accept(obj) {this._accept = obj;},/*** Check if the given `type(s)` is acceptable, returning* the best match when true, otherwise `false`, in which* case you should respond with 406 "Not Acceptable".** The `type` value may be a single mime type string* such as "application/json", the extension name* such as "json" or an array `["json", "html", "text/plain"]`. When a list* or array is given the _best_ match, if any is returned.** Examples:** // Accept: text/html* this.accepts('html');* // => "html"** // Accept: text/*, application/json* this.accepts('html');* // => "html"* this.accepts('text/html');* // => "text/html"* this.accepts('json', 'text');* // => "json"* this.accepts('application/json');* // => "application/json"** // Accept: text/*, application/json* this.accepts('image/png');* this.accepts('png');* // => false** // Accept: text/*;q=.5, application/json* this.accepts(['html', 'json']);* this.accepts('html', 'json');* // => "json"** @param {String|Array} type(s)...* @return {String|Array|false}* @api public*/accepts(...args) {return this.accept.types(...args);},/*** Return accepted encodings or best fit based on `encodings`.** Given `Accept-Encoding: gzip, deflate`* an array sorted by quality is returned:** ['gzip', 'deflate']** @param {String|Array} encoding(s)...* @return {String|Array}* @api public*/acceptsEncodings(...args) {return this.accept.encodings(...args);},/*** Return accepted charsets or best fit based on `charsets`.** Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`* an array sorted by quality is returned:** ['utf-8', 'utf-7', 'iso-8859-1']** @param {String|Array} charset(s)...* @return {String|Array}* @api public*/acceptsCharsets(...args) {return this.accept.charsets(...args);},/*** Return accepted languages or best fit based on `langs`.** Given `Accept-Language: en;q=0.8, es, pt`* an array sorted by quality is returned:** ['es', 'pt', 'en']** @param {String|Array} lang(s)...* @return {Array|String}* @api public*/acceptsLanguages(...args) {return this.accept.languages(...args);},/*** Check if the incoming request contains the "Content-Type"* header field and if it contains any of the given mime `type`s.* If there is no request body, `null` is returned.* If there is no content type, `false` is returned.* Otherwise, it returns the first `type` that matches.** Examples:** // With Content-Type: text/html; charset=utf-8* this.is('html'); // => 'html'* this.is('text/html'); // => 'text/html'* this.is('text/*', 'application/json'); // => 'text/html'** // When Content-Type is application/json* this.is('json', 'urlencoded'); // => 'json'* this.is('application/json'); // => 'application/json'* this.is('html', 'application/*'); // => 'application/json'** this.is('html'); // => false** @param {String|String[]} [type]* @param {String[]} [types]* @return {String|false|null}* @api public*/is(type, ...types) {return typeis(this.req, type, ...types);},/*** Return the request mime type void of* parameters such as "charset".** @return {String}* @api public*/get type() {const type = this.get('Content-Type');if (!type) return '';return type.split(';')[0];},/*** Return request header.** The `Referrer` header field is special-cased,* both `Referrer` and `Referer` are interchangeable.** Examples:** this.get('Content-Type');* // => "text/plain"** this.get('content-type');* // => "text/plain"** this.get('Something');* // => ''** @param {String} field* @return {String}* @api public*/get(field) {const req = this.req;switch (field = field.toLowerCase()) {case 'referer':case 'referrer':return req.headers.referrer || req.headers.referer || '';default:return req.headers[field] || '';}},/*** Inspect implementation.** @return {Object}* @api public*/inspect() {if (!this.req) return;return this.toJSON();},/*** Return JSON representation.** @return {Object}* @api public*/toJSON() {return only(this, ['method','url','header']);}};/*** Custom inspection implementation for newer Node.js versions.** @return {Object}* @api public*//* istanbul ignore else */if (util.inspect.custom) {module.exports[util.inspect.custom] = module.exports.inspect;}
response.js
'use strict';/*** Module dependencies.*/const contentDisposition = require('content-disposition');const getType = require('cache-content-type');const onFinish = require('on-finished');const escape = require('escape-html');const typeis = require('type-is').is;const statuses = require('statuses');const destroy = require('destroy');const assert = require('assert');const extname = require('path').extname;const vary = require('vary');const only = require('only');const util = require('util');const encodeUrl = require('encodeurl');const Stream = require('stream');/*** Prototype.*/module.exports = {/*** Return the request socket.** @return {Connection}* @api public*/get socket() {return this.res.socket;},/*** Return response header.** @return {Object}* @api public*/get header() {const { res } = this;return typeof res.getHeaders === 'function'? res.getHeaders(): res._headers || {}; // Node < 7.7},/*** Return response header, alias as response.header** @return {Object}* @api public*/get headers() {return this.header;},/*** Get response status code.** @return {Number}* @api public*/get status() {return this.res.statusCode;},/*** Set response status code.** @param {Number} code* @api public*/set status(code) {if (this.headerSent) return;assert(Number.isInteger(code), 'status code must be a number');assert(code >= 100 && code <= 999, `invalid status code: ${code}`);this._explicitStatus = true;this.res.statusCode = code;if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];if (this.body && statuses.empty[code]) this.body = null;},/*** Get response status message** @return {String}* @api public*/get message() {return this.res.statusMessage || statuses[this.status];},/*** Set response status message** @param {String} msg* @api public*/set message(msg) {this.res.statusMessage = msg;},/*** Get response body.** @return {Mixed}* @api public*/get body() {return this._body;},/*** Set response body.** @param {String|Buffer|Object|Stream} val* @api public*/set body(val) {const original = this._body;this._body = val;// no contentif (null == val) {if (!statuses.empty[this.status]) this.status = 204;if (val === null) this._explicitNullBody = true;this.remove('Content-Type');this.remove('Content-Length');this.remove('Transfer-Encoding');return;}// set the statusif (!this._explicitStatus) this.status = 200;// set the content-type only if not yet setconst setType = !this.has('Content-Type');// stringif ('string' === typeof val) {if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';this.length = Buffer.byteLength(val);return;}// bufferif (Buffer.isBuffer(val)) {if (setType) this.type = 'bin';this.length = val.length;return;}// streamif (val instanceof Stream) {onFinish(this.res, destroy.bind(null, val));if (original != val) {val.once('error', err => this.ctx.onerror(err));// overwritingif (null != original) this.remove('Content-Length');}if (setType) this.type = 'bin';return;}// jsonthis.remove('Content-Length');this.type = 'json';},/*** Set Content-Length field to `n`.** @param {Number} n* @api public*/set length(n) {this.set('Content-Length', n);},/*** Return parsed response Content-Length when present.** @return {Number}* @api public*/get length() {if (this.has('Content-Length')) {return parseInt(this.get('Content-Length'), 10) || 0;}const { body } = this;if (!body || body instanceof Stream) return undefined;if ('string' === typeof body) return Buffer.byteLength(body);if (Buffer.isBuffer(body)) return body.length;return Buffer.byteLength(JSON.stringify(body));},/*** Check if a header has been written to the socket.** @return {Boolean}* @api public*/get headerSent() {return this.res.headersSent;},/*** Vary on `field`.** @param {String} field* @api public*/vary(field) {if (this.headerSent) return;vary(this.res, field);},/*** Perform a 302 redirect to `url`.** The string "back" is special-cased* to provide Referrer support, when Referrer* is not present `alt` or "/" is used.** Examples:** this.redirect('back');* this.redirect('back', '/index.html');* this.redirect('/login');* this.redirect('http://google.com');** @param {String} url* @param {String} [alt]* @api public*/redirect(url, alt) {// locationif ('back' === url) url = this.ctx.get('Referrer') || alt || '/';this.set('Location', encodeUrl(url));// statusif (!statuses.redirect[this.status]) this.status = 302;// htmlif (this.ctx.accepts('html')) {url = escape(url);this.type = 'text/html; charset=utf-8';this.body = `Redirecting to <a href="${url}">${url}</a>.`;return;}// textthis.type = 'text/plain; charset=utf-8';this.body = `Redirecting to ${url}.`;},/*** Set Content-Disposition header to "attachment" with optional `filename`.** @param {String} filename* @api public*/attachment(filename, options) {if (filename) this.type = extname(filename);this.set('Content-Disposition', contentDisposition(filename, options));},/*** Set Content-Type response header with `type` through `mime.lookup()`* when it does not contain a charset.** Examples:** this.type = '.html';* this.type = 'html';* this.type = 'json';* this.type = 'application/json';* this.type = 'png';** @param {String} type* @api public*/set type(type) {type = getType(type);if (type) {this.set('Content-Type', type);} else {this.remove('Content-Type');}},/*** Set the Last-Modified date using a string or a Date.** this.response.lastModified = new Date();* this.response.lastModified = '2013-09-13';** @param {String|Date} type* @api public*/set lastModified(val) {if ('string' === typeof val) val = new Date(val);this.set('Last-Modified', val.toUTCString());},/*** Get the Last-Modified date in Date form, if it exists.** @return {Date}* @api public*/get lastModified() {const date = this.get('last-modified');if (date) return new Date(date);},/*** Set the ETag of a response.* This will normalize the quotes if necessary.** this.response.etag = 'md5hashsum';* this.response.etag = '"md5hashsum"';* this.response.etag = 'W/"123456789"';** @param {String} etag* @api public*/set etag(val) {if (!/^(W\/)?"/.test(val)) val = `"${val}"`;this.set('ETag', val);},/*** Get the ETag of a response.** @return {String}* @api public*/get etag() {return this.get('ETag');},/*** Return the response mime type void of* parameters such as "charset".** @return {String}* @api public*/get type() {const type = this.get('Content-Type');if (!type) return '';return type.split(';', 1)[0];},/*** Check whether the response is one of the listed types.* Pretty much the same as `this.request.is()`.** @param {String|String[]} [type]* @param {String[]} [types]* @return {String|false}* @api public*/is(type, ...types) {return typeis(this.type, type, ...types);},/*** Return response header.** Examples:** this.get('Content-Type');* // => "text/plain"** this.get('content-type');* // => "text/plain"** @param {String} field* @return {String}* @api public*/get(field) {return this.header[field.toLowerCase()] || '';},/*** Returns true if the header identified by name is currently set in the outgoing headers.* The header name matching is case-insensitive.** Examples:** this.has('Content-Type');* // => true** this.get('content-type');* // => true** @param {String} field* @return {boolean}* @api public*/has(field) {return typeof this.res.hasHeader === 'function'? this.res.hasHeader(field)// Node < 7.7: field.toLowerCase() in this.headers;},/*** Set header `field` to `val` or pass* an object of header fields.** Examples:** this.set('Foo', ['bar', 'baz']);* this.set('Accept', 'application/json');* this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });** @param {String|Object|Array} field* @param {String} val* @api public*/set(field, val) {if (this.headerSent) return;if (2 === arguments.length) {if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));else if (typeof val !== 'string') val = String(val);this.res.setHeader(field, val);} else {for (const key in field) {this.set(key, field[key]);}}},/*** Append additional header `field` with value `val`.** Examples:**
- this.append(‘Link’, [‘http://localhost/‘, ‘http://localhost:3000/‘]);
- this.append(‘Set-Cookie’, ‘foo=bar; Path=/; HttpOnly’);
- this.append(‘Warning’, ‘199 Miscellaneous warning’);
- ``` *
- @param {String} field
- @param {String|Array} val
@api public */
append(field, val) { const prev = this.get(field);
if (prev) { val = Array.isArray(prev) ? prev.concat(val) : [prev].concat(val); }
return this.set(field, val); },
/**
- Remove header
field. * - @param {String} name
@api public */
remove(field) { if (this.headerSent) return;
this.res.removeHeader(field); },
/**
- Checks if the request is writable.
- Tests for the existence of the socket
- as node sometimes does not set it. *
- @return {Boolean}
@api private */
get writable() { // can’t write any more after response finished // response.writableEnded is available since Node > 12.9 // https://nodejs.org/api/http.html#http_response_writableended // response.finished is undocumented feature of previous Node versions // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js if (this.res.writableEnded || this.res.finished) return false;
const socket = this.res.socket; // There are already pending outgoing res, but still writable // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486 if (!socket) return true; return socket.writable; },
/**
- Inspect implementation. *
- @return {Object}
@api public */
inspect() { if (!this.res) return; const o = this.toJSON(); o.body = this.body; return o; },
/**
- Return JSON representation. *
- @return {Object}
@api public */
toJSON() { return only(this, [ ‘status’, ‘message’, ‘header’ ]); },
/**
Flush any set headers and begin the body */
flushHeaders() { this.res.flushHeaders(); } };
/**
- Custom inspection implementation for node 6+. *
- @return {Object}
- @api public */
/ istanbul ignore else / if (util.inspect.custom) { module.exports[util.inspect.custom] = module.exports.inspect; }
``` 可以看到,在 request.js / response.js 中,所做的事情其实就是将原生 req、res 做了一层封装。我们在调用 request.js 时则间接调用了 req。(res 同理)
例如我们在访问 ctx.header 时,ctx 会将其委托给 request.header ,而 request 又会将其委托给 req.headers,最终我们拿到了header值。
Koa使用委托模式,把外层暴露的自身对象将请求委托给内部的node原生对象进行处理。 委托模式使得我们可以用 聚合 来替代 继承。
