前言

从egg-core中可以看出,egg的核心类继承于Koa,本着事物的追溯到本源的过程,Koa自然也逃不过去。
**

目录结构

  1. .
  2. ├── lib
  3. ├── application.js
  4. ├── context.js
  5. ├── request.js
  6. └── response.js
  7. └── package.json
  • application.js 是整个koa2 的入口文件,封装了context,request,response,以及最核心的中间件处理流程。
  • context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法
  • request.js 处理http请求
  • response.js 处理http响应

设计模式

  • HTTP服务
    • 处理HTTP请求request
    • 处理HTTP响应response
  • 中间件容器
    • 中间件的加载
    • 中间件的执行

HTTP服务:封装了nodejs原生的http模块。Nodejs http中文文档

  1. // http模块使用例子
  2. const http = require('http');
  3. const PORT = 3001;
  4. const router = (req, res) => {
  5. res.end(`this page url = ${req.url}`);
  6. }
  7. const server = http.createServer(router)
  8. server.listen(PORT, function() {
  9. console.log(`the server is started at port ${PORT}`)
  10. })

中间件容器

  1. const Koa = require('koa');
  2. let app = new Koa();
  3. const middleware1 = async (ctx, next) => {
  4. console.log(1);
  5. await next();
  6. console.log(6);
  7. }
  8. const middleware2 = async (ctx, next) => {
  9. console.log(2);
  10. await next();
  11. console.log(5);
  12. }
  13. const middleware3 = async (ctx, next) => {
  14. console.log(3);
  15. await next();
  16. console.log(4);
  17. }
  18. app.use(middleware1);
  19. app.use(middleware2);
  20. app.use(middleware3);
  21. app.use(async(ctx, next) => {
  22. ctx.body = 'hello world'
  23. })
  24. app.listen(3001)
  25. // 启动访问浏览器
  26. // 控制台会出现以下结果
  27. // 1
  28. // 2
  29. // 3
  30. // 4
  31. // 5
  32. // 6

以上结果主要是Koa.js的一个中间件引擎 koa-compose模块来实现的,也就是Koa.js实现洋葱模型的核心引擎。

  1. +----------------------------------------------------------------------------------+
  2. | |
  3. | middleware 1 |
  4. | |
  5. | +-----------------------------------------------------------+ |
  6. | | | |
  7. | | middleware 2 | |
  8. | | | |
  9. | | +---------------------------------+ | |
  10. | | | | | |
  11. | action | action | middleware 3 | action | action |
  12. | 001 | 002 | | 005 | 006 |
  13. | | | action action | | |
  14. | | | 003 004 | | |
  15. | | | | | |
  16. +---------------------------------------------------------------------------------------------------->
  17. | | | | | |
  18. | | | | | |
  19. | | +---------------------------------+ | |
  20. | +-----------------------------------------------------------+ |
  21. +----------------------------------------------------------------------------------+

application.js源码阅读

module.exports = class Application extends Emitter { // 继承于event事件
  constructor(options) {
    super();
    options = options || {};
    this.middleware = []; // 存放中间件函数的数组
    // 封装执行上下文的ctx,content文件主要做的是初始化原型,给定一些方法,例如:cookies、错误处理等等
    this.context = Object.create(context); 
    // 封装request,给request封装了一些属性和方法,例如请求中可以用ctx.request.query等等
    this.request = Object.create(request); 
    // 封装response,与request差不多,例如返回时候如果什么格式都可以ctx.body=xxxx
    this.response = Object.create(response); 

    // ... 省略代码
  }
  listen(...args) { // 创建http服务
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  use(fn) { // 中间件使用方法 => app.use(middleware1);
    // 必须是函数
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 如果是generator函数用koa-convert处理
    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 || '-');
    // 将函数push到存放中间件函数的数组
    this.middleware.push(fn);
    return this;
  }
  callback() {
    // 将存放的中间件函数用koa-compose处理
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror); // 继承于event监听错误,再做错误处理。
    const handleRequest = (req, res) => {
      // 重新封装原生的req和res到ctx中
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  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;
  }
}

koa-compose

Koa.js实现洋葱模型的核心引擎。非常的重要,代码必须一行不漏的看。

// 在application.js将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!')
  }
  return 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)
      }
    }
  }
}

返回的匿名函数因为是最关键的部分,所以单独拿出来说。
第一次执行:

let index = -1
return dispatch(0)

看看dispatch(0)执行的是什么。

function dispatch (i) {
  // 首先显示判断 i<==index,如果 true 的话,则说明 next() 方法调用了多次。
  if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  index = i // index = 0
  let fn = middleware[i] //fn = middleware[0],执行的是第一个中间件
  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)
  }
}

再讲到return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));之前先看看之前的demo,我参考了许多资料,大部分资料都是以总结性的发言(比较官方),对学习者来说很不友好,所以我觉得以我个人的角度上来说,按着代码运行的逻辑上来写代码,写一下直白易懂的流程。

const middleware1 = async (ctx, next) => { 
  console.log(1); 
  await next();  
  console.log(6);   
}

