Koa2源码解读-application.js

231 阅读6分钟
原文链接: zhuanlan.zhihu.com

入口文件

通过上文我们知晓从packaje.json文件的main字段找到了应用的入口文件。

{
 "main": "lib/application.js"
}

接下来我们看看application.js。


我们先从一段最简单的Koa应用代码开始看起

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
'use strict';

/**
 * Module dependencies.
 */

const isGeneratorFunction = require('is-generator-function');
const debug = require('debug')('koa:application');
const onFinished = require('on-finished');
const response = require('./response');
const compose = require('koa-compose');
const isJSON = require('koa-is-json');
const context = require('./context');
const request = require('./request');
const statuses = require('statuses');
const Emitter = require('events'); // 继承了EventEmitter,很轻松实现订阅发布模式
const util = require('util');
const Stream = require('stream');
const http = require('http');
const only = require('only');
const convert = require('koa-convert');
const deprecate = require('depd')('koa');

/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */

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
    *
    */

  constructor(options) {
    super();
    // 外界传入的构造函数对象
    options = options || {};
    // 是否开启代理,默认false
    // 它的一些使用场景: 现在使用nginx做负载均衡的实例很多, 用户 => CDN => Nginx => 分发至n个服务
    // X-Forwarded-For: 它用来表示HTTP请求端的真实IP, 格式为: X-Forwarded-For:client, proxy1, proxy2
    // 最开始的离服务端最远的设备IP, 然后依次是每一级的代码设备IP,因此只有通过了HTTP代理或者负载均衡服务器才会添加该项
    // 如果开启了代理,想要获取request请求中的host,protocol,ip则需要从Header字段中的X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For获取
    this.proxy = options.proxy || false;
    // 子域名的偏移量,子域是应用程序主域名之前主机的点分隔部分,域名的优先级从右往左(高至低)
    // subdomainOffset决定了request.subdomains返回的子域数组
    // 如果域名为"tobi.ferrets.example.com"
    // 默认情况下(app.subdomainOffset 是 2) ctx.subdomains 是['ferrets', 'tobi']
    // 如果 app.subdomainOffset 是 3, ctx.subdomains 是 ["tobi"]
    this.subdomainOffset = options.subdomainOffset || 2;
    // env: node执行环境
    this.env = options.env || process.env.NODE_ENV || 'development';
    // keys: 设置签名的cookis秘要
    if (options.keys) this.keys = options.keys;
    // 存放中间件
    this.middleware = [];
    // 中间件的第一个参数ctx原型, 上下文
    this.context = Object.create(context);
    // ctx.request的原型
    this.request = Object.create(request);
    // ctx.response的原型
    this.response = Object.create(response);
    if (util.inspect.custom) {
      // 在node v6.6.0及以上版本中util.inspect.custom是一个Symbol类型的值
      // 通过定义对象的[util.inspect.custom]属性为一个函数,可以覆盖util.inspect的默认行为。
      // 文档: http://nodejs.cn/api/util.html#util_util_inspect_custom
      this[util.inspect.custom] = this.inspect;
    }
  }

  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */

  listen(...args) {
    debug('listen');
    // 文档: http://nodejs.cn/api/http.html#http_http_createserver_options_requestlistener
    // this.callback()是一个自动添加到 'request' 事件(http.Server 类)的函数。
    // 也就是说每次有请求触发的时候都会执行this.callback()
    /**
     * @description 可以自己写demo测试
     *  const http = require('http');
     *  let i = 0;
     *  const proxy = http.createServer((req, res) => {
     *    res.writeHead(200, { 'Content-Type': 'text/plain' });
     *    i += 1;
     *    res.end('响应内容: ' + i);
     *  });
     *  proxy.listen(1337, '127.0.0.1', () => {
     *    console.log('server is running 1337')
     * })
    */
    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!');
    // 如果中间件是生成器函数, 则提示让后进行转化
    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);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 保存该中间件到middleware
    this.middleware.push(fn);
    // 返回实例本身支持链式调用
    return this;
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware);
    // compose(this.middleware): 就是把中间件数组中所有的函数合并成一个函数并返回
    // next()方法是一个Promsie, 所以我们可以这样使用 await next()

    /**
     *  @description fn
     *  function (context, next) {
          // last called middleware #
          let index = -1
          return dispatch(0)
          function dispatch (i) {
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i
            let fn = middleware[i]
            if (i === middleware.length) fn = next
            if (!fn) return Promise.resolve()
            try {
              return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
              return Promise.reject(err)
            }
          }
        }
    */

    // 默认提供了一个error事件的错误处理回调
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    // handleRequest: 就是之前我们说的每接受一个请求就会触发的函数
    // 文档: http://nodejs.cn/api/http.html#http_event_request
    // req: <http.IncomingMessage>
    // res: <http.ServerResponse>
    const handleRequest = (req, res) => {
      // 创建一个上下文对象ctx
      const ctx = this.createContext(req, res);
      // this.handleRequest才是真正处理每一个请求的回调
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  handleRequest(ctx, fnMiddleware) {
    // node原生对象 http.ServerResponse 类
    const res = ctx.res;
    // 设置默认的状态码
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    // 通过第三方库on-finished监听http response
    // 当请求结束时执行回调,这里传入的回调是context.onerror(err),即当错误发生时才执行
    onFinished(res, onerror);
    // 开始执行中间件(传入请求上下文对象ctx)
    // 之后执行响应处理函数(respond(ctx))
    // 当抛出异常时同样使用onerror(err)处理所有中间件的异常
    // fnMiddleware(ctx)其实就是
    // koa-compose 中的执行 fn(context, dispatch.bind(null, i + 1));
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

  /**
   * Initialize a new context.
   * 创建上下文对象, 针对每一个请求
   * @api private
   */

  createContext(req, res) {
    // req: 原生的node请求对象  res: 原生的node响应对象
    const context = Object.create(this.context);
    // 挂在Koa2的请求对象, 响应对象到上下文对象ctx
    // 因此可以通过ctx.request, ctx.response来访问
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    // 提供访问应用实例本身的快捷方法
    context.app = request.app = response.app = this;
    // 可以通过ctx.req, ctx.res访问Node原生的请求对象, 响应对象
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    // 初始化当次请求的state,用来存储信息在中间件中共享
    context.state = {};
    return context;
  }

  /**
   * Default error handler.
   * @description 当发送错误时候,会触发Koa实例上的error事件来打印一个错误日志, 错误处理方法支持重写
   * @param {Error} err
   * @api private
   */

  onerror(err) {
    // 如果err不是实例,那么自定义一个TypeError提示
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
    // 不会输出错误日志的情况有: app.silent为true, err.status = 404, err.expose = true
    if (404 == err.status || err.expose) return;
    if (this.silent) return;

    // 输出错误堆栈信息
    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
};

/**
 * Response helper.
 * @description 内置的响应处理函数
 */

function respond(ctx) {
  // allow bypassing koa 允许显示设置ctx.respond = false来跳过Koa内置的respond处理
  // 使用的场景是你想要自定义处理res对象
  if (false === ctx.respond) return;

  // 不可写的情况
  if (!ctx.writable) return;

  const res = ctx.res;
  // 获取body, status
  // body可能的值类型有:string, Buffer, Stream, Object
  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      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);
  }
  // HTTP请求的的最后返回值
  res.end(body);
}

扩展知识

  • koa-compose
'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  // 首先是一些中间件格式校验
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    // 返回一个函数, 从第一个中间件开始执行, 可以通过next()调用后续中间件
    return dispatch(0)
    // dispatch始终返回一个Promise对象
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // next即就是通过dispatch(i+1)来执行下一个中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        // 捕获中间件中发生的异常
        return Promise.reject(err)
      }
    }
  }
}