博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Koa 源码浅析
阅读量:7251 次
发布时间:2019-06-29

本文共 8519 字,大约阅读时间需要 28 分钟。

本文围绕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.jsdelegate(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();// responseapp.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, 贴一张图。

箭头指向说明了从属关系,有五个箭头指向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' .来判断是legacyMiddleware还是modernMiddleware,并根据结果来做相应的转换。 koa-convert的核心使用是co这个库,它提供了一个自动的执行器,并且返回的是promise,generator function有了这两个特性也就可以直接和async函数一起使用了。

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

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

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

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),中间件执行顺序如下(网上扒下来的图)。

图上是遇到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方法来最后处理ctxconst 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库将中间件串联起来执行,并具有可以逆回执行的能力。

转载地址:http://ethbm.baihongyu.com/

你可能感兴趣的文章
2018年美团Android校招
查看>>
Spring消息之WebSocket
查看>>
Java 文件流操作.
查看>>
《11招玩转网络安全》之第三招:Web暴力破解-Low级别
查看>>
Eclipse快捷键大全
查看>>
Android实现TextView字符串波浪式跳动
查看>>
NumPy—random随机数生成函数总结
查看>>
第10章节-Python3.5-Django路由分发
查看>>
排序三 直接插入排序
查看>>
输入输出流体系图
查看>>
玩转报表排名
查看>>
《函数响应式领域建模》读后感
查看>>
一入前端深似海,从此红尘是路人系列第四弹之未来前端路该何去何从
查看>>
java笔记--笔试中极容易出错的表达式的陷阱
查看>>
第140天:前端开发中浏览器兼容性问题总结(一)
查看>>
socket编程的select模型
查看>>
智能医疗的春天在哪里?
查看>>
Kali Linux 无线渗透测试入门指南 第二章 WLAN 和固有的不安全性
查看>>
MyExcel 2.1.2 版本发布,重要 Bug 修复
查看>>
广汽与蔚来达成合作,将共同投资12.8亿元创立新能源汽车公司
查看>>