Koa 拾遗 一 : Koa 的中间件机制

929 阅读4分钟

Node.js HTTP 模块

Node.js HTTP 模块怎么使用?

Node.js 中的 HTTP 模块, 是 Node 可以作为 Webserver 的基础. 是一个原生的模块. 基于这个模块, Node.js 有了处理 HTTP 请求的能力. 使用 HTTP 模块创建一个 HTTPServer 非常简单, 只需要短短的几行代码

const http = require('http')

const server = http.createServer((req, res) => {
  // Handle request and response
  rse.end(`The request path is ${req.url}`);
})

server.listen(3000, () => {
  console.log('The server is listening at 3000')
})

这就创建了一个最简单的 HTTP 服务器. 我们可以看到, 在createServer中的回调函数, 就是我们整个 server 的服务逻辑.在这个例子里面, 我们直接返回了用户请求的 url 信息.

  const server = http.createServer((req, res) => {
    // Handle request and response
    rse.end(`The request path is ${req.url}`);
  })

这个回调函数里面, 有两个参数, 一个是req, 另外一个是res. 他们分别是 Node 的 HTTP 模块对于用户的请求以及响应的一个封装. 具体的文档, 可以参考 http.ClientRequest 以及 http.ServerResponse. 这里内容有点多, 后面会单独写一遍文章进行相应的介绍.这里我们只需要知道所有 HTTP 的操作都是基于在两个对象上面进行的就可以了.

Koa.js, 本质上就是对于 HTTP 模块的一个封装, 并且提供了中间件的加载能力.参照源码, 我们发现, Koa.js的逻辑, 都封装在了callback这个函数中, 这最重要的函数, 则是 compose. 因而要研究Koa.js, 本质上就是在研究compose这个函数在干什么.

module.exports = class Application extends Emitter {
  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */

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

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

    if (!this.listenerCount("error")) this.on("error", this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
};

Koa.js 中的中间件

Koa.js提供了两个功能, 一个是HTTP的请求和响应, 这个是对于 Node.js 中的HTTP模块的调用. 另外一个重要的功能, 则是中间件.

如何使用 Koa.js 的中间件?

根据官方文档的例子, 我们可以通过use函数, 把我们所需要的中间件挂载到Koa上面.

const Koa = require("koa");

const app = new Koa();

app.use(async function (ctx, next) {
  console.log(">> one");
  await next();
  console.log("<< one");
});

app.use(async function (ctx, next) {
  console.log(">> two");
  await next();
  ctx.body = "two";
  console.log("<< two");
});

app.use(async function (ctx, next) {
  console.log(">> three");
  await next();
  console.log("<< three");
});

app.listen(8080);


/**
 * 访问浏览器, 我们可以看到
 * >> one
 * >> two
 * >> three
 * << three
 * << two
 * << one
 * /

上述例子可以在 codesanbox 中查看 example 我们会看到, next函数执行之前的语句会依次执行, 然后则是按照倒序执行next函数之后的函数, 有点栈的感觉. 为什么会出现这个结果呢? 这个就不得不提到 koa中老生常谈的洋葱圈模型.

什么是洋葱圈模型?

洋葱圈模型, 实际上就是指一个 HTTP 的请求, 会在请求的时候路过一次, 返回的时候再路过一次. 他们的分界点就是在next函数上面. 因而一次中间件有两次机会可以对一次完整的HTTP请求做处理.

             +-----------------+
             |                 |
             |   +----------+  |
             |   |          |  |
             |   |  +----+  |  |
             |   |  |    |  |  |
    Request  |   |  |    |  |  |  Response
------------------->|    |------------------>
             |   |  |    |  |  |
             |   |  |    |  |  |
             |   |  +----+  |  |
             |   |          |  |
             |   +----------+  |
             |                 |
             +-----------------+

中间件引擎的实现, koa-compose

对于 middleware 的特性, 我们可以参考栈进行总结.

  • 挂载的操作 First in, last out.
  • 有统一的上下文 ctx.

回到最上面提到Koa初始化的例子, Koa在创建一个 HTTP Server 的时候, 会把callback作为回调函数传入.


  use (fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn) // use 函数把我们的中间件加入到 middleware 的数组中
    return this
  }

  callback () {
    const fn = compose(this.middleware) // 把 middleware 数组传入到 compose 函数, 得到一个新的函数.

    if (!this.listenerCount('error')) this.on('error', this.onerror)

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }

    return handleRequest
  }


  handleRequest (ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror) // 在 handleRequest 函数中, 直接就是把 ctx 对象传入到 fn 中进行执行.
  }

compose 函数则是整个 koa 的核心. 刨除一些异常处理的语句, 我们看看 compose 做了什么

function compose (middleware) {
  return function (context, next) {
    // 记录上一次调用的中间件
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i // 赋值中间件索引
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) // 把下一个中间件的索引通过 bind 绑定
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

这里通过 bind 的链式调用, 把下一个挂载的中间件作为 next参数传入, 达到了控制中间件队列的目的. 同时context对象也是作为参数一并传入.

Koa.js 的核心实现

理解了 compose的原理, 我们就可以自己动手去实现一个简单的 koa.js了.

让我们先创建一个Application的类. 然后把核心的函数编写好, 主要是两大类, 分别是 HTTP 请求处理的handleRequest, 以及中间件相关的use, callback.

const Emitter = require("events");
const compose = require("koa-compose");
const http = require("http");

module.exports = class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */
  constructor(options) {
    super();
    this.middleware = [];
    this.context = Object.create({});
  }

  /**
   * 调用底层 HTTP 模块 createServer
   */
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  /**
   * 挂载中间件
   */
  use(fn) {
    if (typeof fn !== "function")
      throw new TypeError("middleware must be a function!");
    this.middleware.push(fn);
    return this;
  }

  /**
   * 创建通用上下文
   */
  createContext(req, res) {
    let context = Object.create(this.context);
    context.req = req;
    context.res = res;
    return context;
  }

  /**
   * HTTP 请求处理的回调函数
   */
  callback() {
    const fn = compose(this.middleware); // 这里我们依旧借助 koa-compose.

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res); // 创建请求的上下文
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const handleResponse = () => ctx.res.end();
    return fnMiddleware(ctx).then(handleResponse);
  }
};

大家有兴趣可以尝试在 codesandbox 上面运行这里例子, 看看情况, 我这里也把自己实验的地址贴在这里了. Koa example

总结

Koa 通过对Context对象的创建, 以及中间件机制的设计, 奠定了整个框架的运行和后续开发的方向. 后续所有的功能, 例如我们常见的koa-router, koa-logger等, 都是通过中间件的形式进行开发加载. 后面的内容, 主要则是围绕在如何通过开发中间件去扩展 koa 的功能.