Koa2 源码解读之 Application 对象 一文中,我们对 Application 对象进行了解读。在Application 对象的构造函数中,通过 Object.create() 创建了一个 context 对象,并挂载到 Application 的 context 属性下:

  1. constructor() {
  2. // ...
  3. this.context = Object.create(context);
  4. // ...
  5. }

在中间件执行的时候,还会对这个 context 进行包装,然后把包装后的 context 对象作为中间件函数的第一个函数进行传入,因此我们可以通过中间件函数的第一个参数来调用这个 context 对象。

  1. callback() {
  2. // ...
  3. // 在 createContext() 函数中对 this.context 进行包装
  4. const ctx = this.createContext(req, res);
  5. // 在 handleRequest() 函数中把包装后的 context 对象作为中间件函数的第一个函数传入
  6. return this.handleRequest(ctx, fn);
  7. }

在中间件中使用这个context 对象:

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. app.use(async (ctx, next) => {
  4. // 在中间件中使用 ctx(context 对象)
  5. return new Promise((resolve, reject) => {
  6. setTimeout(() => {
  7. ctx.body = 'hello';
  8. resolve();
  9. }, 1000);
  10. });
  11. });
  12. app.listen(3000);

在 Application 中的这个 context 对象,就来源于 lib/context.js ,它提供了 toJson、inspect、throw、onerror、get cookies()、set cookies() 等方法,并对 Request 和 Response 对象做了代理访问。

错误处理 onerror

  1. /**
  2. * Default error handling.
  3. *
  4. * @param {Error} err
  5. * @api private
  6. */
  7. onerror(err) {
  8. // don't do anything if there is no error.
  9. // this allows you to pass `this.onerror`
  10. // to node-style callbacks.
  11. if (null == err) return;
  12. // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
  13. // See https://github.com/koajs/koa/issues/1466
  14. // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
  15. const isNativeError = // 判断是否为原生错误
  16. Object.prototype.toString.call(err) === '[object Error]' ||
  17. err instanceof Error;
  18. if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
  19. let headerSent = false;
  20. if (this.headerSent || !this.writable) { // 检查是否已经发送了一个响应头
  21. headerSent = err.headerSent = true;
  22. }
  23. // delegate
  24. // emit 这个错误,在 Application 中通过 on 监听错误:this.on('error', this.onerror) ,
  25. // 这里通过 emit 将错误转交给application的onerror处理。这里可以做到emit、on来发布订阅错误就是因为application继承了Emitter模块。
  26. this.app.emit('error', err, this);
  27. // nothing we can do here other
  28. // than delegate to the app-level
  29. // handler and log.
  30. if (headerSent) { // 已经发送了一个响应头,则直接return,不再往下执行
  31. return;
  32. }
  33. const { res } = this;
  34. // first unset all headers
  35. /* istanbul ignore else */
  36. if (typeof res.getHeaderNames === 'function') { // headerNames 为 function,则删除 res 上的所有 Header
  37. res.getHeaderNames().forEach(name => res.removeHeader(name));
  38. } else {
  39. res._headers = {}; // Node < 7.7
  40. }
  41. // 下面的处理就是对这个错误的ctx进行修改。如header设置成err.headers、statusCode设置成err.status ,msg设置为如err.message等等
  42. // then set those specified
  43. this.set(err.headers);
  44. // force text/plain
  45. this.type = 'text';
  46. let statusCode = err.status || err.statusCode;
  47. // ENOENT support
  48. if ('ENOENT' === err.code) statusCode = 404;
  49. // default to 500
  50. if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
  51. // respond
  52. const code = statuses[statusCode];
  53. const msg = err.expose ? err.message : code;
  54. this.status = err.status = statusCode;
  55. this.length = Buffer.byteLength(msg);
  56. res.end(msg);
  57. },

在 context 中,当发生了 error 时,对挂载在 context 对象上的 response 进行处理,并把 error emit 给 Application 处理。

  1. // emit 这个错误,在 Application 中通过 on 监听错误:this.on('error', this.onerror) ,
  2. // 这里通过 emit 将错误转交给application的onerror处理。这里可以做到emit、on来发布订阅错误就是因为application继承了Emitter模块。
  3. this.app.emit('error', err, this);

在 Application 的 callback() 中,通过 on 监听 error 事件,并调用了 Application 的 onerror 方法来处理错误:

  1. callback() {
  2. // ...
  3. if (!this.listenerCount('error')) this.on('error', this.onerror);
  4. // ...
  5. }

我们看看 Application 是怎么处理错误的:

  1. /**
  2. * Default error handler.
  3. *
  4. * @param {Error} err
  5. * @api private
  6. */
  7. onerror(err) {
  8. // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
  9. // See https://github.com/koajs/koa/issues/1466
  10. // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
  11. const isNativeError =
  12. Object.prototype.toString.call(err) === '[object Error]' ||
  13. err instanceof Error;
  14. if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));
  15. if (404 === err.status || err.expose) return;
  16. if (this.silent) return;
  17. const msg = err.stack || err.toString();
  18. console.error(`\n${msg.replace(/^/gm, ' ')}\n`);
  19. }

