Koa2 源码分析记录

2017/6/5 posted in  JavaScript Koa.js

最近在写与Koa(v2)相关的东西,所以稍微看了下koa的源码.便在这里做些笔记.

目录分析:

├── AUTHORS
├── CODE_OF_CONDUCT.md
├── History.md
├── LICENSE
├── Readme.md
├── benchmarks
├── docs
├── lib   // koa主要源码位于lib
│   ├── application.js
│   ├── context.js
│   ├── request.js
│   └── response.js
├── package.json
└── test   // 测试

koa的主要源码位于lib目录中,其中:
application.js => koa的入口文件,会export出Application类(koa类)
context.js => 上下文,context对象,用于代理request和response
request.js => 请求,用于获取和操作请求.
response.js => 响应,用于获取和操作响应.
另外test文件夹下有大量的测试,也是值得我们学习的,下次再说...

application.js(入口)

application.js中会导出一个继承Emitter的类Application,其中构造函数分析如下

1.构造函数

constructor() {
    super();
    this.proxy = false;
    // 是否信任proxy header, 默认为不信任
    this.middleware = [];
    // 创建空数组存放中间件,"洋葱壳"
    this.subdomainOffset = 2;
    // 子域名默认偏移量,默认为2
    this.env = process.env.NODE_ENV || 'development';
    // 环境参数,默认为开发环境
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 分别创建koa 3巨头:context、request、response
  }  

2.app.listen

启动入口(listen函数):

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

实际上listen(...)就是http.createServer(app.callback()).listen(...)的简写,
http.createServer就是利用node中的http模块创建一个http服务器,并在创建完成后执行回调app.callback().而app.callback的用途就是合并koa的3巨头(context、request、response).下面会提到app.callback
很多初学koa的人以为在实例化koa类的时候就已经创建了httpserver了.这段代码可以清楚的看到,其实在运行listen函数后koa才会创建httpserver,才会真正的运行起来.

3.app.callback


  callback() {
    const fn = compose(this.middleware);
    // compose函数来自于koa-compose包,其作用是合并各个中间件函数
    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);
      // 传入ctx为参数,执行完中间件后respond(ctx);
    };

    return handleRequest;
  }

4. app.createContext

  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);
    // 分别创建koa 3巨头对象(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 = {};
    // 在上下文中添加originalUrl,cookies,accept,state 
    // 在request中添加ip或者remoteAddress
    return context;
  }

洋葱模型:

无论在koa的官网还是其他文章中我们都可以看得见这个被称为"洋葱模型"的图,洋葱模型就是koa中中间件的执行过程可以说是整个koa的内涵了.现在我们就来看看koa中 中间件的执行过程.

其中核心代码在koa-compose包中,他的主要用途就是将各个中间件组合起来成为一个函数.看代码:

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!')
  }
  // 判断middleware是否为由函数组成的数组

  return function (context, next) {
    let index = -1
    return dispatch(0)
    //获取第一个中间件,并在返回的 Promise 中执行。
    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, function next () {
          return dispatch(i + 1)
    // 当中间件await next()时执行下一个中间件,即 dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Context.js、Request.js、Response.js

Context.js中使用了node-delegates包,对request和response上的方法和属性进行了代理

delegate(proto, 'response')
  .method('attachment')
  //....
  .method('flushHeaders')
  .access('status')
  // ....
  .access('etag')
  .getter('headerSent')
  .getter('writable');
  // ...

Request.js封装了请求相关的属性以及方法

const request = context.request = Object.create(this.request);
// ...
context.req = request.req = response.req = req;
// ...
request.response = response;

request.req为node.js原生的请求对象
Response.jsRequest.js基本一致.

#EOF#