koa-compose 分析

452 阅读13分钟

原文链接

目录结构

  • application.js : koa 框架的核心实现逻辑
  • context.js : 返回创建的网络请求的上下文
  • request.js : 返回 request 请求信息
  • response.js : 返回 response 响应信息

应用实例

这里我们通过 koa 的官方脚手架初始化出来的项目代码,来看一下如何使用 koa 模块。

只提取关键路径代码进行分析

// 项目目录下
// bin/www 

var app = require('../app');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

// 定义端口
var port = normalizePort(process.env.PORT || '3000');
// app.set('port', port);

/**
 * Create HTTP server.
 */

// 创建一个 http 服务
var server = http.createServer(app.callback());

/**
 * Listen on provided port, on all network interfaces.
 */

// 监听在指定端口
server.listen(port);

代码非常的简单,整个过程只是通过 http.createServer 创建了一个 http 服务。

这里有一个 app.callback 可能是我们的疑问点,因为通常创建 http 服务的方式是这样的:

http.createServer(function (req, res) {
    // TODO
}).listen(3000);

那现在我们可以先猜测,app.callback 可能也会返回一个回调函数,并且在原先的基础上赋能,做了更多的事情。

接下来我们来看一下 app.callback 是在哪里定义的。

// 项目目录下
// app.js

const Koa = require('koa')
const app = new Koa()
const json = require('koa-json')
const onerror = require('koa-onerror')
const logger = require('koa-logger')

// error handler
onerror(app)

// middlewares
app.use(json())
app.use(logger())

// error-handling
app.on('error', (err, ctx) => {
	console.error('server error', err, ctx)
})

module.exports = app

这里做了四件事情:

  1. new 了一个 koa 的实例
  2. 定义了一个错误处理函数
  3. 通过 app.use 注册了一些中间件
  4. 在 koa 实例上订阅了一个叫 error 的事件消息
  5. 把整个koa 实例 exports 出来

代码中并没有定义 app.callback 的方法逻辑,接着往上找 koa 类。

中间件注册和消息订阅的具体实现会在后面具体分析

application.js

  • 注: 主入口的代码有点多,如果不理解可以先跳过,后面会逐个方法进行讲解
// 源码目录下
// application.js
// 核心入口

