1

koa 是什么这里不介绍了,这里通过一个小例子结合源码讲一讲它的实现。
<!-- more -->

koa 源码结构

通过 npm 安装 koa(v2.2.0) 后,代码都在 lib 文件夹内,包括 4 个文件,application.js, context.js, request.js, response.js。
图片描述

  • application.js 包含 app 的构造以及启动一个服务器

  • context.js app 的 context 对象, 传入中间件的上下文对象

  • request.js app 的请求对象,包含请求相关的一些属性

  • response.js app 的响应对象,包含响应相关的一些属性

本文主要关于 application.js 。

先看一个最简单的例子

// app.js
const Koa = require('koa')
const app = new Koa()

app.use(ctx => {
    ctx.body = 'hello world'
})

app.listen(3000)

然后通过 node app.js 启动应用,一个最简单的 koa 服务器就搭建好了,浏览器访问 http://localhost:3000,服务器返回一个 hello world 的响应主体。

源码分析

接下来通过源码看看这个服务器是怎么启动的。

const app = new Koa(), 很明显 Koa 是一个构造函数。

module.exports = class Application extends Emitter {}

Application 类继承了 nodejs 的 Events 类,从而可以监听以及触发事件。

看一下构造函数的实现。

constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);

构造函数定义了一些 app 的实例属性,包括 proxy, middleware, subdomainOffset, env, context, request, response等。

至此 我们就生成了一个 app 的koa实例。

接下来就该用 app.use(middleware) 来使用中间件了。

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;
  }

首先会验证传入的参数是否是一个函数。如果不是一个函数,会报错。之后如果传入的函数是一个generator 函数,那么会将这个函数转化为一个 async 函数。使用的是 koa-convert 模块, 这是一个很重要的模块,能将很多 koa1 时代下的中间件转化为 koa2 下可用的中间件。并且注意到

Support for generators will be removed in v3.

在 koa3 中,将默认不支持 generator 函数作为中间件。

之后将传入的中间件函数推入 middleware 数组中,并且返回 this 以便链式调用。

app.use() 只是定义了一些要使用的中间件,并将它们放入 middleware 数组中,那么怎么使用这些中间件。来看看 app.listen 方法。

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

app.listen 算是 node 原生 listen 方法的语法糖。通过 app.callback 方法生成一个 http.createServer 方法所需要的回调函数,然后再调用原生 http server 的 listen 方法。事实上也可以发现,app 的 listen 方法接收 http server 的 listen 方法一样的参数。

那么再看看 app 的 callback 这个方法了,也是最重要的一个方法。

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

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

    const handleRequest = (req, res) => {
      res.statusCode = 404; // 默认为 404 
      const ctx = this.createContext(req, res);
      // 根据 node.js 原生的 req, res 对象生成一个 ctx 对象
      const onerror = err => ctx.onerror(err);
      // onerror 回调函数
      const handleResponse = () => respond(ctx);
      // 处理服务器响应
      onFinished(res, onerror);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }

可以看到,callback 方法返回的 handleRequest 函数就是 http.createServer 方法所需要的回调函数。

callback 函数内,首先通过 koa-compose 模块将所有的中间件合并成一个中间件函数,以供 app.use 方法调用。随后监听一个 error 事件,onerror 作为默认的错误处理函数。

onerror(err) {
    assert(err instanceof Error, `non-error thrown: ${err}`);

    if (404 == err.status || err.expose) return;
    if (this.silent) return;
    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }

onerror 函数只是仅仅输出 error.stack 作为错误信息。

handleRequest 函数内完成了对请求的处理以及对响应结果的返回。首先 app.createContext 方法生成一个 ctx 供中间件函数 fn 调用。

createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    // request 属性
    const response = context.response = Object.create(this.response);
    // response 属性
    context.app = request.app = response.app = this;
    // request 和 response 上获得 app 属性,指向这个 app 实例
    context.req = request.req = response.req = req;
    // req 属性,req 是原生 node 的请求对象
    context.res = request.res = response.res = res;
    // res 属性,res 是原生 node 的响应对象
    request.ctx = response.ctx = context;
    // request 和 response 上获得 ctx 属性,指向 context 对象
    request.response = response;
    response.request = request;
    // request 和 response 互相指向对方
    context.originalUrl = request.originalUrl = req.url;
    // 获得 originalUrl 属性,为原生 req 对象的 url 属性
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    }); // cookie 属性
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    // ip 属性
    context.accept = request.accept = accepts(req);
    // accept 属性,是个方法,用于判断 Content-Type 
    context.state = {};
    // context.state 属性,用于保存一次请求中所需要的其他信息
    return context;
  }

所以,createContext 方法将一些常用的属性,如 resquest , response, node 原生 req, res 挂载到 context 对象上。

再来看这句话 const handleResponse = () => respond(ctx).

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  const res = ctx.res;
  if (!ctx.writable) return;

  let body = ctx.body; // 响应主体
  const code = ctx.status; // 响应状态码

  // ignore body
  if (statuses.empty[code]) { // 这里具体是指 204 205 304 三种
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) { // 如果是 `HEAD` 方法
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }
  // status body
  if (null == body) { // 如果没有设置 body , 设置ctx.message 为 body。当然默认是 Not Found ,因为 status 默认是 404
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }
  // 以下根据 body 的类型,决定响应结果
  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

所以 respond 函数的作用就是,根据传入的 ctx 对象的 body ,method 属性来决定对 request 处理的方式以及如何 response。

onFinished(res, onerror)

首先看看 onFinished(res, listener) 函数的介绍

Attach a listener to listen for the response to finish. The listener will be invoked only once when the response finished. If the response finished to an error, the first argument will contain the error. If the response has already finished, the listener will be invoked.

也就是当服务端响应完成后,执行 listener 回调函数,如果响应过程中有错误发生,那么 error 对象将作为 listen 回调函数的第一个参数,因此 onFinished(res, onerror) 表示 当 koa 服务器发送完响应后,如果有错误发生,执行 onerror 这个回调函数。

return fn(ctx).then(handleResponse).catch(onerror)。来看看这一句,fn 之前说过了,是所有的中间件函数的 “集合”, 用这一个中间件来表示整个处理过程。同时 fn 也是一个 async 函数,执行结果返回一个 promise 对象,同时 handleResponse 作为其 resolved 函数,onerror 是 rejected 函数。

总结

总结一下,application.js 描述了 一个 koa 服务器(实例)生成的整个过程。

  • new Koa() 生成了一个 koa 实例

  • app.use(middleware) 定义了这个 app 要使用的中间件

  • app.listen 方法,通过 callback 将合并后的中间件函数转化成一个用于 http server.listen 调用的回调函数,之后调用原生的 server.listen 方法。

全文完


499311496
2.4k 声望53 粉丝

[链接]