Koa2.0 中间件的组织嵌套

官方描述: Contrasting Connect's implementation which simply passes control through series of functions until one returns, Koa invoke "downstream", then control flows back "upstream".
说人话: 相比较通过把控制权在中间件队列中按顺序传递,知道返回最后的结果的简单传递方式。Koa 采用先把控制权按顺序中间件加载的顺序向下传递,然后再把控制权原路传递回来。

从一个示例开始

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

// The first middle ware.
app.use(async (ctx, next) => {
    await console.log(`The 1.1 middleware.`);
    await next();
    await console.log(`The 1.2 middleware`);
});

// The second middle ware.
app.use(async (ctx, next) => {
    await console.log(`The 2.1 middleware.`);
    await next();
    await console.log(`The 2.2 middleware`);
});

// The third middle ware.
app.use(async (ctx, next) => {
    await console.log(`The 3.1 middleware.`);
    ctx.body = "Hello world.";
    await console.log(`The 3.2 middleware`);
});

app.listen(3000, () => {
    console.log(`Server port is 3000.`);
})

上面Demo的打印输出依次是:

The 1.1 middleware.
The 2.1 middleware.
The 3.1 middleware.
The 3.2 middleware
The 2.2 middleware
The 1.2 middleware
The 1.1 middleware.

下图就是middle ware work flow的流程图, 我们可以很明显看出downstream与upstream的流程。


Koa2.0 中间件的组织嵌套_第1张图片
middle ware work flow

在使用Koa 框架构建项目时, 通过use() 加载中间件,最后使用listen() 创建Web Server 用来监听、接收并处理HTTP请求,并在最终对请求予以响应。

通过use() 加载中间件

所谓的中间间就是把函数作为use 的参数,由于Koa 1.0时代use的参数是Generator 函数,Koa 2.0 对传进来的Generator 函数做了些特殊处理。最后把加载的中间件添加到middle ware数组中。

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    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.push(fn);
    return this;
  }

通过listen() 创建Web Server

通过listen() 监听端口,也就在这个时候系统创建了WebServer。在Http.createServer()方法中处理request请求和response 响应的放回调函数。

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
}

在callback 函数中,先是把中间件数组中的中间件串联在一起。然后通过req,与res 对象组建context,以用来响应web server 的请求。

  callback() {
    const fn = compose(this.middleware);

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

    const handleRequest = (req, res) => {
      res.statusCode = 404;
      const ctx = this.createContext(req, res);
      const onerror = err => ctx.onerror(err);
      const handleResponse = () => respond(ctx);
      onFinished(res, onerror);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }
  • koa-compose 将中间件串联在一起

Async/Await 返回的都是Promise 对象,compose() 利用Promise 函数状态的性质,将middle ware 数组转换为Promise 对象的实例,实现中间件数组的递归嵌套。

koa-compose 源码通过递归函数dispatch() 将middle ware list里面的中间件串联在一起。

若中间件是async 函数
中间件的参数有两个:一个是context (Koa 的上下对象), 另一个是next 函数。next 函数指向的是下一个中间件(即下一个Promise 对象),由于await 关键字会等下一个Promise 对象执行完毕,因此当前中间件就会被block,直至下一个中间件有Promise 对象的返回。在执行下一个中间件的时候也是遵循同样的逻辑,这样就实现了中间件的嵌套执行。
由于next函数指向下一个中间件,因此同一个中间件内只能有一个next函数只能被执行一次,因此源码里通过index 标记实现next函数调用次数的限制。

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) {
    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()
      //  实现middle ware 数组的嵌套递归
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

middle ware 数组通过koa-compose 第三方库转换后,如下图

Koa2.0 中间件的组织嵌套_第2张图片
Koa middle ware process

如中间件是普通函数 如何实现与async/await 一样的逻辑?
由于源码会把next 函数强制封装为Promise 对象,因此我们只要利用Promise对象的特性就可以实现阻塞进程的性质。

app.use((ctx, next) => {
  const start = new Date();
  return next().then(() => {
    const ms = new Date() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});
  • Context - Koa 的上下文对象
    把context 类作为按照特定规则组织后的中间件参数,实现多个中间件共同操作一个context 类,因此可以把context作为Koa 运行环境的上下文类。相比较于其他面向对象的语言,我们可以认为context 是Koa 运行环境的静态类。
callback() {
    const fn = compose(this.middleware);

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

    const handleRequest = (req, res) => {
      res.statusCode = 404;
      const ctx = this.createContext(req, res);
      const onerror = err => ctx.onerror(err);
      const handleResponse = () => respond(ctx);
      onFinished(res, onerror);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }

request - request继承于Request静态类,包含操作request的一些常用方法
response - response继承于Response静态类,包含操作response的一些常用方法
req - nodejs原生的request对象
res - nodejs原生的response对象
app - koa的原型对象

  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,request,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.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;
  }

你可能感兴趣的:(Koa2.0 中间件的组织嵌套)