Skip to content

手撸一个 Koa

An Example

const MyKoa = require('./application');
const app = new MyKoa();

async function f1(ctx, next) {
  console.log('f1');
  await next();
}

async function f2(ctx, next) {
  console.log('f2');
  await next();
}

async function f3(ctx, next) {
  console.log('f3');
  throw new Error('抛出一个 TEST!错误');
}

app.use(f1);
app.use(f2);
app.use(f3);

app.listen(3000, () => {
  console.log('[example] listening on port 3000');
});

Koa

文件树

├── README.md
├── package.json
└── lib
    ├── application.js // 入口文件
    ├── context.js
    ├── request.js
    └── response.js

application.js

const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');

class Application {
  constructor () {
    this.context = context;
    this.request = request;
    this.response = response;
    this.callbackFunc;
  }

    /**
   * 开启http server并传入callback
   */
  listen(...args) {
    const server = http.createServer(this.callback());
    server.listen(...args);
  }

  /**
   * 获取http server所需的callback函数
   * @return {Function} fn
   */
  callback() {
    return  (req, res) => {
      // req, res 分别为 node 的原生 request,response 对象
      console.log('[Application] callback in listen');
      const ctx = this.createContext(req, res);
      function respond() {
        // ctx.res.end(ctx.body);
        const content = ctx.body;
        if (typeof content === 'string') {
          ctx.res.end(content);
        } else if (typeof content === 'object') {
          ctx.res.end(JSON.stringify(content));
        }
      }

      this.callbackFunc(ctx).then(respond);
    }
  }

  /**
   * 构造 context:
   * 将 koa request/response 对象挂载在 ctx 上,并返回
   */
  createContext(req, res) {
    console.log('[Application] createContext');
    const ctx = Object.create(this.context);
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }

  /**
   * 挂载回调函数
   * @param {Function} fn 回调处理函数
   */
    use (fn) {
    console.log('[Application] use: 注册中间件 fn')
        this.callbackFunc = fn
    }
}

module.exports = Application;

context.js

context 作为请求的代理,挂载了 koa 的 request/response 对象

  • 代码
 module.exports = {
  get query() {
    return this.request.query;
  },

  get body() {
    console.log('[context.js] getting  body.')
    return this.response.body;
  },

  set body(data) {
    console.log('[context.js] setting body.')
    this.response.body = data;
  },

  get status() {
    return this.response.status;
  },   

  set status(statusCode) {
    this.response.body = data;
  }
};
  • 改造一下
let proto = {};

// 为 proto 名为 property 的属性设置getter
function delegateGet(property, name) {
  proto.__defineGetter__(name, function () {
    console.log('[context.js] delegateGet: ' + property + '.'+ name)
      return this[property][name];
  });
}

// 为proto名为property的属性设置setter
function delegateSet(property, name) {
  proto.__defineSetter__(name, function (val) {
    console.log('[context.js] delegateSet: ' + property + '.'+ name + ' 为 '+val)
    this[property][name] = val;
  });
}

// 代理的 setter/getter
// request
delegateGet('request', 'query');

// response
delegateGet('response', 'body');
delegateSet('response', 'body');
delegateGet('response', 'status');
delegateSet('response', 'status');

module.exports = proto;

request.js

const url = require('url');

module.exports = {
  get query() {
    console.log('[request.js] getter query')
    return url.parse(this.req.url,true).query;
  }
};

response.js