可以看到,在 onerror 中,只是把 error 打印了出来。但是对于中间件内部的错误,koa 是无法捕捉的(除非转同步),我们的应用如果需要记录这个错误,可以使用 node 的 process 监听:

  1. process.on("unhandledRejection", (err) => {
  2. console.log(err);
  3. });

处理cookie

在 Koa 中,cookie 的存取是被放在 context 中的:

  1. // 获取 cookies,如果已经有 cookie,则直接返回,如果没有,则生成cookie
  2. get cookies() {
  3. if (!this[COOKIES]) {
  4. this[COOKIES] = new Cookies(this.req, this.res, {
  5. keys: this.app.keys,
  6. secure: this.request.secure
  7. });
  8. }
  9. return this[COOKIES];
  10. },
  11. // 设置 cookie
  12. set cookies(_cookies) {
  13. this[COOKIES] = _cookies;
  14. }
  15. };

Koa 中的委托模式

为了方便对 Request 和 Response 对象进行操作,Koa 通过 delegate 对 Context 进行了代理访问处理,也就是将 Request 和 Response 挂载到了 context 对象上,使得可以通过 Context 即可操作对应的 Request 和 Response :

  1. /**
  2. * Response delegation.
  3. */
  4. // 委托响应
  5. delegate(proto, 'response')
  6. .method('attachment')
  7. .method('redirect')
  8. .method('remove')
  9. .method('vary')
  10. .method('has')
  11. .method('set')
  12. .method('append')
  13. .method('flushHeaders')
  14. .access('status')
  15. .access('message')
  16. .access('body')
  17. .access('length')
  18. .access('type')
  19. .access('lastModified')
  20. .access('etag')
  21. .getter('headerSent')
  22. .getter('writable');
  23. /**
  24. * Request delegation.
  25. */
  26. // 委托请求
  27. delegate(proto, 'request')
  28. .method('acceptsLanguages')
  29. .method('acceptsEncodings')
  30. .method('acceptsCharsets')
  31. .method('accepts')
  32. .method('get')
  33. .method('is')
  34. .access('querystring')
  35. .access('idempotent')
  36. .access('socket')
  37. .access('search')
  38. .access('method')
  39. .access('query')
  40. .access('path')
  41. .access('url')
  42. .access('accept')
  43. .getter('origin')
  44. .getter('href')
  45. .getter('subdomains')
  46. .getter('protocol')
  47. .getter('host')
  48. .getter('hostname')
  49. .getter('URL')
  50. .getter('header')
  51. .getter('headers')
  52. .getter('secure')
  53. .getter('stale')
  54. .getter('fresh')
  55. .getter('ips')
  56. .getter('ip');

可以看到,Koa 使用 delegate 这个函数将 原本属于 Request 和 Response 的属性或方法代理到了 context 对象上,下面我们来看看 delegate 的源码。

delegates ,可以帮我们方便快捷地使用设计模式当中的委托模式(Delegation Pattern),即外层暴露的对象将请求委托给内部的其他对象进行处理。

初始化

  1. function Delegator(proto, target) {
  2. if (!(this instanceof Delegator)) return new Delegator(proto, target);
  3. this.proto = proto;
  4. this.target = target;
  5. this.methods = [];
  6. this.getters = [];
  7. this.setters = [];
  8. this.fluents = [];
  9. }

在初始化函数 Delegate 中,首先判断当前的 this 是否是 Delegate 的实例,如果不是,就调用 new Delegate(proto, target) 进行实例化。通过这种方式,可以避免在调用初始化函数时忘记写 new 造成的问题。因此下面的两种写法是等价的:

  1. let d = new Delegator(proto, target);
  2. let d = Delegator(proto, target);

在初始化函数中,还定义了 methods、getters、setters、fluents 等属性,用于记录委托了哪些属性和方法。

method

  1. Delegator.prototype.method = function(name){
  2. var proto = this.proto;
  3. var target = this.target;
  4. this.methods.push(name);
  5. proto[name] = function(){
  6. return this[target][name].apply(this[target], arguments);
  7. };
  8. return this;
  9. };

在 method 中,将需要代理的方法存入 methods 中记录起来,然后通过 apply 方法将当前代理的方法的 this 对象指向Delegate的内部对象 this[target],从而确保在执行 this[target][name] 时,函数体内的 this 是指向对应的内部对象。

getter

  1. Delegator.prototype.getter = function(name){
  2. var proto = this.proto;
  3. var target = this.target;
  4. this.getters.push(name);
  5. proto.__defineGetter__(name, function(){
  6. return this[target][name];
  7. });
  8. return this;
  9. };

