koa.js 源码阅读(二)

application.js

本文将重点解析Koa的入口文件。

代码逻辑

该图展示当调用koa时,application.js经历了以下过程。


执行原理

该图展示了 application.js 核心机制


逐句解析

'use strict';
 * Module dependencies.
// 判断中间件函数是否是生成器函数: 源码解析 https://zhuanlan.zhihu.com/p/150880220
const isGeneratorFunction = require('is-generator-function');
// node的debug库,比console更方便的调试输出工具:简单了解 https://segmentfault.com/a/1190000012699304
const debug = require('debug')('koa:application');
// http请求关闭,完成或错误时的回调函数:官方文档 https://www.npmjs.com/package/on-finished
const onFinished = require('on-finished');
// 请求响应处理
const response = require('./response');
// 工具函数,也是中间件执行的发动机。
// 源码解析1 https://zhuanlan.zhihu.com/p/29455788
// 源码解析2 https://cnodejs.org/topic/5780e12e69d72f545483ca69 (包含koa1的,此文档需要重点看一下)
// 源码解析3 https://www.cnblogs.com/mapleChain/p/11527868.html
const compose = require('koa-compose');
// koa的核心文件之一,作用于获取当前会话的上下文,主要包含当前请求的请求头和响应。
const context = require('./context');
// koa的核心文件之一,整个请求相关的数据存储在此。
const request = require('./request');
// node的statuses库,用于处理并返回正确的http状态码。
const statuses = require('statuses');
// node的events库,通常用于处理/监听/触发/注册/事件,一个典型的发布订阅模式。
const Emitter = require('events');
// node的util库,常用函数集合,通常用于数据类型判断/转换,也支持流输出。
const util = require('util');
// node的Stream库,node的流处理工具。http请求的request对象就是一个流。通过这个工具可监听/读写过程数据。因为所有的stream对象都是EventEmitter的实例
const Stream = require('stream');
// npm内置的http服务包
const http = require('http');
// 一个简单的枚举对象工具,利用reduce返回指定对象中的指定的参数
const only = require('only');
// koa的一个转换器,用于将旧版生成器(基于generator实现)的中间件转换为基于promise实现的中间件。
const convert = require('koa-convert');
// node的depd库,作用于弃用声明,当用户使用koa方法时,告知用户该方法即将弃用
// 使用指南 https://segmentfault.com/a/1190000012734409
const deprecate = require('depd')('koa');
// node的http-error库,作用于获取http error信息,express和koa社区使用广泛。
const { HttpError } = require('http-errors');
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
// 继承Emitter 支持异步处理
module.exports = class Application extends Emitter {
   * Initialize a new `Application`.
   * @api public
    * @param {object} [options] Application options
    * @param {string} [options.env='development'] Environment
    * @param {string[]} [options.keys] Signed cookie keys
    * @param {boolean} [options.proxy] Trust proxy headers
    * @param {number} [options.subdomainOffset] Subdomain offset
    * @param {boolean} [options.proxyIpHeader] proxy ip header, default to X-Forwarded-For
    * @param {boolean} [options.maxIpsCount] max ips read from proxy ip header, default to 0 (means infinity)
  constructor(options) {
    super();
    // 请求头配置参数
    options = options || {};
    // 代理相关参数
    this.proxy = options.proxy || false;
    // 忽略的子域名偏移个数,
    // 例如:test.api.ali.com, 如果设置subdomainOffset为2, 那么返回的数组值为 [“api”, “ali”]
    this.subdomainOffset = options.subdomainOffset || 2;
    // 指定代理IP头,默认X-Forwarded-For,还有X-Real-IP。
    // X-Forwarded-For详细介绍 https://www.runoob.com/w3cnote/http-x-forwarded-for.html
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    // 代理IP头最大IP数量,默认为个,表示无代理。
    this.maxIpsCount = options.maxIpsCount || 0;
    // node环境变量
    this.env = options.env || process.env.NODE_ENV || 'development';
    // 存储所有传入的配置键名
    if (options.keys) this.keys = options.keys;
    // 用于存放所有通过use函数的引入的中间件函数
    this.middleware = [];
    // 通过context.js创建对应的context对象
    this.context = Object.create(context);
    // 通过request.js创建对应的request对象
    this.request = Object.create(request);
    // 通过response.js创建对应的response对象
    this.response = Object.create(response);
    // util.inspect.custom support for node 6+
    /* istanbul ignore else */
    // node版本功能支持校验
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
   * Shorthand for:
   *    http.createServer(app.callback()).listen(...)
   * @param {Mixed} ...
   * @return {Server}
   * @api public
  // 开启一个http服务
  listen(...args) {
    debug('listen');
    // 创建一个http服务,并执行(返回请求处理)回调函数。该回调是将原生request&response挂载到koa的context中的回调。
    const server = http.createServer(this.callback());
    // 开启端口监听
    return server.listen(...args);
   * Return JSON representation.
   * We only bother showing settings.
   * @return {Object}
   * @api public
    // 返回一个对象,对象包含指定配置项,仅展示用。
  toJSON() {
    return only(this, [
      'subdomainOffset',
      'proxy',
      'env'
   * Inspect implementation.
   * @return {Object}
   * @api public
    // 参数检查
  inspect() {
    // 读取环境变量、代理地址、子域名
    return this.toJSON();
   * Use the given middleware `fn`.
   * Old-style middleware will be converted.
   * @param {Function} fn
   * @return {Application} self
   * @api public
    // 中间价函数入口(同时会转换旧的中间件函数)
  use(fn) {
    // 必须传入一个有效的生成器函数
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 新版本的koa使用了async wait的语法,所以检测generator函数进行转化,并引导用户使用新的语法
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn); // 生成器函数转为promise函数
    // 在指定的环境变量下输出中间件函数名称
    debug('use %s', fn._name || fn.name || '-');
    // 添加中间件函数进入中间件数组
    this.middleware.push(fn);
    return this;
   * Return a request handler callback
   * for node's native http server.
   * @return {Function}
   * @api public
    // 返回请求处理的回调函数
  callback() {
    // 通过compose建立中间件机制
    const fn = compose(this.middleware);
        // 如果没有error监听事件,则绑定error监听处理事件
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      // 将原生request和response挂在到koa的全局对象上,生成ctx(即context)
      const ctx = this.createContext(req, res);
      // 处理请求并执行中间件
      return this.handleRequest(ctx, fn);
        // 返回ctx生成函数
    return handleRequest;
   * Handle request in callback.
   * @api private
  // 在回调中处理请求
  handleRequest(ctx, fnMiddleware) {
    // 获取response响应
    const res = ctx.res;
    // 默认响应状态为404
    res.statusCode = 404;
    // 获取异常处理函数
    const onerror = err => ctx.onerror(err);
    // 对响应内容的输出处理
    const handleResponse = () => respond(ctx);
    // 执行http请求关闭、完成或错误时的回调事件
    onFinished(res, onerror);
    // 执行中间件并监听,返回执行结果
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
   * Initialize a new context.
   * @api private
    // 将原生request和response对象挂载到Koa全局对象中,生成新的context
  createContext(req, res) {
    // 创建一个对象,原型指向this.context的原型
    // 这里有一个常考知识点:Object.create(), new Object(), {} 的区别
    // 有兴趣细究看这:https://zhuanlan.zhihu.com/p/113015729 
    const context = Object.create(this.context);
    // 创建一个对象,原型指向this.request的原型,并保证request指向的是当前context中的request
    const request = context.request = Object.create(this.request);
    // 创建一个对象,原型指向this.response,并保证response指向的是当前context中的response
    const response = context.response = Object.create(this.response);
    // 保存原生request对象到context.app中,并将koa中request.app 和 response.app对象 指向同一个地址(即当前上下文所处的中间件函数请求体)。
    context.app = request.app = response.app = this;
    // 同上
    context.req = request.req = response.req = req;
    // 同上
    context.res = request.res = response.res = res;
    // 将请求和响应体的ctx对象都指向新生成的context
    request.ctx = response.ctx = context;
    // 同上
    request.response = response;
    // 同上
    response.request = request;
    // 保存原生请求头地址,暂时还不知道做用于什么。
    context.originalUrl = request.originalUrl = req.url;
    // 为koa中context生存请求状态占位池。
    context.state = {};
    // 返回挂载完毕的context
    return context;
   * 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.
    // ⬆️ 大致意思,jest中存在一个问题,instanceof在处理全局变量(具体指在http请求后,对响应头内容的检查工作中)时没有返回正确的Error对象而导致执行层抛出异常。
    // 而koa目前为了兼容老旧版本暂时对这个问题做了补丁式的处理,未来jest彻底解决这个问题后会移除这个补丁。(jest是js测试框架:https://www.jestjs.cn/)
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));
    // 忽略错误状态为404的错误
    if (404 === err.status || err.expose) return;
    // 如果有静默设置,则忽略
    if (this.silent) return;
        // 打印出错时的堆栈信息
    const msg = err.stack || err.toString();
    // 常用g,表示全局匹配,很少用m,查了一下,m表示多行匹配。将多行转化为空格。
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`);
   * Help TS users comply to CommonJS, ESM, bundler mismatch.
   * @see https://github.com/koajs/koa/issues/1513
    // 模块抛出的多方兼容写法
  static get default() {
    return Application;
 * Response helper.
// 针对响应内容的处理函数,返回给定的输出内容
function respond(ctx) {
  // allow bypassing koa
  // 略过没有内容的响应
  if (false === ctx.respond) return;
  // 略过没有输出的响应
  if (!ctx.writable) return;
  // 获取整个响应体
  const res = ctx.res;
  // 获取响应内容的主体部分
  let body = ctx.body;
  // 获取响应状态码
  const code = ctx.status;
  // ignore body
  // 过滤不符合http标准的状态码
  if (statuses.empty[code]) {
    // strip headers
    // 清空不符合http标准状态码的主题内容
    ctx.body = null;
    // 结束响应事件
    return res.end();
  // 获取 http 请求方法
  // method 共计15种请求方法:https://img.alicdn.com/imgextra/i4/O1CN01RlfU2G1nUdq8qBtLU_!!6000000005093-2-tps-1406-927.png
  // 请求方法说明(6种): https://www.runoob.com/http/http-methods.html 
  if ('HEAD' === ctx.method) {
    // 首次请求尚未发送出去时
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      // 这个length 对应 Content-Length 消息实体的传输长度
      // Content-Length MDN: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Length
      // Content-Length 常见问题及解法: https://www.jianshu.com/p/d606732f2ebc
      // 延伸阅读 Content-Type: https://www.jianshu.com/p/46d26cc5800b
      // 在首次请求没有发出去前先存储可能存在的消息实体的传输长度
      // [不知道为什么这一步是必要的?猜测可能是浏览器在发出预检的时候,服务器会优先返回一个传输长度?]
      const { length } = ctx.response;
      // 服务器返回的消息实体的传输长度为整数则保存在ctx中(app是koa中单个请求的实例、ctx则是完整请求的实体、每个请求koa不直接操作req和res,而是将原生res和req封装在request.js和response.js中并委托给ctx代理操作。)
      if (Number.isInteger(length)) ctx.length = length;
    // 若已发出请求则忽略并结束本次请求
    return res.end();
  // status body
  // 这里处理的是服务器返回了不合规则的响应状态或响应的消息实体为null时。
  if (null == body) {
    // 消息主体为空时
    if (ctx.response._explicitNullBody) {
      // Content-Type MDN: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type
      ctx.response.remove('Content-Type');
      // Transfer-Encoding MDN: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Transfer-Encoding
      // 延伸复习了一下 Accept-Encoding 和 Content-Encoding: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Encoding, https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Encoding
      // 了解 浏览器如何处理 Transfer-Encoding 和 Content-Encoding: https://www.cnblogs.com/digdeep/p/4808022.html
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    // httpVersionMajor 表示 HTTP 版本,这里的判断作用于抹平不同版本之间获取状态码字段的差异
    // HTTP 版本协议及对比: https://zhuanlan.zhihu.com/p/45173862
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    // 请求尚未发出时初始化ctx的部分属性
    if (!res.headersSent) {
      ctx.type = 'text';
      // 了解node中的Buffer: https://www.runoob.com/nodejs/nodejs-buffer.html
      // Buffer 是node.js中用于处理二进制数据流/文件的类,TCP中包含二进制数据流(再次get到一个TCP的土话:粘包)
      // TCP 流解析: https://segmentfault.com/a/1190000022104811
      // TCP 粘包: https://www.zhihu.com/question/20210025
      // 合并内容长度
      ctx.length = Buffer.byteLength(body);
    return res.end(body);
  // responses
  // 规范返回的消息体为流文件
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' === typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);
  // body: json
  // 在下一个消息发送出去之前存储当前内容的长度
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  res.end(body);