const middleware2 = async (ctx, next) => { 
  console.log(2); 
  await next();  
  console.log(5);   
}

const middleware3 = async (ctx, next) => { 
  console.log(3);
  await next();  
  console.log(4);   
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

可以通过上述代码可以想象的出第一次执行的return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));其实是(middleware1)return Promise.resolve(async (context, dispatch(1)));在middleware1里面执行了console.log(1);然后就必须await ``dispatch(1)了。如此反复,那么输出结果再最后一个await之前,执行的代码是console.log(1); console.log(2); console.log(3);
直到执行完middleware3的时候,那么运行了console.log(4);
middleware3执行完了之后,middleware2被await打通了,那么就执行console.log(5);
紧接着middleware1被await打通了,那么就执行console.log(6);

async (ctx, next=middleware2) => { 
  console.log(1); 
  await async (ctx, next=middleware3) => { 
    console.log(2); 
    await async (ctx, next=Promise.resolve()) => { 
      console.log(3);
      await next();  
      console.log(4);   
    }
    console.log(5);   
  }
  console.log(6);   
}

将上述的描述表达成代码就是上面的代码,这样的形式我觉得在我个人角度上是最好理解的方式了。
举个最简单的中间件的例子

const logger = async function(ctx, next) {
  let res = ctx.res;

  // 拦截操作请求 request
  console.log(`<-- ${ctx.method} ${ctx.url}`);

  await next();

  // 拦截操作响应 request
  res.on('finish', () => {
    console.log(`--> ${ctx.method} ${ctx.url}`);
  });
};

app.use(logger);

// 如果接受到请求的时候,控制台显示结果<-- GET /hello
// 返回请求的时候,控制台显示结果--> GET /hello

koa-convert

如果传入的中间件是generator函数用koa-convert处理,在了解koa-conver之前先了解一下co模块,co模块是保证generator函数自动执行。如果不了解generator函数的点Generator 函数的语法

co模块源码,看看co是如何让generator函数自动执行的。

const co = require('co');
const sleep = (num) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num)
    }, 1000);
  })
}
var gen = function* () {
  var f1 = yield sleep(1)
  console.log(f1);
  var f2 = yield sleep(2)
  console.log(f2);
  return 3
};
co(gen).then(function (){
  console.log('Generator 函数执行完成');
});

上面是一个使用co模块的简单例子,方便后面讲解。

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)
  return new Promise(function(resolve, reject) {
    // 在返回的 Promise 对象里面,co 先检查参数gen是否为 Generator函数。如果是,就执行该函数,得到一个内部指针对象;
    // 如果不是就返回,并将 Promise 对象的状态改为resolved。
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    // 关键的地方:执行onFulfilled
    onFulfilled();
    function onFulfilled(res) {
      var ret;
      try {
        // 第一次执行的时候 以上面例子为流程,ret = { value: Promise { <pending> }, done: false }
        // 这个Promise就是sleep(1)
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      // 再将这个promise放入next函数
      next(ret);
    }
    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function next(ret) {
      // 如果generator已经走到底了,那么直接resolve
      if (ret.done) return resolve(ret.value);
      // 确保每一步的返回值,是 Promise 对象。
      var value = toPromise.call(ctx, ret.value);
      // 使用then方法,为返回值加上回调函数,然后通过onFulfilled函数再次调用next函数。又会执行onFulfilled,从而返回是sleep(2)
      // 知道处理完成,那么执行到该函数的最上面一步{ value: 3, done: true }
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

再看一下toPromise方法

function toPromise(obj) {
  // 分别会对promis、对象、数组等写法做处理
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

所以说需要并发的时候可以有如下写法,都会被toPromise处理。

// 数组的写法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
    console.log(res);
}).catch(onerror);

// 对象的写法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).catch(onerror);

看完co模块后再来看koa-convert

function convert (mw) {
  //首先判断该参数mw是不是一个函数,如果该mw不是一个函数的话,就直接抛出一个异常提示,该中间件必须是一个函数。
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  // 判断该 mw.constructor.name !== 'GeneratorFunction' 是不是一个Generator函数,如果不是Generator函数的话,就直接返回该mw。
  if (mw.constructor.name !== 'GeneratorFunction') { // 如果是async函数是AsyncFunction
    // assume it's Promise-based middleware
    return mw
  }
  // 如果它是Generator函数的话,就会执行 converted 函数,该函数有2个参数,第一个参数ctx是运行Generator的上下文,第二个参数是传递给Generator函数的next参数。
  const converted = function (ctx, next) {
    // mw.call(ctx, createGenerator(next)),如果mw是一个Generator函数的话,就直接调用该Generator函数,返回return yield next(); 返回下一个中间件,然后使用调用co包,使返回一个Promise对象。该本身对象的代码会自动执行完
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

总结

koa完成了http的封装和中间件容器,其他功能可以根据业务需求通过中间件加入进去,留下了很大的灵活空间,不过提高了Web服务的开发成本。例如接口必须携带token并解析完校验之后才能请求接口,就可以写一个登陆限制的中间件来拦截请求,并做验证。以上就是koa的源码解读,希望对读者理解koa有不错的帮助。