Venus' Blog

Archive

About

Koa.js 源码浅析

2019 / 03 / 08

源码就 4 个文件,文件目录:

lib
├── application.js
├── context.js
├── request.js
└── response.js

从入口开始,一个简单 Koa 应用如下:

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

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

app.listen(3000)

Koa 这个类的定义在 application.js 文件中:

// ...
const Emitter = require('events')
// ...

module.exports = class Application extends Emitter {
  constructor() {
    super()
    
    // ...
    this.middleware = []    // ...
  }
  
  use(fn) {
    if (typeof(fn) !== 'function') throw new TypeError('middleware must be a function!  ')
    this.middleware.push(fn)    return this
  }
}

可见,在 Koa 实例 app 上使用的 use(fn) 方法,只是将一个函数 push 进 middlware 这个属性里而已。那 Koa 是怎样启动服务器的呢,我们来看看 listen() 方法:

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

listen() 方法中,Koa 便会新一个 http 模块的 Server 类,并调用了 this.callback() 方法,把这个方法返回的东西传了进去。我们知道,内置的 http.Server 类需要传一个回调处理函数进去,例如:

const http = require('http')

http.createServer(function(request, response) {
  response.end('Hello')
}).listen(3000)

所以,this.callback() 应该也是返回了一个函数,这个函数就应该接收 http.IncomingMessage 类的实例和 http.ServerResponse 类的实例。callback() 方法的定义:

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

正如我们所想的,callback() 方法定义了一个 handleRequest 函数并返回了该函数,且该函数的参数就是 reqres。不过,callback() 这个方法稍微有点复杂,里面又用到了:

  • compose(this.middleware)
  • this.createContext(req, res):返回值传给 this.handleRequest 作为参数
  • this.handleRequest(ctx, fn)handleRequest 的返回值

我们先来看下 handleRequest 函数,里面使用到了 this.createContext(req, res)

const response = require('./response')
const context = require('./context')
const request = require('./request')

  constructor() {
    // ...
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
    // ...
  }

  createContext(req, res) {
    const context = Object.create(this.context)
    const request = context.request = Object.create(this.request)
    const response = context.response = Object.create(this.resopnse)
    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
  }

this.createContext() 方法主要就是新建了 3 个对象,这 3 个对象的 __proto__ 属性分别是 this.contextthis.requestthis.response,而 3 个实例上的属性对象的 __proto__ 属性又是(在 constructor() 构造函数里新建的):

  • context.js 文件里的 context
  • response.js 文件里的 response
  • request.js 文件里的 request

createContext() 方法还初始化了这 3 个对象和传进来的 reqres 之间的指向。最后返回的 context 对象有那么几个属性:

  • app:指向 this,即 Koa 类的实例 app
  • request:指向新建的 request
  • response:指向新建的 response
  • req:指向传进来的 req,即传给 http.createServer() 方法的 Request 类实例
  • res:指向传进来的 res,即传给 http.createServer() 方法的 http.ServerResponse 类实例
  • originalUrl:指向 req.originalUrl
  • state:一个空的 JS 对象

详细的指向关系可参考下图:

../images/createContext.png

这样 this.handleRequest(ctx, fn) 方法的第一个参数 ctx 就有了,下面来看下第二个参数 fn,它在 this.callback() 方法里定义:

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

compose 函数来自 koa-compose 这个 npm 模块:

function compose (middleware) {
  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 = 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)
      }
    }
    return dispatch(0)
  }
}

首先,compose() 函数接受一个函数数组(即 this.middleware),并返回一个函数(该函数的参数的形式也是中间件函数的参数的形式,即一个 context 一个 next),该函数内会依次调用传入的 middleware 函数,需要在自己定义的中间件函数内手动调用第二个参数 next,不然下一个中间不会被调用。因为在调用第一个中间件函数的时候把下一个中间件函数当作参数传了进去,所有,如果想要调用下一个中间函数,就需要手动调用第二个参数 next

function compose (middleware) {
  return function(context, next) {
    // 最后一次调用的中间件数
    let index = -1
    function dispatch (i) {
      // ...
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))  // 这一句是关键      } catch (err) {
        return Promise.reject(err)
      }
    }
    return dispatch(0)
  }
}

所以,只要调用 compose() 函数返回的函数就会开始执行传入的 middleware 函数,需要注意的是,调用这个函数需要传入一个 context 对象,这样在开始调用第一个中间件函数的时候才能使用这个对象,还有一个 next 函数,这个函数会在最后的中间件函数调用 next() 的时候被调用:

