深入浅出 Koa
Posted 前端那些事儿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出 Koa相关的知识,希望对你有一定的参考价值。
学习koa需要一些相关知识,有两个关键词
generator
promise
本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。
如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~
koa总体流程图
让我们从一张图开始
上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。
那我们按照时间线说起~
启动前
图中有三个蓝色的方块,分别代表三个静态类
。
什么是静态类
?这个是我自己给起的名,哈哈
静态类
就是程序运行前就存在的方法集合,动态类
就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。
三个静态类分别是Request
,Context
,Response
Request
Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API
Response
Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API
Context
Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如
从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。
Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。
下面是Context源码片段。
delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上
对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~
method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter
下面是源码片段
从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。
简单来个栗子
我们在来看看getter方法
可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~
我们在来看看access
可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累…
应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?
启动server
我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。
简单吧?
不了解内部机制的同学,通常会认为server是在koa()
这个时候启动的,app.listen
只是监听下端口而已~
事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。
有木有被刷新三观???
我们看下源码片段
从源码中可以看到,执行koa()
的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。
Request和Response的属性和方法委托到Context中也是在这一步进行的
并没有启动server
我们看第二步,在看一段源码
可以看到,在执行app.listen(1995)
的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback
这个方法上。
我们一起看一下this.callback
是何方神圣
这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。
而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!
第一部分
先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????
童鞋,,,不要着急,听我慢慢道来~~
我们添加中间的时候使用app.use
方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)
很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线
这样的功能。。。。
那么这些中间件处理之后会变成什么样的????
我们先看代码,上面的代码中用this.experimental
这个属性做了一个判断。这个属性是什么鸟。
this.experimental
关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true
,开启这个属性之后,中间件可以传入async函数。
我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental
,所以这里我们针对不开启的情况进行讨论~
这样一来,第一部分的代码就简化成了这样
虽然只剩下一行代码,但不要小瞧它哦~~
我们先看compose(this.middleware)
这部分,compose
的全名叫koa-compose
,他的作用是把一个个不相干的中间件串联在一起。。
例如
有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。
这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next
那compose
是如何实现这样的功能的呢??我们看一下代码
这是这个模块的所有代码,很简单,逻辑是这样的
先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noop
,noop
是空的generator函数。
koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑
最后,有一个非常巧妙的地方,就是最后一行return yield *next;
这行代码可以实现把compose
执行后return的函数变成第一个中间件,也就是说,执行compose
之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。
现在中间件的状态就已经从不可用
变成可用
了。不可用的中间件是一个数组,可用的中间件是一个generator函数。
我们接着说刚才没说完的
上面这段代码现在就可以理解成下面这样
里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap
是干什么用的呢??
co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)
看下源码
从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。
这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑
从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~
到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。
东风就是request请求~~
接收请求
前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。
上面我们说this.callback
这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。
简单回顾下
所以第二部分的重点就是下面段代码啦~
我们先看这段代码
不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???
这行代码就是创建一个最终可用版的context。
从上图中,可以看到分别有五个箭头指向ctx,表示ctx上包含5个属性,分别是request,response,req,res,app。request和response也分别有5个箭头指向它们,所以也是同样的逻辑。
这里需要说明下
request - request继承于Request静态类,包含操作request的一些常用方法
response - response继承于Response静态类,包含操作response的一些常用方法
req - nodejs原生的request对象
res - nodejs原生的response对象
app - koa的原型对象
不多说,咱们观摩下代码
讲到这里其实我可以很明确的告诉大家,,,koa中的this
其实就是app.createContext
方法返回的完整版context
又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文
上下文到这里我们就说完啦。我们接着往下说,看下一行代码
这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror
中的逻辑,设置response类型,状态码和错误信息等。
源码如下:
我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。
我们先看代码
fn - 我们上面讲的
co.wrap
返回的那个函数ctx - app.createContext执行后返回的完整版context对象
总体上来说,执行fn.call(ctx)
会返回promise,koa会监听执行的成功和失败,成功则执行respond.call(ctx);
,失败则执行ctx.onerror
,失败的回调函数刚刚已经讲过。这里先说说respond.call(ctx);
。
我们在写koa的时候,会发现所有的response操作都是this.body = xxx;
this.status = xxxx;
这样的语法,但如果对原生nodejs有了解的童鞋知道,nodejs的response只有一个api那就是res.end();
,而设置status状态码什么的都有不同的api,那么koa是如何做到通过this.xxx = xxx
来设置response的呢?
先看一张图,,我盗的图
从图中看到,request请求是以respond结束的。
是滴,所有的request请求都是以respond这个函数结束的,这个函数会读取this.body中的值根据不同的类型来决定以什么类型响应请求
我们来欣赏一下源码
仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400
这样设置的,为啥代码中没有??
这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了
主要是this.res.statusCode = code;
this.res.statusMessage = statuses[code];
这两句,statusCode
和statusMessage
都是nodejs原生api。有兴趣可以自行查看~
接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?
我们看这段代码
fn刚刚上面说过,就是co.wrap
返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.call
把ctx
设为上下文,也就是this。
那中间件逻辑是什么样的呢。我们先看一下源码:
先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数
吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧
那我们看co的源码:
可以看到,代码并不是很多。
首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。
我们在看这段代码
这个函数最重要的作用是运行gen.next
来执行中间件中的业务逻辑。
通常在开发中间件的时候会这样写
所以ret中包含下一个中间件对象
(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)
然后把下一个中间件对象传到了next(ret)
这个函数里,next函数是干什么的?我们看看
可以看到,逻辑是这样的
如果中间件已经结束(没有yield了),那么调用promise的resolve。
否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);
上面是toPromise中的一段代码
既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。
所以我们可以看到,如果第二个中间件里依然有yield next
这样的语句,那么第三个中间件依然会被co包裹一层并运行.next方法,依次列推,这是一个递归的操作
所以我们可以肯定的是,每一个中间件都被promise包裹着,直到有一天中间件中的逻辑运行完成了,那么会调用promise的resolve来告诉程序这个中间件执行完了。
那么中间件执行完了之后,会触发onFulfilled
,这个函数会执行.next方法。
所以有一个非常重要的一点需要注意,onFulfilled
这个函数非常重要,重要在哪里???重要在它执行的时间上。
onFulfilled
这个函数只在两种情况下被调用,一种是调用co的时候执行,还有一种是当前promise中的所有逻辑都执行完毕后执行
其实就这一句话就能说明koa的中间件为什么会回逆。
回逆其实是有一个去和一个回的操作
请求的时候经过一次中间件,响应的时候在经过一次中间件。
而onFulfilled的两种被调用的情况正好和这个回逆的过程对应上。
前方高能预警!!!
比如有3个中间件,当系统接收到请求的时候,会执行co,co会立刻执行onFulfilled来调用.next往下执行,将得到的返回结果(第二个中间件的generator对象,上面我们分析过)传到co中在执行一遍。以此类推,一直运行到最后一个yield,这个时候系统会等待中间件的执行结束,一旦最后一个中间件执行完毕,会立刻调用promise的resolve方法表示结束。(这个时候onFulfilled函数的第二个执行时机到了,这样就会出现一个现象,一个generator对象的yield只能被next一次,下次执行.next的时候从上一次停顿的yield处继续执行,所以现在当有一个中间件执行完毕后,在执行.next就会在前一个中间件的yield处继续执行)当最后一个中间件执行完毕后,触发promise的resolve,而别忘了,第二个中间件可是用then监听了成功和失败的不同处理方法,一旦第三个中间件触发成功,第二个中间件会立刻调用onFulfilled来执行.next,继续从第二个中间件上一次yield停顿处开始执行下面的代码,而第二个中间件的逻辑执行完毕后,同样会执行resolve表示成功,而这个时候第一个中间件正好也通过.then方法监听了第二个中间件的promise,也会立刻调用onFulfilled函数来执行.next方法,这样就会继续从第一个中间件上一次yield的停顿处继续执行下面的逻辑,以此类推。
这样就实现了中间件的回逆,通过递归从外到里执行一遍中间件,然后在通过promise+generator从里往外跳。
所以如果我们在一个中间件中写好多yield,就可以看出关键所在,先通过递归从外往里(从第一个中间件运行到最后一个中间件)每次遇到yield next就会进入到下一个中间件执行,当运行到最后发现没有yield的时候,会跳回上一个中间件继续执行yield后面的,结果发现又有一个yield next,它会再次进入到下一个中间件,进入到下一个中间件后发现什么都没有,因为yield的特性(一个generator对象的yield只能被next一次,下次执行.next的时候从上一次停顿的yield处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。
我们试一下:
上面的代码打印的log是下面这样的
如果非要画一个图的话,我脑海中大概长这样
到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。
两个重要技术点
最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念
Middleware - 中间件
Context - 上下文
最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。
好东西记得分享哦!
以上是关于深入浅出 Koa的主要内容,如果未能解决你的问题,请参考以下文章