Koa 源码浅析

栏目: Node.js · 发布时间: 6年前

内容简介:本文围绕koa服务从启动,到处理请求再到回复响应这个过程对源码进行简单的解析在koa中ctx是贯穿整个请求过程的,它是这次请求原信息的承载体,可以从ctx上获取到request、response、cookie等,方便我们进行后续的计算处理。 ctx在实现上原本就是一个空对象,在koa服务起来时,往上挂载了很多对象和方法。当然开发者也可以自定义挂载的方法。 在相对于这种写法,还有另外一种较为优雅的挂载方法。

本文围绕koa服务从启动,到处理请求再到回复响应这个过程对源码进行简单的解析

在koa中ctx是贯穿整个请求过程的,它是这次请求原信息的承载体,可以从ctx上获取到request、response、cookie等,方便我们进行后续的计算处理。 ctx在实现上原本就是一个空对象,在koa服务起来时,往上挂载了很多对象和方法。当然开发者也可以自定义挂载的方法。 在 context.js 文件中对ctx初始化了一些内置的对象和属性,包括错误处理,设置cookie。

get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
复制代码

相对于这种写法,还有另外一种较为优雅的挂载方法。

// ./context.js

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

复制代码

delegate方法的作用是将其他对象的方法或者属性挂载在指定对象上,在这里就是proto,也就是最初的ctx对象,属性提供方就是第二个参数"response"。 method是代理方法,getter代理get,access代理set和get. 看delegate如何实现:

function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name);

  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };

  return this;
};

复制代码

delegate中的method的实现是在调用原属性上指定方法时,转而调用提供方的方法。这里可以发现提供方也被收在了this上,这里的不直接传入一个对象而是将该对象赋值在原对象上的原因,我想应该是存放一个副本在原对象上,这样可以通过原对象直接访问到提供属性的对象。

./context.js 中使用delegates为ctx赋值的过程并不完整,因为这里的属性提供方虽然是request和response, 但是是从 ./application.js createContext方法中传入,这样delegates才算完成了工作

到这里我们就可以看下平时用koa时常走的流程。

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

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);
复制代码

基本上就是分为三步,实例化Koa,注册中间件再监听端口, 这里正常能让koa服务或者说一个http服务起的来的操作其实是在app.listen(...args)里,是不是和想象中的有点差距, 看下源码实现。

// ./application.js

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

在listen方法里使用了http模块的createServer方法来启动http服务,这里相当于是声明了一个http.Server实例,该实例也继承于EventEmitter,是一个事件类型的服务器,并监听了该实例的request事件,意为当客户端有请求发过来的时候,这个事件将会触发,等价于如下代码

var http = require("http");
var server = new http.Server();

server.on("request", function(req, res){
    // handle request
});

server.listen(3000);
复制代码

这个事件有两个参数req和res,也就是这次事件的请求和响应信息。有点扯远了,回到koa源码, 处理req和res参数的任务就交给了this.callback()的返回值来做,继续看callback里做了什么

// 去除了一些不影响主流程的处理代码

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

  handleRequest(ctx, fnMiddleware) {
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
复制代码

callback返回一个函数由他来处理req和res,这个函数内部做了两件事, 这两件事分别在koa服务的初始化和响应时期完成,上述代码中compose中间件就是在服务初始化完成, 而当request事件触发时,该事件会由callback返回的handleRequest方法处理,这个方法保持了对fn,也就是初始化过后中间件的应用, handleRequest先会初始化贯穿整个事件的ctx对象,这个时候就可以将ctx以此走入到各个中间件中处理了。

可以说koa到这里主流程已经走一大半了,让我们理一理经过简单分析过的源码可以做到哪个地步(忽略错误处理)

  • 响应http请求 √
  • 生成ctx对象 √
  • 运用中间件 √
  • 返回请求 ×

如上我们已经可以做到将响应进入readly状态,但还没有返回响应的能力,后续会说道。在前三个过程中有两个点需要注意,ctx和middleware,下面我们依次深入学习下这两个关键点。

ctx是贯穿整个request事件的对象,它上面挂载了如req和res这种描述该次事件信息的属性,开发者也可以根据自己喜好,通过前置中间件挂载一些属性上去。 ctx在koa实例createContext方法上创建并被完善,再由callback返回的handleRequest也就是响应request的处理函数消费。看下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);
    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;
  }

复制代码

