通过50行代码看Koa的核心

220 阅读6分钟

Koa 是一个小而美的node server框架,其优秀的中间件机制,为整个框架提供了强大的横向扩展能力;

Koa 核心源码只有四个文件,github源码地址,外加一些依赖包,但如果仅仅只是去理解 Koa 最核心的部分源码,理解中间件运行机制,其实并不需要全部看完核心源码的四个文件和这些依赖包,下面就来看看如何通过50行代码体现 Koa 最核心 http server 和中间件机制;

// application.js
const http = require('http');
const context = {};
function compose(middlewares) {
  return function(context, next) {
    let index = -1;
    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'));
      index = i;
      let fn = middlewares[i];
      if (i === middlewares.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 dispatch(0)
  }
}
module.exports = class Application {
  constructor(options) {
    this.middleware = [];
    this.context = Object.create(context);
  }
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  callback () {
    const fn = compose(this.middleware);
    return (req, res) => {
      this.context.req = req;
      this.context.res = res;
      return fn(this.context).then(() => {
        respond(this.context);
      }).catch((error) => {
        console.error(error);
      });
    };
  }
  use(fn) {
    this.middleware.push(fn);
    return this;
  }
}

function respond(ctx) {
  ctx.res.end(ctx.body);
}

利用上面50行源码启动一个监听3000端口的 http server 服务;

const Koa = require('./application');

const app = new Koa();

app.listen(3000);

裁减掉额外的对 context、 request 和 response 封装的语法糖,简化 respond 方法对返回数据的处理,再加上 koa-composekoa-compose源码)对中间件的处理,以上50行代码,就可以了解到 Koa 最核心的两个点:

一、http server功能

Koa 也是依赖 node 原生的 http 模块来实现 http server 的能力,关于 node 的 http 模块,这里不再深入介绍,先挖个坑,后面再单独写一篇文章,理解下 http 模块及相关的 net 模块;

可以看到,原生 http 模块可以通过几行代码就启动一个监听在 8000 端口的http服务,createServer 的第一个参数是一个回掉函数,这个回掉函数有两个参数,一个是请求对象,一个是响应对象,可以根据请求对象的内容来决定响应数据的内容;

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('node server on port 8000');
});
server.listen(8000);

在 Koa 中,createServer 回掉函数中的 req 和 res 会被保存到 ctx 对象上,伴随整个处理请求的生命周期,Koa 源码中的 request.js 和 response.js 就是对这两个对象添加了大量便捷获取数据和设置数据的方法,如获取请求的方法、请求的路径、设置返回数据体、设置返回状态码等操作,在本文的分析中,省略了这两部分;

二、中间件机制 middleware

比起 http server,中间件机制是 Koa 更具特色的特点,也是 Koa 框架的核心,在 Koa 生成的 app 对象上添加一个中间件并不复杂,如下代码,便可以添加一个中间件:

app.use((ctx, next) => {
  console.log('this is a koa middleware');
  next();
});

koa appliation.js 中提供了一个 use 方法,该方法只有一个参数,而且这个参数必须是一个函数,use会将这个函数 pushmiddleware 的数组中,这个数组就是保存整个 Koa 实例中间件的数组;

那么在中间件数组中保存这些中间件后,中间件是如何执行的呢?koa 源码中依赖的是一个 koa-compose 的中间件,将数组中所有中间件串联起来,每一个中间件函数执行的时候,会有两个参数:ctx:koa 的上下文对象,里面包含 http 请求原生req对象,http 请求原生res对象,以及koa封装的快捷方法;next:其实是下一个中间件的引用,当前中间件执行到 next 的时候,会进入下一个中间件函数中,以此类推,直到最后一个中间件执行完成,再依次向上执行上一个中间件 next 后的函数代码,执行完后再向上一个中间件,直到最后执行完第一个中间件 next 后的代码后,返回这次请求的执行结果; 下图是 koa 作者提供的一张中间件执行顺序示意图: 抽象出执行流程如下图:

中间件的规则

上面介绍了 koa 是如何依次执行所有中间件的,通过中间件机制,框架使用者也可以根据实际业务需求,开发满足业务需求的中间件;在向 Koa 中添加一个中间件的时候,需要满足那些要求,已经中间件执行有哪些规则呢?通过下面的问题,来回答这两个问题。

Q1.中间件需要满足什么样的规则?

A1:koa中对中间件只有一个条件:必须是一个函数,同步函数和异步函数都可以作为中间件,但如果是 generator 函数的话,会有一个警告:koa 3.x 不会再支持 generators 类型中间件;所以最好是如下形式的函数:

// 含有异步逻辑的中间件
app.use(async (context, next) => {
  console.log('middleware 1: something before next');
  await db.query();
  await next();
  console.log('middleware 1: something after next');
});
// 只有同步逻辑的中间件
app.use((context, next) => {
  console.log('middleware 2: something before next');
  next();
  console.log('middleware 2: something after next');
});
// 最后一个中间件,不需要再调用 next,但调用后不会报错,compose 函数监测到没有下一个中间件后,会执行一个空的 promise 后返回
app.use((context, next) => {
  context.body = 'response body';
  console.log("middleware 3: it's all~");
});

Q2.中间件的执行顺序是什么?

A2: 中间件的执行顺序取决于app.use()的调用时机,即中间件被 push到 middleware 中的顺序;

  • 最早被 app.use()/push 到中间件数组的中间件,next 之前的代码最早执行,next 之后的代码最晚执行;
  • 最后被 app.use()/push 到中间件数组的中间件,next 之前的代码最晚执行,但早于前面的中间件 next 后的代码执行, next 之后的代码也早于前面的中间件 next 后的代码执行,但晚于当前中间件 next 之前的代码;

Q3.中间件函数的第二个参数 next 一定要被调用吗?可以多次调用吗?

A3: 理论上 next 参数是对下一个中间件的引用,可以不被调用,但如果不主动调用 next 函数,后面的中间件将无法执行,当前请求执行完当前中间件后,将向上执行之前中间件 next 的后的代码,然后返回; next 函数在一个中间件中只能被调用一次,若超过一次,app 会抛错:new Error('next() called multiple times')

Q4. 中间件有两个参数:context 和 next,可以向中间件函数添加别的参数吗?

A4:中间件只有这两个参数,无法添加额外参数;如果上一个中间件处理后的数据,需要在后面中间件中使用,可以将数据挂到 context 对象下面,context 贯穿于整个请求的生命周期;

中间件实践总结

  1. 即使一个中间件中只包含同步逻辑,仍然会被 koa-compose 包装成一个 promise 对象,实际开发中很容易疏忽下一个中间件是同步执行还是异步执行,所以最好将所有中间件变为异步函数,在调用 next() 的时候,加上 await 关键字,如下:
app.use(async (context, next) => {
  console.log(111);
  await next();
  console.log(222);
});

同步和异步,以及 async/await 函数、promise 对象的关系,可以在这里查看:Promise对象异步操作和Async函数

  1. next 不可以被多次调用;如果不需要调用后面的中间件,可以不调用 next,如用户鉴权失败或未登录,直接返回返回失败,则可以在一个中间件中判断后,直接返回,而不用调用 next 下一个中间件;

查看完整代码: codesandbox.io/s/little-ni…