koa@2.5.0源代码解读

koa简介

koa是由Express原班人马开发的一个nodejs服务器框架。koa使用了ES2017的新标准:async function来实现了真正意义上的中间件(middleware)。koa的源代码极其简单,可是借由其强大的中间件扩展能力,使得koa成为了一个极其强大的服务器框架。借助中间件,你能够作任何nodejs能作到的事儿。html

一些繁琐的交代

本文并不会像其余的代码分析那样贴段代码加注释,所以须要你本身打开koa@2.5.0的源代码一块儿阅读。node

koa目前已经更新到了2.x版本,1.x之前的版本相对于koa@2.x已经再也不兼容。本文针对的是koa@2.5.0进行的代码分析。jquery

此外,koa的源代码里面涉及部分http协议的内容,这部份内容本文不会过度强调,默认读者已经掌握了基本的知识。git

另外,用于koa2是用了ES2017新特性编写的,所以你须要了解一些ES2017的新语法才行。程序员

为了使得这篇文章简单,我有意地忽略了错误处理,参数判断之类。github

本文你还能够在这里找到。express

$1.查看package.json

对于nodejs甚至是JavaScript项目,第一件事儿就是看看它的package.jsonpackage.json里面能够找到很多有用的信息。json

咱们打开koa@2.5.0的目录,发现它依赖了很多的库。其实这些库大多都十分简单,koa的编写原则其实就是把功能分割到其余的库中去。咱们暂且先无论这些依赖。安全

咱们找到main字段,这里就是‘通往新世界的大门’了。顺着main打开lib/application.js服务器

$2.分析application.js

好家伙,一上来就是一大串的引入,这可不是什么好东西,咱么先不看这些东西。先看下面的代码。

首先是定义了一个application的类,接着在构造函数中定义了一些变量。咱们主要关注如下几个变量,由于他们的用处最大:

this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
复制代码

Object.create是用来克隆对象的,这里克隆了三个对象,也是koa最重要的三个对象requestresponsecontext。这三个对象几乎就是koa的所有内容了。待会儿会逐一分析。

咱们接着往下看,listen函数你们都很熟悉了,就是用来监听端口的。koalisten函数也很简单。

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

短短两行,对于nodejs不熟的同窗,建议在这里就打住了。其中this.callback()是个什么玩意儿呢?它返回一个函数,这个函数接收两个参数requestresponse,也就是createServer的回调函数,在中间件原理章节会更详细介绍。

接着就是toJSSON这个方法。JSON.stringify调用的方法,目的是当你JSON化一个application实例时,返回指定的属性,而并不是全部。这个用处不大,几乎用不到。

inspect也就是调用了toJSON这个方法而已。

接着就是use函数了。use函数自己不是很复杂,可是use函数做为中间件的接口,背后的中间件却有点儿复杂。为此,本文在后面专门解读了中间件相关的源代码,这里暂时跳过。

callbackhandleRequestrespond这几个方法涉及中间件的,所以放到中间件的章节讲。

createContext这个方法是用来封装context的。这个context就是你在使用koause方法,你传递的回调函数的第一个ctx参数。createContext执行的最重要的操做就是把context.request设置成了Request,把context.response设置成了Response。以及把Response.resh和Request.req分别设置成了原生的responserequest

为何这样说,这个就得追到context.jsrequest.js以及response.js的代码里面了,先等等。

值得强调的是,这里的RequestResponse并非nodejs里面的,而是koa封装事后的。为了区分原生的和koa封装好的,我把RequestResponse称为封装事后的,requestresponse称为原生的。你须要记住的是context.res是指原生的response,而context.response则是封装后的ResponseRequest以此类推。

封装的东西看起来并无什么高大上,无非是把经常使用的一些方法给简化了。就像jquery简化了jsdom的操做同样。

$3.分析context.js

打开context.js,代码很少,可是含金量挺高的。首先是把proto赋值成一个对象,这个对象也是模块的导出值。

inspecttoJSON功能和application.js里面同样,不作过多介绍了。

接着看到个assert,这个和nodejs里面的assert实际上是差很少,它实际上是提供了一些断言的操做。好比equalnotEqualstrictEqual之类的。比较有意思的是,assert提供了一个深度比较的方法deepEqual,这个但是个好东西。js里面的深度比较一直是个比较麻烦的问题,有经验的程序员会使用JSON来比较,这里提供了一种性能更好的方法。代码其实不复杂,就是引用了deep-eqaul这个库而已,有兴趣的能够去看看哦。

跳过两个关于错误处理的函数(本文不讲解错误处理),来到了context.js最精华的地方了。 这里使用了delegate这个库。这是个啥?delegate其实很简单的,你甚至不须要去查看delegate的源代码,看我解释就好了。