getter 的实现也十分简单,在 getter 中,也是一样首先将需要代理的属性存储到 getters 中记录起来,然后通过 __defineGetter__ 劫持 proto 的get,转而去访问 target 。__defineGetter__ 可以将一个函数绑定在当前对象的指定属性上,当那个属性的值被调用时,绑定在该属性上的函数就会被调用。该特性已经从 Web 标准中删除,建议通过 Proxy 或 Object.defineProperty 进行劫持。(Vue2 使用Object.defineProperty进行数据劫持,从而实现数据响应式,而Vue3则抛弃了Object.defineProperty,使用 Proxy 劫持数据,实现数据的响应式。)

setter

  1. Delegator.prototype.setter = function(name){
  2. var proto = this.proto;
  3. var target = this.target;
  4. this.setters.push(name);
  5. proto.__defineSetter__(name, function(val){
  6. return this[target][name] = val;
  7. });
  8. return this;
  9. };

与 getter 的实现不同的是,setter 使用 __defineSetter__ 为已存在的对象添加可读属性。该特性也已经从 Web 标准中删除,建议使用 Proxy 或 Object.defineProperty 实现同样的功能。

access

  1. Delegator.prototype.access = function(name){
  2. return this.getter(name).setter(name);
  3. };

access 则通过直接调用 Delegate 对象的 getter 和 setter,从而实现对属性的 getter 和 setter 的代理。

Koa 在 context 中将 Request 和 Response 的属性和方法代理到了 context 对象上,我们来看看 request.js 和 response.js 的源码:

