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
对于nodejs
甚至是JavaScript
项目,第一件事儿就是看看它的package.json
。package.json
里面能够找到很多有用的信息。json
咱们打开koa@2.5.0
的目录,发现它依赖了很多的库。其实这些库大多都十分简单,koa
的编写原则其实就是把功能分割到其余的库中去。咱们暂且先无论这些依赖。安全
咱们找到main
字段,这里就是‘通往新世界的大门’了。顺着main
打开lib/application.js
。服务器
好家伙,一上来就是一大串的引入,这可不是什么好东西,咱么先不看这些东西。先看下面的代码。
首先是定义了一个application
的类,接着在构造函数中定义了一些变量。咱们主要关注如下几个变量,由于他们的用处最大:
this.middleware = []; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); 复制代码
Object.create
是用来克隆对象的,这里克隆了三个对象,也是koa
最重要的三个对象request
,response
和context
。这三个对象几乎就是koa
的所有内容了。待会儿会逐一分析。
咱们接着往下看,listen
函数你们都很熟悉了,就是用来监听端口的。koa
的listen
函数也很简单。
const server = http.createServer(this.callback()); return server.listen(...args); 复制代码
短短两行,对于nodejs
不熟的同窗,建议在这里就打住了。其中this.callback()
是个什么玩意儿呢?它返回一个函数,这个函数接收两个参数request
和response
,也就是createServer
的回调函数,在中间件原理章节会更详细介绍。
接着就是toJSSON
这个方法。JSON.stringify
调用的方法,目的是当你JSON
化一个application
实例时,返回指定的属性,而并不是全部。这个用处不大,几乎用不到。
inspect
也就是调用了toJSON
这个方法而已。
接着就是use
函数了。use
函数自己不是很复杂,可是use
函数做为中间件的接口,背后的中间件却有点儿复杂。为此,本文在后面专门解读了中间件相关的源代码,这里暂时跳过。
callback
,handleRequest
,respond
这几个方法涉及中间件的,所以放到中间件的章节讲。
createContext
这个方法是用来封装context
的。这个context
就是你在使用koa
的use
方法,你传递的回调函数的第一个ctx
参数。createContext
执行的最重要的操做就是把context.request
设置成了Request
,把context.response
设置成了Response
。以及把Response.res
h和Request.req
分别设置成了原生的response
和request
。
为何这样说,这个就得追到context.js
和request.js
以及response.js
的代码里面了,先等等。
值得强调的是,这里的Request
和Response
并非nodejs
里面的,而是koa
封装事后的。为了区分原生的和koa
封装好的,我把Request
和Response
称为封装事后的,request
和response
称为原生的。你须要记住的是context.res
是指原生的response
,而context.response
则是封装后的Response
。Request
以此类推。
封装的东西看起来并无什么高大上,无非是把经常使用的一些方法给简化了。就像jquery
简化了js
对dom
的操做同样。
打开context.js
,代码很少,可是含金量挺高的。首先是把proto
赋值成一个对象,这个对象也是模块的导出值。
inspect
和toJSON
功能和application.js
里面同样,不作过多介绍了。
接着看到个assert
,这个和nodejs里面的assert
实际上是差很少,它实际上是提供了一些断言的操做。好比equal
,notEqual
,strictEqual
之类的。比较有意思的是,assert
提供了一个深度比较的方法deepEqual
,这个但是个好东西。js
里面的深度比较一直是个比较麻烦的问题,有经验的程序员会使用JSON
来比较,这里提供了一种性能更好的方法。代码其实不复杂,就是引用了deep-eqaul
这个库而已,有兴趣的能够去看看哦。
跳过两个关于错误处理的函数(本文不讲解错误处理),来到了context.js
最精华的地方了。 这里使用了delegate
这个库。这是个啥?delegate
其实很简单的,你甚至不须要去查看delegate
的源代码,看我解释就好了。
delegate
提供了一种相似Proxy
的手段,也就是代理。代理什么?具体来讲delegate(proto, 'response')
这段代码的意思就是把proto
上的一些属性代理到proto.response
上面去。具体是哪些代理呢?就是接下来排列工整的代码作的了。delegate
区分了method
,getter
,access
等类型。前面两个还好理解,就是方法和只读属性,第三个呢?其实就是可读可写属性罢了,至关于同时代理了getter
和setter
。因此其实你访问ctx.redirect
实际上访问的是ctx.request.redirect
,以此类推。须要注意的是,这里的request
和response
不是nodejs原生的,是koa
封装事后的。
context.js
就这么简单。
request.js
和response.js
分别是对createServer
回调函数接收的的request
和response
进行封装。
先看request.js
。还记得createContext
吗?咱们说过,他把Request.req
设置成了原生的request
。因此你能够看到,不少方法其实本质就是在操做this.req
,这一点和response.js
相似,后面就不重复说了。
首先是一些个经常使用的属性,header
分别设置了getter
和setter
,都是对this.req.headers
操做。headers
和header
如出一辙,并非用来区分单复数的(这有点儿坑,初学觉得headers是设置多个的)。接下来还有不少经常使用的属性,就不一一介绍了,什么url
,method
之类的,稍微熟悉点儿nodejs的同窗都可以实现出来。
值得注意的是query
和querystring
,一个返回的是对象,一个是字符串哦。
你或许会问search
和querystring
有啥区别。区别,emmmmn。。。多是为了完整吧,毕竟express都有个search,koa
也要提供。。。
另外须要说一下的是,这里的不少属性的操做涉及到了http
协议的内容了,好比fresh
。http
是个很大的内容,不作讲解。若是遇到看不懂的代码,不妨去查看相关的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
,同headers
。status
设置状态码,好比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的数据是个什么类型。
当传递的是字符串时,它使用了一个正则:/^\s*</
来判断是html仍是text。很明显,这个正则很简陋,在不少状况下并不能正确判断,好比<----
就会被判断成html。因此body
的类型仍是要手动的设置type
才行。
当传递的是buffer的时候,把类型设置称为bin
(记住,type
是koa封装事后的属性,它会根据你设置的type
自动匹配最佳的Content-Type
。好比你把type
设置成'json',实际上最后的Content-Type
会是application/json
。后面会说实现方法的)。
当传递的是个stream(经过判断它是否拥有pipe这个函数),先绑定回调函数,当res发送完毕的时候,销毁这个stream,避免内存浪费。接着错误处理。接着判断如下如今这个stream和原来body
的值是否相同,若是不是的话,那就移除Content-Length
,交给nodejs本身处理。(实际上nodejs也并不会处理,为啥呢?header必须在正文发送以前发送,可是Stream的字节数要在发送完才知道,so,你懂得)。最后把type
设置成bin
,由于stream是二进制的数据流。
不知足以上三种,那么就只能是json了呗(别问我为何不判断boolean,symbol这些,谁会没事儿干发送这些玩意儿?)。移除Content-Type
(你可能想问,为啥呢?由于你传递的其实是个Object对象,须要stringify以后才能知道它的字节数,这个其实会在后面处理的)。设置type
成json
。
至此,body
的setter
分析得差很少了。
接着到了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-types
的contentType
函数,而后返回json
类型的mime
,也就是application/json
。
同request.js
同样,response.js
一样封装了set
和get
两个方法,用于设置和读取header
。
inspect
和toJSON
又来了。。。
response.js
不少的属性和方法并无说起,这是由于这些属性和方法就是作了简单的封装而已,方便调用,很容易理解。
好了,至此response.js
也分析完了。
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
语句,返回一个函数。这个函数有两个参数context
和next
,熟悉吗?这不就是中间件函数吗!别慌,接着往下看。
首先声明一个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
的返回值,是一个相似中间件的函数。
回到application
的callback
方法中。定义了一个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
这个方法了,这个方法实际上就是对statusCode
,header
以及body
进行处理,最后调用nodejs提供了发送数据的方法,向客户端发送数据。最后调用ctx.end(body)
,结束本次http请求
。
那么至此,koa
中间件也就完了。
koa
的源码并非十分复杂,有兴趣的同窗能够本身再看看。但愿这篇文章能给你帮助。