module.exports = {
  get body() {
    console.log('[response.js] getter body')
      return this._body;
  },
  set body(data) {
    console.log('[response.js] setter body: '+ data)
    this._body = data;
  },

  get status() {
    return this.res.statusCode;
  },
  set status(data) {
    if(typeof data !== 'number') {
      throw Error('status must be a number!'
    )
    this.res.statusCode = data
  }
};

Koa-Compose

a more real application.js

中间件回调函数,变成一个顺序执行的回调数组。

const  http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
const compose = require('./compose');

class Application {
  constructor () {
    this.context = context;
    this.request = request;
    this.response = response;
    // this.callbackFunc;
    this.middleware = [];
  }

    /**
   * 开启http server并传入callback
   */
  listen(...args) {
    const server = http.createServer(this.callback());
    server.listen(...args);
  }

  /**
   * 获取http server所需的callback函数
   * @return {Function} fn
   */
  callback() {
    return  (req, res) => {
      // req, res 分别为 node 的原生 request,response 对象
      console.log('[Application] callback in listen');
      const ctx = this.createContext(req, res);
      function respond() {
        // ctx.res.end(ctx.body);
        const content = ctx.body;
        if (typeof content === 'string') {
          ctx.res.end(content);
        } else if (typeof content === 'object') {
          ctx.res.end(JSON.stringify(content));
        }
      }

      // this.callbackFunc(ctx).then(respond);
      compose(this.middleware)(ctx).then(respond);
    }
  }

  /**
   * 构造 context:
   * 将 koa request/response 对象挂载在 ctx 上,并返回
   */
  createContext(req, res) {
    console.log('[Application] createContext');
    const ctx = Object.create(this.context);
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }

  /**
   * 挂载回调函数
   * @param {Function} fn 回调处理函数
   */
    use (fn) {
    console.log('[Application] use: 注册中间件 fn')
    // this.callbackFunc = fn
    this.middleware.push(fn);
    }
}

module.exports = Application;

step1. compose conception

/* 
 * 愿望: 依次执行 f1, f2, f3
 */
async function f1(next) {
  console.log('f1');
  await next();
  console.log('/f1');
}

async function f2(next) {
  console.log('f2');
  await next();
  console.log('/f2');
}

async function f3(next) {
  console.log('f3');
  if (next) await next();
  console.log('/f3');
}
/*
* 构造出一个函数,实现让三个函数依次执行的效果
*/
var next2 = async function () {
  await f3();
};

var next1 = async function () {
  await f2(next2);
};

module.exports =  function () {
    console.log('compose')
  f1(next1);
}

step2. compose design

function createNext(fn, next) {
    return async function () {
        await fn(next)
    }
}

const middlewares = [f1, f2, f3];
const len = middlewares.length;

let next = async () => {
  return Promise.resolve();
};

for(let i = len-1; i>=0; i--){
    next = createNext(middlewares[i], next);
}

module.exports =  function () {
    console.log('compose');
    next();
}

step3. compose realization

module.exports =  function(middlewares) {
  console.log('-- enter compose --');
  return async function(ctx) {
    // next 工厂
    function createNext(fn, next) {
      return async function () {
        await fn(ctx, next)
      }
    }

    let next = async () => {
      return Promise.resolve();
    };

    const len = middlewares.length;
    for(let i = len - 1; i >= 0; i--){
      next = createNext(middlewares[i], next);
    }

    await next();
    console.log('-- exit compose --');
  };
}

step4. compose recursion

思想: 所有的迭代与递归都能相互转换。

module.exports =  function(middlewares) {
  return async function(ctx) {
    // next 工厂 递归化
    function createNext(i) {
      fn = middlewares[i];
      if (!fn) {
        return Promise.resolve();
      }

      return Promise.resolve(
        fn(ctx, async () => await createNext(i + 1))
      );
    }

    await createNext(0);
  };
}

simplified source code

'use strict'
// const fn = compose(this.middleware);
// return fn(ctx).then(handleResponse).catch(onerror);

/**
 * Expose compositor.
 */
// 暴露compose函数
module.exports = compose

// compose函数需要传入一个数组队列 [fn,fn,fn,fn]
function compose (middleware) {
    console.log('compose')
  // compose函数调用后,返回的是以下这个匿名函数
  // 匿名函数接收两个参数,第一个随便传入,根据使用场景决定
  // 第一次调用时候第二个参数next实际上是一个undefined,因为初次调用并不需要传入next参数
  // 这个匿名函数返回一个promise
  return function (context, next) {
    // last called middleware #
    // 初始下标为-1
    let index = -1
    return dispatch(0)

    function dispatch (i) {
      // 如果传入i为负数且<=-1 返回一个Promise.reject携带着错误信息
      // 所以执行两次next会报出这个错误。将状态rejected,就是确保在一个中间件中next只调用一次
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))

      // 执行一遍next之后,这个index值将改变
      index = i

      // 根据下标取出一个中间件函数
      let fn = middleware[i]

      // next在这个内部中是一个局部变量,值为undefined
      // 当i已经是数组的length了,说明中间件函数都执行结束,执行结束后把fn设置为undefined
      // 问题:本来middleware[i]如果i为length的话取到的值已经是undefined了,为什么要重新给fn设置为undefined呢?
      if (i === middleware.length) fn = next

      //如果中间件遍历到最后了。那么。此时return Promise.resolve()返回一个成功状态的promise
      // 方面之后做调用then
      if (!fn) return Promise.resolve()

      // 调用后依然返回一个成功的状态的Promise对象
      // 用Promise包裹中间件,方便await调用
      // 调用中间件函数,传入context(根据场景不同可以传入不同的值,在KOa传入的是ctx)
      // 第二个参数是一个next函数,可在中间件函数中调用这个函数
      // 调用next函数后,递归调用dispatch函数,目的是执行下一个中间件函数
      // next函数在中间件函数调用后返回的是一个promise对象
      // 读到这里不得不佩服作者的高明之处。
      return Promise.resolve(fn(context, function next () {
        return dispatch(i + 1)
      }))
    }
  }
}