request.js

  1. 'use strict';
  2. /**
  3. * Module dependencies.
  4. */
  5. const URL = require('url').URL;
  6. const net = require('net');
  7. const accepts = require('accepts');
  8. const contentType = require('content-type');
  9. const stringify = require('url').format;
  10. const parse = require('parseurl');
  11. const qs = require('querystring');
  12. const typeis = require('type-is');
  13. const fresh = require('fresh');
  14. const only = require('only');
  15. const util = require('util');
  16. const IP = Symbol('context#ip');
  17. /**
  18. * Prototype.
  19. */
  20. module.exports = {
  21. /**
  22. * Return request header.
  23. *
  24. * @return {Object}
  25. * @api public
  26. */
  27. get header() {
  28. return this.req.headers;
  29. },
  30. /**
  31. * Set request header.
  32. *
  33. * @api public
  34. */
  35. set header(val) {
  36. this.req.headers = val;
  37. },
  38. /**
  39. * Return request header, alias as request.header
  40. *
  41. * @return {Object}
  42. * @api public
  43. */
  44. get headers() {
  45. return this.req.headers;
  46. },
  47. /**
  48. * Set request header, alias as request.header
  49. *
  50. * @api public
  51. */
  52. set headers(val) {
  53. this.req.headers = val;
  54. },
  55. /**
  56. * Get request URL.
  57. *
  58. * @return {String}
  59. * @api public
  60. */
  61. get url() {
  62. return this.req.url;
  63. },
  64. /**
  65. * Set request URL.
  66. *
  67. * @api public
  68. */
  69. set url(val) {
  70. this.req.url = val;
  71. },
  72. /**
  73. * Get origin of URL.
  74. *
  75. * @return {String}
  76. * @api public
  77. */
  78. get origin() {
  79. return `${this.protocol}://${this.host}`;
  80. },
  81. /**
  82. * Get full request URL.
  83. *
  84. * @return {String}
  85. * @api public
  86. */
  87. get href() {
  88. // support: `GET http://example.com/foo`
  89. if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;
  90. return this.origin + this.originalUrl;
  91. },
  92. /**
  93. * Get request method.
  94. *
  95. * @return {String}
  96. * @api public
  97. */
  98. get method() {
  99. return this.req.method;
  100. },
  101. /**
  102. * Set request method.
  103. *
  104. * @param {String} val
  105. * @api public
  106. */
  107. set method(val) {
  108. this.req.method = val;
  109. },
  110. /**
  111. * Get request pathname.
  112. *
  113. * @return {String}
  114. * @api public
  115. */
  116. get path() {
  117. return parse(this.req).pathname;
  118. },
  119. /**
  120. * Set pathname, retaining the query string when present.
  121. *
  122. * @param {String} path
  123. * @api public
  124. */
  125. set path(path) {
  126. const url = parse(this.req);
  127. if (url.pathname === path) return;
  128. url.pathname = path;
  129. url.path = null;
  130. this.url = stringify(url);
  131. },
  132. /**
  133. * Get parsed query string.
  134. *
  135. * @return {Object}
  136. * @api public
  137. */
  138. get query() {
  139. const str = this.querystring;
  140. const c = this._querycache = this._querycache || {};
  141. return c[str] || (c[str] = qs.parse(str));
  142. },
  143. /**
  144. * Set query string as an object.
  145. *
  146. * @param {Object} obj
  147. * @api public
  148. */
  149. set query(obj) {
  150. this.querystring = qs.stringify(obj);
  151. },
  152. /**
  153. * Get query string.
  154. *
  155. * @return {String}
  156. * @api public
  157. */
  158. get querystring() {
  159. if (!this.req) return '';
  160. return parse(this.req).query || '';
  161. },
  162. /**
  163. * Set query string.
  164. *
  165. * @param {String} str
  166. * @api public
  167. */
  168. set querystring(str) {
  169. const url = parse(this.req);
  170. if (url.search === `?${str}`) return;
  171. url.search = str;
  172. url.path = null;
  173. this.url = stringify(url);
  174. },
  175. /**
  176. * Get the search string. Same as the query string
  177. * except it includes the leading ?.
  178. *
  179. * @return {String}
  180. * @api public
  181. */
  182. get search() {
  183. if (!this.querystring) return '';
  184. return `?${this.querystring}`;
  185. },
  186. /**
  187. * Set the search string. Same as
  188. * request.querystring= but included for ubiquity.
  189. *
  190. * @param {String} str
  191. * @api public
  192. */
  193. set search(str) {
  194. this.querystring = str;
  195. },
  196. /**
  197. * Parse the "Host" header field host
  198. * and support X-Forwarded-Host when a
  199. * proxy is enabled.
  200. *
  201. * @return {String} hostname:port
  202. * @api public
  203. */
  204. get host() {
  205. const proxy = this.app.proxy;
  206. let host = proxy && this.get('X-Forwarded-Host');
  207. if (!host) {
  208. if (this.req.httpVersionMajor >= 2) host = this.get(':authority');
  209. if (!host) host = this.get('Host');
  210. }
  211. if (!host) return '';
  212. return host.split(/\s*,\s*/, 1)[0];
  213. },
  214. /**
  215. * Parse the "Host" header field hostname
  216. * and support X-Forwarded-Host when a
  217. * proxy is enabled.
  218. *
  219. * @return {String} hostname
  220. * @api public
  221. */
  222. get hostname() {
  223. const host = this.host;
  224. if (!host) return '';
  225. if ('[' === host[0]) return this.URL.hostname || ''; // IPv6
  226. return host.split(':', 1)[0];
  227. },
  228. /**
  229. * Get WHATWG parsed URL.
  230. * Lazily memoized.
  231. *
  232. * @return {URL|Object}
  233. * @api public
  234. */
  235. get URL() {
  236. /* istanbul ignore else */
  237. if (!this.memoizedURL) {
  238. const originalUrl = this.originalUrl || ''; // avoid undefined in template string
  239. try {
  240. this.memoizedURL = new URL(`${this.origin}${originalUrl}`);
  241. } catch (err) {
  242. this.memoizedURL = Object.create(null);
  243. }
  244. }
  245. return this.memoizedURL;
  246. },
  247. /**
  248. * Check if the request is fresh, aka
  249. * Last-Modified and/or the ETag
  250. * still match.
  251. *
  252. * @return {Boolean}
  253. * @api public
  254. */
  255. get fresh() {
  256. const method = this.method;
  257. const s = this.ctx.status;
  258. // GET or HEAD for weak freshness validation only
  259. if ('GET' !== method && 'HEAD' !== method) return false;
  260. // 2xx or 304 as per rfc2616 14.26
  261. if ((s >= 200 && s < 300) || 304 === s) {
  262. return fresh(this.header, this.response.header);
  263. }
  264. return false;
  265. },
  266. /**
  267. * Check if the request is stale, aka
  268. * "Last-Modified" and / or the "ETag" for the
  269. * resource has changed.
  270. *
  271. * @return {Boolean}
  272. * @api public
  273. */
  274. get stale() {
  275. return !this.fresh;
  276. },
  277. /**
  278. * Check if the request is idempotent.
  279. *
  280. * @return {Boolean}
  281. * @api public
  282. */
  283. get idempotent() {
  284. const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
  285. return !!~methods.indexOf(this.method);
  286. },
  287. /**
  288. * Return the request socket.
  289. *
  290. * @return {Connection}
  291. * @api public
  292. */
  293. get socket() {
  294. return this.req.socket;
  295. },
  296. /**
  297. * Get the charset when present or undefined.
  298. *
  299. * @return {String}
  300. * @api public
  301. */
  302. get charset() {
  303. try {
  304. const { parameters } = contentType.parse(this.req);
  305. return parameters.charset || '';
  306. } catch (e) {
  307. return '';
  308. }
  309. },
  310. /**
  311. * Return parsed Content-Length when present.
  312. *
  313. * @return {Number}
  314. * @api public
  315. */
  316. get length() {
  317. const len = this.get('Content-Length');
  318. if (len === '') return;
  319. return ~~len;
  320. },
  321. /**
  322. * Return the protocol string "http" or "https"
  323. * when requested with TLS. When the proxy setting
  324. * is enabled the "X-Forwarded-Proto" header
  325. * field will be trusted. If you're running behind
  326. * a reverse proxy that supplies https for you this
  327. * may be enabled.
  328. *
  329. * @return {String}
  330. * @api public
  331. */
  332. get protocol() {
  333. if (this.socket.encrypted) return 'https';
  334. if (!this.app.proxy) return 'http';
  335. const proto = this.get('X-Forwarded-Proto');
  336. return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http';
  337. },
  338. /**
  339. * Shorthand for:
  340. *
  341. * this.protocol == 'https'
  342. *
  343. * @return {Boolean}
  344. * @api public
  345. */
  346. get secure() {
  347. return 'https' === this.protocol;
  348. },
  349. /**
  350. * When `app.proxy` is `true`, parse
  351. * the "X-Forwarded-For" ip address list.
  352. *
  353. * For example if the value was "client, proxy1, proxy2"
  354. * you would receive the array `["client", "proxy1", "proxy2"]`
  355. * where "proxy2" is the furthest down-stream.
  356. *
  357. * @return {Array}
  358. * @api public
  359. */
  360. get ips() {
  361. const proxy = this.app.proxy;
  362. const val = this.get(this.app.proxyIpHeader);
  363. let ips = proxy && val
  364. ? val.split(/\s*,\s*/)
  365. : [];
  366. if (this.app.maxIpsCount > 0) {
  367. ips = ips.slice(-this.app.maxIpsCount);
  368. }
  369. return ips;
  370. },
  371. /**
  372. * Return request's remote address
  373. * When `app.proxy` is `true`, parse
  374. * the "X-Forwarded-For" ip address list and return the first one
  375. *
  376. * @return {String}
  377. * @api public
  378. */
  379. get ip() {
  380. if (!this[IP]) {
  381. this[IP] = this.ips[0] || this.socket.remoteAddress || '';
  382. }
  383. return this[IP];
  384. },
  385. set ip(_ip) {
  386. this[IP] = _ip;
  387. },
  388. /**
  389. * Return subdomains as an array.
  390. *
  391. * Subdomains are the dot-separated parts of the host before the main domain
  392. * of the app. By default, the domain of the app is assumed to be the last two
  393. * parts of the host. This can be changed by setting `app.subdomainOffset`.
  394. *
  395. * For example, if the domain is "tobi.ferrets.example.com":
  396. * If `app.subdomainOffset` is not set, this.subdomains is
  397. * `["ferrets", "tobi"]`.
  398. * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
  399. *
  400. * @return {Array}
  401. * @api public
  402. */
  403. get subdomains() {
  404. const offset = this.app.subdomainOffset;
  405. const hostname = this.hostname;
  406. if (net.isIP(hostname)) return [];
  407. return hostname
  408. .split('.')
  409. .reverse()
  410. .slice(offset);
  411. },
  412. /**
  413. * Get accept object.
  414. * Lazily memoized.
  415. *
  416. * @return {Object}
  417. * @api private
  418. */
  419. get accept() {
  420. return this._accept || (this._accept = accepts(this.req));
  421. },
  422. /**
  423. * Set accept object.
  424. *
  425. * @param {Object}
  426. * @api private
  427. */
  428. set accept(obj) {
  429. this._accept = obj;
  430. },
  431. /**
  432. * Check if the given `type(s)` is acceptable, returning
  433. * the best match when true, otherwise `false`, in which
  434. * case you should respond with 406 "Not Acceptable".
  435. *
  436. * The `type` value may be a single mime type string
  437. * such as "application/json", the extension name
  438. * such as "json" or an array `["json", "html", "text/plain"]`. When a list
  439. * or array is given the _best_ match, if any is returned.
  440. *
  441. * Examples:
  442. *
  443. * // Accept: text/html
  444. * this.accepts('html');
  445. * // => "html"
  446. *
  447. * // Accept: text/*, application/json
  448. * this.accepts('html');
  449. * // => "html"
  450. * this.accepts('text/html');
  451. * // => "text/html"
  452. * this.accepts('json', 'text');
  453. * // => "json"
  454. * this.accepts('application/json');
  455. * // => "application/json"
  456. *
  457. * // Accept: text/*, application/json
  458. * this.accepts('image/png');
  459. * this.accepts('png');
  460. * // => false
  461. *
  462. * // Accept: text/*;q=.5, application/json
  463. * this.accepts(['html', 'json']);
  464. * this.accepts('html', 'json');
  465. * // => "json"
  466. *
  467. * @param {String|Array} type(s)...
  468. * @return {String|Array|false}
  469. * @api public
  470. */
  471. accepts(...args) {
  472. return this.accept.types(...args);
  473. },
  474. /**
  475. * Return accepted encodings or best fit based on `encodings`.
  476. *
  477. * Given `Accept-Encoding: gzip, deflate`
  478. * an array sorted by quality is returned:
  479. *
  480. * ['gzip', 'deflate']
  481. *
  482. * @param {String|Array} encoding(s)...
  483. * @return {String|Array}
  484. * @api public
  485. */
  486. acceptsEncodings(...args) {
  487. return this.accept.encodings(...args);
  488. },
  489. /**
  490. * Return accepted charsets or best fit based on `charsets`.
  491. *
  492. * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
  493. * an array sorted by quality is returned:
  494. *
  495. * ['utf-8', 'utf-7', 'iso-8859-1']
  496. *
  497. * @param {String|Array} charset(s)...
  498. * @return {String|Array}
  499. * @api public
  500. */
  501. acceptsCharsets(...args) {
  502. return this.accept.charsets(...args);
  503. },
  504. /**
  505. * Return accepted languages or best fit based on `langs`.
  506. *
  507. * Given `Accept-Language: en;q=0.8, es, pt`
  508. * an array sorted by quality is returned:
  509. *
  510. * ['es', 'pt', 'en']
  511. *
  512. * @param {String|Array} lang(s)...
  513. * @return {Array|String}
  514. * @api public
  515. */
  516. acceptsLanguages(...args) {
  517. return this.accept.languages(...args);
  518. },
  519. /**
  520. * Check if the incoming request contains the "Content-Type"
  521. * header field and if it contains any of the given mime `type`s.
  522. * If there is no request body, `null` is returned.
  523. * If there is no content type, `false` is returned.
  524. * Otherwise, it returns the first `type` that matches.
  525. *
  526. * Examples:
  527. *
  528. * // With Content-Type: text/html; charset=utf-8
  529. * this.is('html'); // => 'html'
  530. * this.is('text/html'); // => 'text/html'
  531. * this.is('text/*', 'application/json'); // => 'text/html'
  532. *
  533. * // When Content-Type is application/json
  534. * this.is('json', 'urlencoded'); // => 'json'
  535. * this.is('application/json'); // => 'application/json'
  536. * this.is('html', 'application/*'); // => 'application/json'
  537. *
  538. * this.is('html'); // => false
  539. *
  540. * @param {String|String[]} [type]
  541. * @param {String[]} [types]
  542. * @return {String|false|null}
  543. * @api public
  544. */
  545. is(type, ...types) {
  546. return typeis(this.req, type, ...types);
  547. },
  548. /**
  549. * Return the request mime type void of
  550. * parameters such as "charset".
  551. *
  552. * @return {String}
  553. * @api public
  554. */
  555. get type() {
  556. const type = this.get('Content-Type');
  557. if (!type) return '';
  558. return type.split(';')[0];
  559. },
  560. /**
  561. * Return request header.
  562. *
  563. * The `Referrer` header field is special-cased,
  564. * both `Referrer` and `Referer` are interchangeable.
  565. *
  566. * Examples:
  567. *
  568. * this.get('Content-Type');
  569. * // => "text/plain"
  570. *
  571. * this.get('content-type');
  572. * // => "text/plain"
  573. *
  574. * this.get('Something');
  575. * // => ''
  576. *
  577. * @param {String} field
  578. * @return {String}
  579. * @api public
  580. */
  581. get(field) {
  582. const req = this.req;
  583. switch (field = field.toLowerCase()) {
  584. case 'referer':
  585. case 'referrer':
  586. return req.headers.referrer || req.headers.referer || '';
  587. default:
  588. return req.headers[field] || '';
  589. }
  590. },
  591. /**
  592. * Inspect implementation.
  593. *
  594. * @return {Object}
  595. * @api public
  596. */
  597. inspect() {
  598. if (!this.req) return;
  599. return this.toJSON();
  600. },
  601. /**
  602. * Return JSON representation.
  603. *
  604. * @return {Object}
  605. * @api public
  606. */
  607. toJSON() {
  608. return only(this, [
  609. 'method',
  610. 'url',
  611. 'header'
  612. ]);
  613. }
  614. };
  615. /**
  616. * Custom inspection implementation for newer Node.js versions.
  617. *
  618. * @return {Object}
  619. * @api public
  620. */
  621. /* istanbul ignore else */
  622. if (util.inspect.custom) {
  623. module.exports[util.inspect.custom] = module.exports.inspect;
  624. }