delegate提供了一种相似Proxy的手段,也就是代理。代理什么?具体来讲delegate(proto, 'response')这段代码的意思就是把proto上的一些属性代理到proto.response上面去。具体是哪些代理呢?就是接下来排列工整的代码作的了。delegate区分了methodgetteraccess等类型。前面两个还好理解,就是方法和只读属性,第三个呢?其实就是可读可写属性罢了,至关于同时代理了gettersetter。因此其实你访问ctx.redirect实际上访问的是ctx.request.redirect,以此类推。须要注意的是,这里的requestresponse不是nodejs原生的,是koa封装事后的。

context.js就这么简单。

$4.request.js & response.js

request.jsresponse.js分别是对createServer回调函数接收的的requestresponse进行封装。

先看request.js。还记得createContext吗?咱们说过,他把Request.req设置成了原生的request。因此你能够看到,不少方法其实本质就是在操做this.req,这一点和response.js相似,后面就不重复说了。

首先是一些个经常使用的属性,header分别设置了gettersetter,都是对this.req.headers操做。headersheader如出一辙,并非用来区分单复数的(这有点儿坑,初学觉得headers是设置多个的)。接下来还有不少经常使用的属性,就不一一介绍了,什么urlmethod之类的,稍微熟悉点儿nodejs的同窗都可以实现出来。

值得注意的是queryquerystring,一个返回的是对象,一个是字符串哦。

你或许会问searchquerystring有啥区别。区别,emmmmn。。。多是为了完整吧,毕竟express都有个search,koa也要提供。。。

另外须要说一下的是,这里的不少属性的操做涉及到了http协议的内容了,好比freshhttp是个很大的内容,不作讲解。若是遇到看不懂的代码,不妨去查看相关的http协议哦。

另外在idempotent你能够看到!!~,这是个啥玩意儿???第一次看见都是一脸懵逼。这个其实就是位操做而已。咱们通常把!!看作一组,它的做用是把任意数据变成boolean值。这个操做其实很简单,就是判断是否是-1,若是是-1,那么就是false;若是不是-1,那么都是true。这个操做很巧妙。稍微解释一下吧。

咱们假设数字是8位表示的,那么-1的原码就是1000 0001,反码就是1111 1110,补码就是1111 1111。而~操做符是取反的意思,因此取反之后就成了0000 0000。计算机存储负数是用的补码(相关知识能够取google搜索一下),因此最后就是判断是否是-1的。

有几个accept打头的函数能够忽略,这几个函数是判断是否符合指定类型、语言、编码的,它内部调用了一个accepts的库。这个功能其实用得不多,但涉及编码之类较为复杂的知识了。

在最后的代码里面,request.js提供了get方法,其实就是获取header

让咱们转到response.js里面去。劈头盖脸一看,和request.js差很少,知识封装的方法和属性不同而已。

首先是socket,这个是套接字,http模块的底层,不讲解。

header调用的是getHeaders(),获取已经设置好的全部的header,同headersstatus设置状态码,好比200,404之类的。值得一提的是,一般状况下使用nodejs的statusCode还须要你设置一个statusMessage来提示用户发生了什么错误,koa会智能的为你设置好。好比你设置好了status为404,会自动把statusMessage设置成404 not found。这是由于koa使用了statuses这个库,这个库会根据你传入的状态码返回指定的状态信息。

接下来是Response最重要的一个属性,也就是body。对body的操做反应在内部的_body上面。body的setter作了各类处理。好比判断传给body的值是否是空,若是是空就进行一些操做。比较有意思的是,body的setter会在你没有设置Content-Type时,判断一下传递给body的数据是个什么类型。

  1. 当传递的是字符串时,它使用了一个正则:/^\s*</来判断是html仍是text。很明显,这个正则很简陋,在不少状况下并不能正确判断,好比<----就会被判断成html。因此body的类型仍是要手动的设置type才行。

  2. 当传递的是buffer的时候,把类型设置称为bin(记住,type是koa封装事后的属性,它会根据你设置的type自动匹配最佳的Content-Type。好比你把type设置成'json',实际上最后的Content-Type会是application/json。后面会说实现方法的)。

  3. 当传递的是个stream(经过判断它是否拥有pipe这个函数),先绑定回调函数,当res发送完毕的时候,销毁这个stream,避免内存浪费。接着错误处理。接着判断如下如今这个stream和原来body的值是否相同,若是不是的话,那就移除Content-Length,交给nodejs本身处理。(实际上nodejs也并不会处理,为啥呢?header必须在正文发送以前发送,可是Stream的字节数要在发送完才知道,so,你懂得)。最后把type设置成bin,由于stream是二进制的数据流。

  4. 不知足以上三种,那么就只能是json了呗(别问我为何不判断boolean,symbol这些,谁会没事儿干发送这些玩意儿?)。移除Content-Type(你可能想问,为啥呢?由于你传递的其实是个Object对象,须要stringify以后才能知道它的字节数,这个其实会在后面处理的)。设置typejson

至此,bodysetter分析得差很少了。

接着到了length,这个其实就是封装了设置Content-Length的方法。反却是它的getter有点儿复杂来着。咱们不妨细看一下。