'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');
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 {

  constructor(options) {
    super();
    // 中间件执行队列
    this.middleware = [];
    // http 请求的上下文信息
    response和request.js一样,也是对http模块中的response对象进行封装,通过对response对象的某些属性和方法通过重写 getter/setter 函数进行代理
    this.context = Object.create(context);
    // 存储 request 相关的信息
    // request.js 主要是对原生的 http 模块的 request 对象进行封装
    this.request = Object.create(request);
    // 存储 response 相关的信息
    // response.js 主要是对原生的 http 模块的 response 对象进行封装
    this.response = Object.create(response);
  }

  /**
   * 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!');
    // 判断是否是 GeneratorFunction 
    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');
      // 若是 GeneratorFunction,则调用 koa-convert 模块的
      convert(fn) 方法将 generator 函数包装成 Promise
      // 为了兼容 1.x 的 koa 版本
      fn = convert(fn);
    }
    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
   */

  // 返回给 http.createServer(function) 用的回调函数(function)
  callback() {
    // 使用 koa-compose 组合生成一个大的中间件
    const fn = compose(this.middleware);

    // Application 类继承了 events 模块,this.listenerCount 和 this.on 都是 events 上的方法
    // 消息的订阅和分发机制
    // 如果没有开始订阅 error 事件,则订阅,当接收到 error 消息的时候执行 this.onerror 方法
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    // handleRequest 相当于 http.createServer 中的回调函数,req => 原生 request,res => 原生 response
    const handleRequest = (req, res) => {
      // 创建一个全新的 context(ctx) ,包含 response 、request 等信息
      const ctx = this.createContext(req, res);
      // 并将 context 透传到中间件中执行
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

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

  // fnMiddleware 是组合出来的中间件
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    // 错误处理函数
    const onerror = err => ctx.onerror(err);
    // 响应处理函数
    const handleResponse = () => respond(ctx);
    // 在 response 响应结束之后执行 context 上的 onerror 方法
    onFinished(res, onerror);
    // 执行所有中间件
    // 等中间件执行结束之后开始响应 response,这里表明了中间的执行时间节点
    // 中间件执行过程出现异常,也会执行 context 上的 onerror 方法
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

  /**
   * Initialize a new context.
   *
   * @api private
   */

  // 创建一个全新的 context 对象,建立 context 对象的 request 属性和 response 属性和原生 request 和 request 的联系
  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    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;
    context.state = {};
    return context;
  }

  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */

  onerror(err) {
    // 如果 err 不是 Error 的实例
    if (!(err instanceof Error)) 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();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
};

/**
 * Response helper.
 */

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  // 判断原生 response 是否是可写流
  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  // 获取响应的状态码
  const code = ctx.status;

  // ignore body
  // statuses 模块定义了一些状态码
  // 此处用来判断当前 statusCode 是否是 body 为空的类型
  // 当 statusCode 为 204、205和304时,body 返回 null
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  // HEAD 请求方式
  if ('HEAD' == ctx.method) {
    // headersSent 是 原生 response上的一个属性,返回响应头是否已经被发送。
    // 如果响应头还未发送 并且 body 是 JSON 对象
    if (!res.headersSent && isJSON(body)) {
      往 context 里写入 length
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  // 当 body 为 null 时
  if (null == body) {
    // httpVersionMajor 是原生 request 上的一个属性,返回当前 http 版本。
    // 这里是为了给http1 和 http2 做一个 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
  // 处理 body 为 Buffer 类型
  if (Buffer.isBuffer(body)) return res.end(body);
  // 处理 body 为 string 类型
  if ('string' == typeof body) return res.end(body);
  // 处理 body 为 流类型
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  // body 为 json 类型
  body = JSON.stringify(body);
  // 如果响应头还未发送,将 body 的字节长度写入到 context 中 
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

首先,在构造函数里初始化了一个存储中间件的数组、一个存储 request 信息的对象、一个存储 response 信息的对象和一个存储请求上下文的对象。初始化出来的请求上下文的 context 对象会在后续介绍,这里先大致理解它是一个包含 request 对象和 respond 对象 的请求相关信息的集合,它会将 request 对象和 respond 对象委托给自己代理。

找到这里终于找到了核心的 app.callback 逻辑,接下来分析一下 callback 具体做了什么事情。

callback

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */
  // 返回给 http.createServer(function) 用的回调函数(function)
  callback() {
    // 使用 koa-compose 组合生成一个大的中间件
    const fn = compose(this.middleware);

    // Application 类继承了 events 模块,this.listenerCount 和 this.on 都是 events 上的方法
    // 消息的发布订阅机制
    // 如果没有开始订阅 error 事件,则订阅,当接收到 error 消息的时候执行 this.onerror 方法
    // 默认错误处理方式
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    // handleRequest 相当于 http.createServer 中的回调函数,req === 原生 request,res === 原生 response
    const handleRequest = (req, res) => {
      // 创建一个全新的 context(ctx) ,包含 response 、request 等信息
      const ctx = this.createContext(req, res);
      // 并将 context 透传到中间件中执行
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

我们可以看到 callback 干了3件事情。

1.中间件组合

const fn = compose(this.middleware);

koa-compose 模块可以将多个中间组合成一个大的中间件,然后通过调用组合出来的中间件依次调用添加进来的中间件函数。不理解 koa-compose 工作原理的可以看一下 koa-compose 分析

这里我们可能会有一个疑问,那么中间件是怎么来的呢?

在 koa 项目的 app.js 文件中上面提到过 app.use 这个方法,现在我们先大致了解一下,koa 是通过 use 方法往实例中添加中间件的,后面会具体分析 use 到底做了什么。

const Koa = require('koa')
const app = new Koa()
const json = require('koa-json')
const logger = require('koa-logger')

// middlewares
app.use(json())
app.use(logger())

2.默认错误处理

if (!this.listenerCount('error')) this.on('error', this.onerror);

这行代码可能不是很好理解,因为在 application.js 的整个源码中并没有定义 listenerCounton 方法。

回头看一下我们在定义 Application 这个类的时候实际上继承了 event 模块的 Emitter 这个类,这是一个 消息分发订阅模式 的实现。

当 koa 实例上没有订阅 error 事件的时候,会默认订阅一个 error 事件,当接收到错误消息的时候执行 this.onerror 方法。

在 koa 项目的 app.js 文件中提到过,通过 app.on 我们可以订阅错误消息,如果在调用 callback 之前已经订阅过 error 事件,这里就不会再订阅执行默认操作。

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

// error-handling
app.on('error', (err, ctx) => {
	console.error('server error', err, ctx)
})

介绍到这里肯定会有一个疑问,既然订阅了消息,那么在哪里进行消息分发?我们接着往下看。

3.返回原生 http 请求的请求回调函数

return this.handleRequest(ctx, fn);

返回的新执行函数以 context 和组合后的中间件作为形参。通过 this.createContext 方法创建一个全新的请求上下文对象,包含 response 、request 等信息,并且创建相互之间的联系。

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

  // fnMiddleware 是组合出来的中间件
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    // 错误处理函数
    const onerror = err => ctx.onerror(err);
    // 响应处理函数
    const handleResponse = () => respond(ctx);
    // 在 response 响应结束之后执行 context 上的 onerror 方法
    onFinished(res, onerror);
    // 执行所有中间件
    // 等中间件执行结束之后开始响应 response,这里表明了中间的执行时间节点
    // 中间件执行过程出现异常,也会执行 context 上的 onerror 方法
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

在新返回的请求回调函数里做了以下几件事情:

  1. 在执行体里会定义一个错误处理函数,注意这里要区分开上面说的实例上订阅的错误处理。
  2. 紧接着定义了一个响应处理函数,这里先大概了解一下 respond 的主要职责就是对请求上下文做一下判断处理,然后结束响应。
  3. 在 response 响应结束之后执行 context 上的 onerror 方法。onFinished 是 on-finished 模块中定义的方法,当 response 响应结束之后会进行一些操作
  4. 依次执行全部中间件
  5. 中间件执行完毕之后进行响应处理(handleResponse
  6. 若在执行中间件期间报错或是在响应处理期间报错,都会调用 context.onerror 方法。

接下来我们来看一下 context 对象上的 onerror 方法具体干了什么(这里我们看一下关键节点)。

// context.js 文件
//

  // 默认错误处理函数
  onerror(err) {
  
    // delegate
    // 派发 error 事件消息
    this.app.emit('error', err, this);

    const { res } = this;
    res.end(msg);
  },

看到这里我们终于知道错误消息是从哪里派发出来的了,我们再来分析一遍整个错误订阅和分发模式。

  1. 首先 koa 实例会在 callback 方法执行时检查用户是否自己订阅了错误处理方法,如果没有则默认订阅一个错误处理函数 - this.onerror 方法。
  2. 在response响应结束的时候,或是当中间件执行期间产生报错,都会执行 context 对象上的 onerror 方法,这个方法会分发错误消息,通知所有订阅者进行错误处理。

看到这里我们可能还会有一个疑问,为什么 context 对象中定义的 onerror 方法是默认的处理函数,那我们可以在哪里修改错误处理的默认行为呢?我们回过头来看一下最初在实例化 koa 的时候干了什么。

const onerror = require('koa-onerror')

// error handler
onerror(app)

这里的 onerror(app) 会覆盖 context 对象上定义的默认 onerror 方法。koa-onerror 这个模块具体做了什么可以自行前往查看,实现比较简单。

use

 /**
   * 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!');
    // 判断是否是 GeneratorFunction 
    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');
      // 若是 GeneratorFunction,则调用 koa-convert 模块的
      convert(fn) 方法将 generator 函数包装成 Promise
      // 为了兼容 1.x 的 koa 版本
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 添加中间件
    this.middleware.push(fn);
    return this;
  }

当调用 app.use 注册中间件的时候,会先判断注册注册体是否为 function 类型,并且当中间件是 GeneratorFunction 类型的时候,会先调用 koa-convert 模块的 convert(fn) 方法将 generator 函数包装成 Promise,用于兼容 koa1.x。

最后将处理后的中间件添加到中间件数组中,并且返回当前实例对象,可以支持链式调用。

讲到这里,application.js 的源码基本上分析结束了,接下来我们来看一下 context.js 是如何返回一个上下文对象的。

context.js

// context.js
const delegate = require('delegates');

/**
 * Context prototype.
 */

const proto = module.exports = {
    // 隐藏了自身定义的一些方法
};

/**
 * Response delegation.
 */
 
 // 将 response 对象委托给 context

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .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.
 */
 
// 将 request 对象委托给 context

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');

在不知道 delegate 具体是干什么用的情况下,好像是将 context 、request 和 response 建立某种联系。接下来我们来分析一下 delegates 这个模块。

delegate 委托

module.exports = Delegator;

function Delegator(proto, target) {
  // 如果不是通过 new 标识符来创建实例,则帮你自动 new 一下,并返回一个实例
  // 可以支持链式调用
  // 这也是一种良好的兼容写法
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  // 被委托者
  this.proto = proto;
  // 委托者
  this.target = target;
  // 存储所有的方法
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

/**
 * Delegate method `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.method = function(name){
  // 这里指代 context 对象
  var proto = this.proto;
  // 这里指代 response 对象,或是 request 对象
  var target = this.target;
  this.methods.push(name);

  // 将 request 和 response 对象上的方法复制给 context 对象
  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };

  return this;
};

/**
 * Delegator accessor `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */
// 相当于组合调用了 getter 和 setter 方法
Delegator.prototype.access = function(name){
  return this.getter(name).setter(name);
};

/**
 * Delegator getter `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.getter = function(name){
  // 这里指代 context 对象
  var proto = this.proto;
  // 这里指代 request 对象和 response 对象
  var target = this.target;
  this.getters.push(name);
  // 通过原生的 __defineGetter__ 方法开启一层代理
  // 访问 proto[name] 就相当于访问 proto[target][name]
  // 这里访问 context[name] 就代理到访问 context.request[name]
  proto.__defineGetter__(name, function(){
    return this[target][name];
  });

  return this;
};

/**
 * Delegator setter `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.setter = function(name){
  // 这里指代 context 对象
  var proto = this.proto;
  // 这里指代 request 对象和 response 对象
  var target = this.target;
  this.setters.push(name);

  // 通过原生的 __defineSetter__ 方法开启一层代理
  // 修改 proto[name] 就相当于修改 proto[target][name]
  // 这里修改 context[name] 就代理到修改 context.request[name]
  proto.__defineSetter__(name, function(val){
    return this[target][name] = val;
  });

  return this;
};

委托代理的逻辑比较简单,主要就是通过 defineGetterdefineSetter 将 request 对象和 response 对象委托到 context 对象上,从而实现代理功能。

源码分析中并没有对 request.js 和 response.js 做过多介绍,这两个 js 主要职责就是对原生 http 模块的 request 和 response 对象进行二次封装。