response.js

  1. 'use strict';
  2. /**
  3. * Module dependencies.
  4. */
  5. const contentDisposition = require('content-disposition');
  6. const getType = require('cache-content-type');
  7. const onFinish = require('on-finished');
  8. const escape = require('escape-html');
  9. const typeis = require('type-is').is;
  10. const statuses = require('statuses');
  11. const destroy = require('destroy');
  12. const assert = require('assert');
  13. const extname = require('path').extname;
  14. const vary = require('vary');
  15. const only = require('only');
  16. const util = require('util');
  17. const encodeUrl = require('encodeurl');
  18. const Stream = require('stream');
  19. /**
  20. * Prototype.
  21. */
  22. module.exports = {
  23. /**
  24. * Return the request socket.
  25. *
  26. * @return {Connection}
  27. * @api public
  28. */
  29. get socket() {
  30. return this.res.socket;
  31. },
  32. /**
  33. * Return response header.
  34. *
  35. * @return {Object}
  36. * @api public
  37. */
  38. get header() {
  39. const { res } = this;
  40. return typeof res.getHeaders === 'function'
  41. ? res.getHeaders()
  42. : res._headers || {}; // Node < 7.7
  43. },
  44. /**
  45. * Return response header, alias as response.header
  46. *
  47. * @return {Object}
  48. * @api public
  49. */
  50. get headers() {
  51. return this.header;
  52. },
  53. /**
  54. * Get response status code.
  55. *
  56. * @return {Number}
  57. * @api public
  58. */
  59. get status() {
  60. return this.res.statusCode;
  61. },
  62. /**
  63. * Set response status code.
  64. *
  65. * @param {Number} code
  66. * @api public
  67. */
  68. set status(code) {
  69. if (this.headerSent) return;
  70. assert(Number.isInteger(code), 'status code must be a number');
  71. assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
  72. this._explicitStatus = true;
  73. this.res.statusCode = code;
  74. if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
  75. if (this.body && statuses.empty[code]) this.body = null;
  76. },
  77. /**
  78. * Get response status message
  79. *
  80. * @return {String}
  81. * @api public
  82. */
  83. get message() {
  84. return this.res.statusMessage || statuses[this.status];
  85. },
  86. /**
  87. * Set response status message
  88. *
  89. * @param {String} msg
  90. * @api public
  91. */
  92. set message(msg) {
  93. this.res.statusMessage = msg;
  94. },
  95. /**
  96. * Get response body.
  97. *
  98. * @return {Mixed}
  99. * @api public
  100. */
  101. get body() {
  102. return this._body;
  103. },
  104. /**
  105. * Set response body.
  106. *
  107. * @param {String|Buffer|Object|Stream} val
  108. * @api public
  109. */
  110. set body(val) {
  111. const original = this._body;
  112. this._body = val;
  113. // no content
  114. if (null == val) {
  115. if (!statuses.empty[this.status]) this.status = 204;
  116. if (val === null) this._explicitNullBody = true;
  117. this.remove('Content-Type');
  118. this.remove('Content-Length');
  119. this.remove('Transfer-Encoding');
  120. return;
  121. }
  122. // set the status
  123. if (!this._explicitStatus) this.status = 200;
  124. // set the content-type only if not yet set
  125. const setType = !this.has('Content-Type');
  126. // string
  127. if ('string' === typeof val) {
  128. if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
  129. this.length = Buffer.byteLength(val);
  130. return;
  131. }
  132. // buffer
  133. if (Buffer.isBuffer(val)) {
  134. if (setType) this.type = 'bin';
  135. this.length = val.length;
  136. return;
  137. }
  138. // stream
  139. if (val instanceof Stream) {
  140. onFinish(this.res, destroy.bind(null, val));
  141. if (original != val) {
  142. val.once('error', err => this.ctx.onerror(err));
  143. // overwriting
  144. if (null != original) this.remove('Content-Length');
  145. }
  146. if (setType) this.type = 'bin';
  147. return;
  148. }
  149. // json
  150. this.remove('Content-Length');
  151. this.type = 'json';
  152. },
  153. /**
  154. * Set Content-Length field to `n`.
  155. *
  156. * @param {Number} n
  157. * @api public
  158. */
  159. set length(n) {
  160. this.set('Content-Length', n);
  161. },
  162. /**
  163. * Return parsed response Content-Length when present.
  164. *
  165. * @return {Number}
  166. * @api public
  167. */
  168. get length() {
  169. if (this.has('Content-Length')) {
  170. return parseInt(this.get('Content-Length'), 10) || 0;
  171. }
  172. const { body } = this;
  173. if (!body || body instanceof Stream) return undefined;
  174. if ('string' === typeof body) return Buffer.byteLength(body);
  175. if (Buffer.isBuffer(body)) return body.length;
  176. return Buffer.byteLength(JSON.stringify(body));
  177. },
  178. /**
  179. * Check if a header has been written to the socket.
  180. *
  181. * @return {Boolean}
  182. * @api public
  183. */
  184. get headerSent() {
  185. return this.res.headersSent;
  186. },
  187. /**
  188. * Vary on `field`.
  189. *
  190. * @param {String} field
  191. * @api public
  192. */
  193. vary(field) {
  194. if (this.headerSent) return;
  195. vary(this.res, field);
  196. },
  197. /**
  198. * Perform a 302 redirect to `url`.
  199. *
  200. * The string "back" is special-cased
  201. * to provide Referrer support, when Referrer
  202. * is not present `alt` or "/" is used.
  203. *
  204. * Examples:
  205. *
  206. * this.redirect('back');
  207. * this.redirect('back', '/index.html');
  208. * this.redirect('/login');
  209. * this.redirect('http://google.com');
  210. *
  211. * @param {String} url
  212. * @param {String} [alt]
  213. * @api public
  214. */
  215. redirect(url, alt) {
  216. // location
  217. if ('back' === url) url = this.ctx.get('Referrer') || alt || '/';
  218. this.set('Location', encodeUrl(url));
  219. // status
  220. if (!statuses.redirect[this.status]) this.status = 302;
  221. // html
  222. if (this.ctx.accepts('html')) {
  223. url = escape(url);
  224. this.type = 'text/html; charset=utf-8';
  225. this.body = `Redirecting to <a href="${url}">${url}</a>.`;
  226. return;
  227. }
  228. // text
  229. this.type = 'text/plain; charset=utf-8';
  230. this.body = `Redirecting to ${url}.`;
  231. },
  232. /**
  233. * Set Content-Disposition header to "attachment" with optional `filename`.
  234. *
  235. * @param {String} filename
  236. * @api public
  237. */
  238. attachment(filename, options) {
  239. if (filename) this.type = extname(filename);
  240. this.set('Content-Disposition', contentDisposition(filename, options));
  241. },
  242. /**
  243. * Set Content-Type response header with `type` through `mime.lookup()`
  244. * when it does not contain a charset.
  245. *
  246. * Examples:
  247. *
  248. * this.type = '.html';
  249. * this.type = 'html';
  250. * this.type = 'json';
  251. * this.type = 'application/json';
  252. * this.type = 'png';
  253. *
  254. * @param {String} type
  255. * @api public
  256. */
  257. set type(type) {
  258. type = getType(type);
  259. if (type) {
  260. this.set('Content-Type', type);
  261. } else {
  262. this.remove('Content-Type');
  263. }
  264. },
  265. /**
  266. * Set the Last-Modified date using a string or a Date.
  267. *
  268. * this.response.lastModified = new Date();
  269. * this.response.lastModified = '2013-09-13';
  270. *
  271. * @param {String|Date} type
  272. * @api public
  273. */
  274. set lastModified(val) {
  275. if ('string' === typeof val) val = new Date(val);
  276. this.set('Last-Modified', val.toUTCString());
  277. },
  278. /**
  279. * Get the Last-Modified date in Date form, if it exists.
  280. *
  281. * @return {Date}
  282. * @api public
  283. */
  284. get lastModified() {
  285. const date = this.get('last-modified');
  286. if (date) return new Date(date);
  287. },
  288. /**
  289. * Set the ETag of a response.
  290. * This will normalize the quotes if necessary.
  291. *
  292. * this.response.etag = 'md5hashsum';
  293. * this.response.etag = '"md5hashsum"';
  294. * this.response.etag = 'W/"123456789"';
  295. *
  296. * @param {String} etag
  297. * @api public
  298. */
  299. set etag(val) {
  300. if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
  301. this.set('ETag', val);
  302. },
  303. /**
  304. * Get the ETag of a response.
  305. *
  306. * @return {String}
  307. * @api public
  308. */
  309. get etag() {
  310. return this.get('ETag');
  311. },
  312. /**
  313. * Return the response mime type void of
  314. * parameters such as "charset".
  315. *
  316. * @return {String}
  317. * @api public
  318. */
  319. get type() {
  320. const type = this.get('Content-Type');
  321. if (!type) return '';
  322. return type.split(';', 1)[0];
  323. },
  324. /**
  325. * Check whether the response is one of the listed types.
  326. * Pretty much the same as `this.request.is()`.
  327. *
  328. * @param {String|String[]} [type]
  329. * @param {String[]} [types]
  330. * @return {String|false}
  331. * @api public
  332. */
  333. is(type, ...types) {
  334. return typeis(this.type, type, ...types);
  335. },
  336. /**
  337. * Return response header.
  338. *
  339. * Examples:
  340. *
  341. * this.get('Content-Type');
  342. * // => "text/plain"
  343. *
  344. * this.get('content-type');
  345. * // => "text/plain"
  346. *
  347. * @param {String} field
  348. * @return {String}
  349. * @api public
  350. */
  351. get(field) {
  352. return this.header[field.toLowerCase()] || '';
  353. },
  354. /**
  355. * Returns true if the header identified by name is currently set in the outgoing headers.
  356. * The header name matching is case-insensitive.
  357. *
  358. * Examples:
  359. *
  360. * this.has('Content-Type');
  361. * // => true
  362. *
  363. * this.get('content-type');
  364. * // => true
  365. *
  366. * @param {String} field
  367. * @return {boolean}
  368. * @api public
  369. */
  370. has(field) {
  371. return typeof this.res.hasHeader === 'function'
  372. ? this.res.hasHeader(field)
  373. // Node < 7.7
  374. : field.toLowerCase() in this.headers;
  375. },
  376. /**
  377. * Set header `field` to `val` or pass
  378. * an object of header fields.
  379. *
  380. * Examples:
  381. *
  382. * this.set('Foo', ['bar', 'baz']);
  383. * this.set('Accept', 'application/json');
  384. * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
  385. *
  386. * @param {String|Object|Array} field
  387. * @param {String} val
  388. * @api public
  389. */
  390. set(field, val) {
  391. if (this.headerSent) return;
  392. if (2 === arguments.length) {
  393. if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
  394. else if (typeof val !== 'string') val = String(val);
  395. this.res.setHeader(field, val);
  396. } else {
  397. for (const key in field) {
  398. this.set(key, field[key]);
  399. }
  400. }
  401. },
  402. /**
  403. * Append additional header `field` with value `val`.
  404. *
  405. * Examples:
  406. *
  407. *
  • 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原生对象进行处理。 委托模式使得我们可以用 聚合 来替代 继承。