首先判断Content-Length设置没有,有就直接返回,有的话那就分状况读body的字节数。当body是stream的时候,啥都不返回。

这里有个奇淫巧技,~~这个玩意儿能够用来把字符串转换成数字。为何呢?!我就知道你要问!其实这个东西要对js有比较高的理解才行的,js里面存在隐式类型转换,当遇到一些特殊的操做符,例如位操做符,会把字符串转换成数字来进行计算。其实+这个符号也能够进行字符串转数字(str+str这个不算哈,这个不会进行隐式类型转换),那么为何要用~~而不是+呢?我思索再三,认为是做者可能不了解。但实际上,~~要比'+'安全,+在遇到不能转换的式子时,会返回NaN,而~~是基于位操做的,返回安全的0。

跳到type,这个和length相似,是对Content-Type实现的封装。这里引用了一个mime-types的库,这个库功能很强大,能够根据传入的参数返回指定的mime类型。好比咱们设置type为json,会去调用mime-typescontentType函数,而后返回json类型的mime,也就是application/json

request.js同样,response.js一样封装了setget两个方法,用于设置和读取header

inspecttoJSON又来了。。。

response.js不少的属性和方法并无说起,这是由于这些属性和方法就是作了简单的封装而已,方便调用,很容易理解。

好了,至此response.js也分析完了。

$5.koa中间件原理分析

koa的中间件原理很强大,实现起来其实并非特别复杂。记得怎么使用koa中间件吗?只须要use一个函数就好了!这个函数接受两个参数,一个是context,咱们已经分析过了。另外一个是next,这个就是中间件的核心了。

让咱们回到开头,看看use怎么实现的。不看错误处理的那些内容,这里先对fn进行了一次判断。判断什么呢?判断fn是否是generator function。koa官方建议是不要继续使用Generator function了,换成了async function。若是你使用的是Generator function,那么内部会调用co模块来处理。因为处理内容比较晦涩,且与正文关系不大,故不做讲解。咱们假设全部的中间件都是async function。

application维护了一个middleware的队列,use方法把中间件推送进这个队列,除此以外什么都没作。

还记得listen方法吗?它调用了callback这个方法。最终的答案都在这里了!

看到callback方法。首先,它对middleware队列调用了compose方法。咱们打开compose对应的模块,短短几十行代码。

不看错误处理,那么compose只有一个return语句,返回一个函数。这个函数有两个参数contextnext,熟悉吗?这不就是中间件函数吗!别慌,接着往下看。

首先声明一个index游标,接着定义一个dispatch函数,而后默认返回dispatch(0)

dispatch函数用来分发中间件(和分发事件很像)。它接收一个数字,这个数字是中间件队列中某个中间件的下标。首先先判断一下有没有越界,也就是index和传入的i进行比较,没有越界把游标移动到当前分发的中间件。接着判断i是否已经遍历完了中间件队列,i === middleware.length判断。若是完了,就把fn设置成传入的next。接着使用Promise.resolve,并调用当前中间件,注意

return Promise.resolve(fn(context, function next () {
    return dispatch(i + 1)
}))
复制代码

这里传入中间件的第二个参数,也就是next,是一个函数,这个函数正是用来分发下一个事件的!!!中间件最重要的原理就在这里,为何能够用next转移控制权,逻辑就在这里!

compose函数分析完毕了,记住compose的返回值,是一个相似中间件的函数。

回到applicationcallback方法中。定义了一个handleRequest函数而且直接返回,handleRequest其实就是http.createServer的回调函数。这个回调函数首先封装一下createContext,上面已经讲过了。接着调用了application上的handleRequest方法(别搞混了,这个是下面那个handleRequest方法)。

咱们看看handleRequest方法,它接受两个参数,第一个是context,第二个是什么呢?其实就是compose处理后的middleware中间件队列。抛开一些’多余‘的代码不看,把它精简成这样:

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

记得fnMiddleware的返回值是什么吗?是dispatch(0)。那记得dispatch的返回值是什么吗?是一个Promise。咱们再来看看这个Promise

return Promise.resolve(fn(context, function next () {
    return dispatch(i + 1)
}))
复制代码

好好想想,fn如今是第一个中间件,它先被调用了。在这个中间件里面调用了next函数,也就是至关于调用了dispatch(i + 1),如此下去。这不就至关于依次调用了dispatch函数吗?

最后一点,中间件是async function,你明白为何要使用Promise了吗?对了,就是为了await。

最后的最后,就是respond这个方法了,这个方法实际上就是对statusCodeheader以及body进行处理,最后调用nodejs提供了发送数据的方法,向客户端发送数据。最后调用ctx.end(body),结束本次http请求

那么至此,koa中间件也就完了。

结语

koa的源码并非十分复杂,有兴趣的同窗能够本身再看看。但愿这篇文章能给你帮助。

推广一下本身的GitHub,个人开源项目doxjs,有兴趣的能够看看,给个star之类的。

相关文章
相关标签/搜索