function compose (middleware) {
  return function(context, next) {
    // 最后一次调用的中间件数
    let index = -1
    function dispatch (i) {
      // ...
      if (i === middleware.length) fn = next      if (!fn) return Promise.resolve()      // ...
    }
    return dispatch(0)
  }
}

而如果没有提供这个 next 参数的话,就会直接返回一个空的 Promise。

题外话:可以与 Redux 的 compose 函数 做比较。

好了,现在回到我们的 this.callback() 方法:

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

this.createContext()compose() 函数都将完了,分别返回一个对象(该对象上引用着 reqres 等对象)和一个函数,调用这个函数就会开始执行中间件函数。最后来看一下 this.handleRequest() 方法,它会接收 this.createContext() 方法返回的对象和 compose() 函数返回的函数作为参数:

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

思路还是比较清晰的,首先,res.statusCode 属性如果没有处理的话,默认是 404,然后定义了一个 Promise 发生 reject 的时候的处理函数 onerror,默认是 Context 类实例的 onerror() 方法(在 context.js 文件内有定义),并且定义了一个用来处理响应的处理函数 handleResponse(),它会调用 respond(ctx)。可见,响应的生成应该会在 respond() 函数里,最后将这个响应处理函数传给返回后的中间件函数,而这个 handleResponse() 函数没有接受任何的参数,可见,中间件函数不用返回任何的值也可以(返回也白塔)。最后的 onFinished(res, onerror) 主要处理一些收尾工作,该函数来自第三方模块 on-finished。最后来看一下 respond(ctx) 函数是什么,它应该会修改 res.statusCode 的值,因为默认是 404:

function respond(ctx) {
  // ...
  const res = ctx.res
  let body = ctx.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 的时候,
  // 即在中间件函数中将一个 JS 对象赋值给了 body
  body = JSON.stringify(body)  if (!res.headersSent) {    ctx.length = Buffer.byteLength(body)  }  res.end(body)}

respond() 函数做了很多的工作,需要结合 response.jscontext.js 文件一起看。该函数主要的工作就是根据不同的 body 以不同的形式调用 ctx.res.end(ctx.body) 方法,返回 HTTP 响应给客户端。

不过,和我们想的不一样,这个函数并没有修改 res.statusCode。修改工作在 response.js 文件中:

  get status() {
    return this.res.statusCode
  },
  set status(code) {
    // ...
    this._explicitStatus = true
    // ...
  },
  // ...
  set body(val) {
    // ...
    if (!this._explicitStatus) this.status = 200    // ...
  }

大意就是,如果没有显式地给 ctx.status 赋值,那么在给 ctx.body 赋值的时候就会将 ctx.status 设为 200,所以,只要设置了 ctx.bodyctx.status 就是 200

另外,Koa 使用到了 delegates 这个第三方模块,才使得 ctx 对象可以直接使用 ctx.body 来访问 ctx.response.body,即访问 ctx.body 等于访问 ctx.response.body,少写了点代码,这些便人的修改在 context.js 文件的最后。不过,也会导致一些困惑,例如 ctx.body 同义于 ctx.response.body,而 ctx.headers 却同义于 ctx.request.headers,见 这个 issue

没有分析到的东西:

  • context.js 文件:200+ 行代码,东西不多,模块导出的是 Application 类实例的 this.context 对象的 prototype;
  • request.js 文件:700+ 行代码,模块导出的是 this.request 类的 prototype,主要都是设置一些 getter 和 setter;
  • response.js 文件:500+ 行代码,模块导出的是 this.response 类的 prototype,主要也是一些 getter 和 setter。

最后,以 context 为线索,一路下来的过程:

const response = require('./response');
const context = require('./context');const request = require('./request');

module.exports = class Application extends Emitter {
  constructor() {
    super();

    // ...
    this.context = Object.create(context);    this.request = Object.create(request);
    this.response = Object.create(response);
    // ...
  }
  
  callback() {
    const fn = compose(this.middleware);

    const handleRequest = (req, res) => {
      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);
    // ...
    return context;  }

  handleRequest(ctx, fnMiddleware) {
    // ...
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);    // ...
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

  function respond(ctx) {
    // ...
    const res = ctx.res;
    let body = ctx.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);  }
}