koa源碼閱讀之koa-compose/application.js

koa源碼閱讀之koa-compose/application.js

koa-Compose

爲了理解方便特意把註釋也粘進來node

//這英語。我也來翻譯一波
//大概就是把全部的中間件組合返回一個完整大塊的中間件
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
//開始他的魔法
// middleware傳入的是一個數組 返回的是一個函數
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
   */
  // 上下文對象 返回一個promise
  // 注意上面的所謂返回函數其實是return這個匿名函數。這個匿名函數返回一個promise
  // tj對於異步回調的處理方案太強了

  return function (context, next) {
    // last called middleware #
    let index = -1
    //初始下標爲-1
    return dispatch(0)
    
    
    function dispatch (i) {
      // 若是傳入i爲負數且<=-1 返回一個Promise.reject攜帶着錯誤信息
      // 這裏也就是確保了next只調用一次
      // 執行一遍next以後,這個index值將改變,由於下面有一個賦值
      // 因此執行兩次next會報出這個錯誤。將狀態rejected
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // index = i;
      index = i
      // 取出一箇中間件
      let fn = middleware[i]
      // 這塊處理在application中能夠發現。實際上next是個undefined
      // 也就是在Promise鏈式最深處是一個Promise.resolve
      if (i === middleware.length) fn = next
      //若是中間件遍歷到最後了。那麼。此時咱們最後return Promise.resolve()
      // 這個Promise reloved狀態
      if (!fn) return Promise.resolve()
      // try catch保證錯誤在Promise的狀況下可以正常被捕獲。
      // 這是一個遞歸。
      // 咱們知道Promise.resolve的狀態是resolve
      // 而當Promise.resolve(value) value爲一個promise的時候。
      // 返回的是傳入的這個promise
      // var d = Promise.reject('test')
      // var z = Promise.resolve(d)
      // z == d
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

若是你用過koa的話。如今你應該理解了next洋蔥體的奧妙了。


application.js

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 Cookies = require('cookies');
const accepts = require('accepts');
const Emitter = require('events');
const assert = require('assert');
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`.
 */
// appliaction繼承了nodejs 的Events模塊
module.exports = class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */
  // 初始化
  constructor() {
    super();
    
    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    // 這個很常見,區分開發仍是線上模式
    this.env = process.env.NODE_ENV || 'development';
    //前面幾篇講過的文件
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */
  // 這塊的用了個小細節。
  // http.createServer(app.callback()).listen(...)
  // 其實就是上面這個的形式
  listen(...args) {
    debug('listen');
    //callback是個很主要的點。
    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
   */
  // 咱們使用koa的時候.
  // var koa = require('koa')();
  // koa.use(async function(context,next){})
  // 這樣子就至關因而一個簡單的中間件了。
  use(fn) {
    // 判斷傳入類型 須要是函數
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 若是是generator函數的話,那麼咱們應該使用conver轉化一下。
    // 上一篇個人文章就有提到這個庫。這裏不提了
    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 || '-');
    //在this.middleware壓入這個函數
    // 這裏請聯想一下koa-compose是傳入什麼的。
    this.middleware.push(fn);
    // 提供鏈式調用
    return this;
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */
  // 當callback用在createServer回調內。那麼應該是一個function(req,res)的函數
  callback() {
    const fn = compose(this.middleware);
    // 使用compose處理咱們的middleware中間件數組
    // 返回一個promise
    if (!this.listeners('error').length) this.on('error', this.onerror);
    
    // 唔 就是這個handleRequest
    const handleRequest = (req, res) => {
      // 默認設置statuscODE
      res.statusCode = 404;
      //  建立的上下文對象
      const ctx = this.createContext(req, res);
      // onerror函數
      const onerror = err => ctx.onerror(err);
      const handleResponse = () => respond(ctx);
      //這個也是說過的啦
      onFinished(res, onerror);
      // compose middlewares數組返回一個promise
      // 由於咱們內部有trycatch顯式地拋出狀態,因此在鏈上能夠catch
      // fn(ctx,undefined);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }

  /**
   * Initialize a new context.
   *
   * @api private
   */
  //建立咱們中間件用的ctx對象
  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
    //         ---req--->request ---req--->response---req --->req(原生可讀流)
    //         ---res--->request ---res--->response---res --->res(原生可寫流)
    //                           ---ctx--->response---ctx---->context
    //                           ---response->response
    //                                        response--request--->request
    // 這塊注意區分req request res 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.js文件裏面有一大堆屬性方法的委託吧。那麼意味着咱們能夠直接
    // ctx.body==>ctx.response.body
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }

  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */
  // 默認錯誤處理
  onerror(err) {
    assert(err instanceof Error, `non-error thrown: ${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.
 */
// 能夠想成res響應體輔助
function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;
    
  const res = ctx.res;
  if (!ctx.writable) return;

  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) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  // 三種處理  buffer 字符串 流
  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
  // bodyJSON字符串序列化 handle ctx.body是對象
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

主要的點在於koa-compose的包裝非常重要。
感受tj在這方面的功底真的太強了。
異步,next鏈式組裝方式。都很強。git


接下來文章的內容

繼續閱讀幾個koa中間件。而且本身寫點小demo在文章內,那麼koa系列就結束啦。

若是這個文章幫助到你。

不妨給我一個贊/或者將這篇文章收藏起來。由於那是對我最好的鼓勵

若是有什麼錯誤的地方也不妨告訴我thanks


參考資料:

相關文章
相關標籤/搜索