前三行依次声明了context、request和response,分别继承于koa实例的三个静态属性,这三个静态属性由koa自己定义,在上面有一些快捷操作方法,比如在Request静态类上可以获取通过query获取查询参数,通过URL解析url等,可以理解为request的 工具 库,Response和Context同理。res和rep是node的原生对象,还记得吗,这两个参数是由http.Server()实例触发request事件带来的入参。 res是http.incomingMessage的实例而rep继承于http.ServerResponse, 贴一张图。

Koa 源码浅析

箭头指向说明了从属关系,有五个箭头指向ctx表面ctx上有五个这样的的属性,可以很清楚看到ctx上各个属性之间的关系。

接下来我们再来看看koa中的中间件,在koa中使用use方法可以注册中间件.

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;
  }
复制代码

两件事,统一中间件格式,再将中间件推入中间件数组中。 在koa2.0以后middleware都是使用async/await语法,使用generator function也是可以的,2.0以后版本内置了koa-convert,它可以根据 fn.constructor.name == 'GeneratorFunction' check here .来判断是legacyMiddleware还是modernMiddleware,并根据结果来做相应的转换。 koa-convert的核心使用是co这个库,它提供了一个自动的执行器,并且返回的是promise,generator function有了这两个特性也就可以直接和async函数一起使用了。

回到koa源码来,callback中是这样处理中间件数组的

const fn = compose(this.middleware);
复制代码

这里的compose也就是koa-compose模块,它负责将所有的中间件串联起来,并保证执行顺序。经典的洋葱圈图:

Koa 源码浅析

koa-compose模块的介绍只有简单的一句话

Compose the given middleware and return middleware.

言简意赅,就是组合中间件。贴上源码

function compose (middleware) {
  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()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
复制代码

compose先存入中间件数组,从第一个开始执行依次resolve到最后一个,中间件函数签名为 (ctx,next)=>{} ,在内部调用next就会间接唤起下一个中间件,也就是执行 dispatch.bind(null, i + 1) ,中间件执行顺序如下(网上扒下来的图)。

Koa 源码浅析

图上是遇到yield,和执行next同理。 也不是所有的中间件都需要next,在最后一个middleware执行完毕后可以不调用next,因为这个时候已经走完了所有中间件的前置逻辑。当然这里调用next也是可以的,是为了在所有前置逻辑执行完后有一个回调。我们单独使用koa-compose:

const compose = require("koa-compose");
let _ctx = {
    name: "ctx"
};

const mw_a = async function (ctx, next) {
    console.log("this is step 1");
    ctx.body = "lewis";
    await next();
    console.log("this is step 4");
}

const mw_b = async function (ctx, next) {
    console.log("this is step 2");
    await next();
    console.log("this is step 3");
}

const fn = compose([mw_a, mw_b]);

fn(_ctx, async function (ctx) {
    console.log("Done", ctx)
});

// => 
// 
// this is 1
// this is 2
// Done {name: "ctx", body: "lewis"}
// this is 3
// this is 4
复制代码

compose返回的函数接受的参数不光是ctx,还可以接受一个函数作为走完所有中间件前置逻辑后的回调。有特殊需求的开发者可以关注一下。 当然整个中间件执行完后会返回一个resolve状态的promise,在这个回调中koa用来告诉客户端“响应已经处理完毕,请查收”,这个时候客户端才结束等待状态,这个过程的源码:

// handleRequest 的返回值
// 当中间件已经处理完毕后,交由handleResponse也就是respond方法来最后处理ctx
const handleResponse = () => respond(ctx);
fnMiddleware(ctx).then(handleResponse).catch(onerror);

/** 
 * 交付response
 */
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]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  if (null == body) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(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);
}

复制代码

以上代码对各种款式的status和ctx.body做了相应的处理,最关键的还是这一句 res.end(body) ,它调用了node原生response的end方法,来告诉服务器本次的请求以回传body来结束,也就是告诉服务器此响应的所有报文头及报文体已经发出,服务器在此调用后认为这条信息已经发送完毕,并且这个方法必须对每个响应调用一次。

总结

至此,koa整个流程已经走通,可以看到koa的关键点集中在ctx对象和中间件的运用上。 通过delegate将原生res和req的方法属性代理至ctx上,再挂载koa内置的Request和Reponse,提供koa风格操作底层res和req的实现途径和获取请求信息的工具方法。 中间件则是使用koa-compose库将中间件串联起来执行,并具有可以逆回执行的能力。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Developing Large Web Applications

Developing Large Web Applications

Kyle Loudon / Yahoo Press / 2010-3-15 / USD 34.99

As web applications grow, so do the challenges. These applications need to live up to demanding performance requirements, and be reliable around the clock every day of the year. And they need to withs......一起来看看 《Developing Large Web Applications》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具