Skip to content

深入浅出 Koa 原理 #8

Description

@berwin

koa

学习koa需要一些相关知识,有两个关键词

  • generator
  • promise

本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

koa总体流程图

让我们从一张图开始

上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

那我们按照时间线说起~

启动前

图中有三个蓝色的方块,分别代表三个静态类

什么是静态类?这个是我自己给起的名,哈哈

静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

三个静态类分别是RequestContextResponse

Request

Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

Response

Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

Context

Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

this.body='hello world';

从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

下面是Context源码片段。

vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

下面是源码片段

functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

简单来个栗子

varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

我们在来看看getter方法

Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

我们在来看看access

Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

启动server

我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

// 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

简单吧?

不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

有木有被刷新三观???

我们看下源码片段

module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

Request和Response的属性和方法委托到Context中也是在这一步进行的

并没有启动server

我们看第二步,在看一段源码

app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

我们一起看一下this.callback是何方神圣

app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

第一部分

先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

童鞋,,,不要着急,听我慢慢道来~~

我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

那么这些中间件处理之后会变成什么样的????

我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

这样一来,第一部分的代码就简化成了这样

varfn=co.wrap(compose(this.middleware));

虽然只剩下一行代码,但不要小瞧它哦~~

我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

例如

// 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

compose是如何实现这样的功能的呢??我们看一下代码

/** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

这是这个模块的所有代码,很简单,逻辑是这样的

先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

最后,有一个非常巧妙的地方,就是最后一行return yield *next;

这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

我们接着说刚才没说完的

varfn=co.wrap(compose(this.middleware));

上面这段代码现在就可以理解成下面这样

varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

看下源码

co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

东风就是request请求~~

接收请求

前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

简单回顾下

// 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

所以第二部分的重点就是下面段代码啦~

returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

我们先看这段代码

varctx=self.createContext(req,res);

不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

这行代码就是创建一个最终可用版的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的原型对象

不多说,咱们观摩下代码

app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

上下文到这里我们就说完啦。我们接着往下说,看下一行代码

onFinished(res,ctx.onerror);

这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

源码如下:

onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

我们先看代码

fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
  • 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中的值根据不同的类型来决定以什么类型响应请求

我们来欣赏一下源码

functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

我们看这段代码

fn.call(ctx)

fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

那中间件逻辑是什么样的呢。我们先看一下源码:

co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

那我们看co的源码:

functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

可以看到,代码并不是很多。

首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

我们在看这段代码

functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

通常在开发中间件的时候会这样写

yieldnext;

所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

可以看到,逻辑是这样的

如果中间件已经结束(没有yield了),那么调用promise的resolve。

否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

上面是toPromise中的一段代码

既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

value.then(onFulfilled,onRejected);

所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

我们试一下:

varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

上面的代码打印的log是下面这样的

f1: prenext
f2: prenext
f3: prenexthelloworld
f3: postnext
f3: fuck
f2: postnext
f2: fuck
f1: postnext
f1: fuck

如果非要画一个图的话,我脑海中大概长这样

其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

用一张图表示大概是这样的。

middleware

为了方便理解,伪代码大概是下面这样

newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

这就是最核心的思想!!!

总结

简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

比如有几个中间件,mw1,mw2,mw3,mwn...

站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

两个重要技术点

最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

  • Middleware - 中间件
  • Context - 上下文

最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

ppt:http://berwin.github.io/ppts/koa/

转载请注明出处

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
     blocks
    (function() {
    function addCopyButtons() {
    document.querySelectorAll('pre code').forEach(function(codeBlock) {
    if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
    codeBlock.parentElement.setAttribute('data-copy-added', 'true');
    var btn = document.createElement('button');
    btn.textContent = 'Copy';
    btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
    btn.onmouseover = function() { this.style.opacity = '1'; };
    btn.onmouseout = function() { this.style.opacity = '0.7'; };
    btn.onclick = function() {
    navigator.clipboard.writeText(codeBlock.textContent).then(function() {
    btn.textContent = 'Copied!';
    setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
    });
    };
    codeBlock.parentElement.style.position = 'relative';
    codeBlock.parentElement.appendChild(btn);
    });
    }
    addCopyButtons();
    // Re-run on dynamic content
    var observer = new MutationObserver(addCopyButtons);
    observer.observe(document.body, { childList: true, subtree: true });
    })();
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    深入浅出 Koa 原理 · Issue #8 · berwin/Blog · GitHub
    Skip to content

    深入浅出 Koa 原理 #8

    Description

    @berwin

    koa

    学习koa需要一些相关知识,有两个关键词

    • generator
    • promise

    本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

    如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

    koa总体流程图

    让我们从一张图开始

    上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

    那我们按照时间线说起~

    启动前

    图中有三个蓝色的方块,分别代表三个静态类

    什么是静态类?这个是我自己给起的名,哈哈

    静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

    三个静态类分别是RequestContextResponse

    Request

    Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

    Response

    Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

    Context

    Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

    this.body='hello world';

    从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

    Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

    下面是Context源码片段。

    vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

    delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

    对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

    method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

    下面是源码片段

    functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

    从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

    简单来个栗子

    varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

    我们在来看看getter方法

    Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

    可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

    我们在来看看access

    Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

    可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

    应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

    启动server

    我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

    // 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

    简单吧?

    不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

    事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

    有木有被刷新三观???

    我们看下源码片段

    module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

    从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

    Request和Response的属性和方法委托到Context中也是在这一步进行的

    并没有启动server

    我们看第二步,在看一段源码

    app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

    可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

    我们一起看一下this.callback是何方神圣

    app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

    这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

    而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

    第一部分

    先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

    童鞋,,,不要着急,听我慢慢道来~~

    我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

    很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

    那么这些中间件处理之后会变成什么样的????

    我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

    this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

    我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

    这样一来,第一部分的代码就简化成了这样

    varfn=co.wrap(compose(this.middleware));

    虽然只剩下一行代码,但不要小瞧它哦~~

    我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

    例如

    // 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

    有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

    这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

    compose是如何实现这样的功能的呢??我们看一下代码

    /** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

    这是这个模块的所有代码,很简单,逻辑是这样的

    先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

    koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

    最后,有一个非常巧妙的地方,就是最后一行return yield *next;

    这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

    现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

    我们接着说刚才没说完的

    varfn=co.wrap(compose(this.middleware));

    上面这段代码现在就可以理解成下面这样

    varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

    里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

    co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

    看下源码

    co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

    从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

    这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

    app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

    从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

    到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

    东风就是request请求~~

    接收请求

    前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

    上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

    简单回顾下

    // 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

    所以第二部分的重点就是下面段代码啦~

    returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

    我们先看这段代码

    varctx=self.createContext(req,res);

    不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

    这行代码就是创建一个最终可用版的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的原型对象

    不多说,咱们观摩下代码

    app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

    讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

    又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

    上下文到这里我们就说完啦。我们接着往下说,看下一行代码

    onFinished(res,ctx.onerror);

    这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

    源码如下:

    onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

    我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

    我们先看代码

    fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
    • 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中的值根据不同的类型来决定以什么类型响应请求

    我们来欣赏一下源码

    functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

    仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

    这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

    setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

    主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

    接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

    我们看这段代码

    fn.call(ctx)

    fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

    那中间件逻辑是什么样的呢。我们先看一下源码:

    co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

    先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

    那我们看co的源码:

    functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

    可以看到,代码并不是很多。

    首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

    我们在看这段代码

    functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

    这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

    通常在开发中间件的时候会这样写

    yieldnext;

    所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

    然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

    functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

    可以看到,逻辑是这样的

    如果中间件已经结束(没有yield了),那么调用promise的resolve。

    否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

    if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

    上面是toPromise中的一段代码

    既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

    value.then(onFulfilled,onRejected);

    所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

    我们试一下:

    varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

    上面的代码打印的log是下面这样的

    f1: prenext
    f2: prenext
    f3: prenexthelloworld
    f3: postnext
    f3: fuck
    f2: postnext
    f2: fuck
    f1: postnext
    f1: fuck

    如果非要画一个图的话,我脑海中大概长这样

    其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

    第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

    用一张图表示大概是这样的。

    middleware

    为了方便理解,伪代码大概是下面这样

    newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

    这就是最核心的思想!!!

    总结

    简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

    其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

    比如有几个中间件,mw1,mw2,mw3,mwn...

    站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

    到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

    两个重要技术点

    最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

    • Middleware - 中间件
    • Context - 上下文

    最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

    ppt:http://berwin.github.io/ppts/koa/

    转载请注明出处

    Metadata

    Metadata

    Assignees

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 深入浅出 Koa 原理 · Issue #8 · berwin/Blog · GitHub
      Skip to content

      深入浅出 Koa 原理 #8

      Description

      @berwin

      koa

      学习koa需要一些相关知识,有两个关键词

      • generator
      • promise

      本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

      如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

      koa总体流程图

      让我们从一张图开始

      上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

      那我们按照时间线说起~

      启动前

      图中有三个蓝色的方块,分别代表三个静态类

      什么是静态类?这个是我自己给起的名,哈哈

      静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

      三个静态类分别是RequestContextResponse

      Request

      Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

      Response

      Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

      Context

      Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

      this.body='hello world';

      从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

      Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

      下面是Context源码片段。

      vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

      delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

      对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

      method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

      下面是源码片段

      functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

      从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

      简单来个栗子

      varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

      我们在来看看getter方法

      Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

      可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

      我们在来看看access

      Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

      可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

      应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

      启动server

      我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

      // 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

      简单吧?

      不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

      事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

      有木有被刷新三观???

      我们看下源码片段

      module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

      从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

      Request和Response的属性和方法委托到Context中也是在这一步进行的

      并没有启动server

      我们看第二步,在看一段源码

      app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

      可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

      我们一起看一下this.callback是何方神圣

      app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
      ? compose_es7(this.middleware)
      : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

      这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

      而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

      第一部分

      先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

      童鞋,,,不要着急,听我慢慢道来~~

      我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

      很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

      那么这些中间件处理之后会变成什么样的????

      我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

      this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

      我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

      这样一来,第一部分的代码就简化成了这样

      varfn=co.wrap(compose(this.middleware));

      虽然只剩下一行代码,但不要小瞧它哦~~

      我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

      例如

      // 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

      有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

      这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

      compose是如何实现这样的功能的呢??我们看一下代码

      /** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

      这是这个模块的所有代码,很简单,逻辑是这样的

      先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

      koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

      最后,有一个非常巧妙的地方,就是最后一行return yield *next;

      这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

      现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

      我们接着说刚才没说完的

      varfn=co.wrap(compose(this.middleware));

      上面这段代码现在就可以理解成下面这样

      varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

      里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

      co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

      看下源码

      co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

      从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

      这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

      app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
      ? compose_es7(this.middleware)
      : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

      从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

      到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

      东风就是request请求~~

      接收请求

      前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

      上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

      简单回顾下

      // 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
      ? compose_es7(this.middleware)
      : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

      所以第二部分的重点就是下面段代码啦~

      returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

      我们先看这段代码

      varctx=self.createContext(req,res);

      不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

      这行代码就是创建一个最终可用版的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的原型对象

      不多说,咱们观摩下代码

      app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

      讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

      又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

      上下文到这里我们就说完啦。我们接着往下说,看下一行代码

      onFinished(res,ctx.onerror);

      这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

      源码如下:

      onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

      我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

      我们先看代码

      fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
      • 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中的值根据不同的类型来决定以什么类型响应请求

      我们来欣赏一下源码

      functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

      仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

      这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

      setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

      主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

      接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

      我们看这段代码

      fn.call(ctx)

      fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

      那中间件逻辑是什么样的呢。我们先看一下源码:

      co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

      先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

      那我们看co的源码:

      functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

      可以看到,代码并不是很多。

      首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

      我们在看这段代码

      functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

      这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

      通常在开发中间件的时候会这样写

      yieldnext;

      所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

      然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

      functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

      可以看到,逻辑是这样的

      如果中间件已经结束(没有yield了),那么调用promise的resolve。

      否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

      if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

      上面是toPromise中的一段代码

      既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

      value.then(onFulfilled,onRejected);

      所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

      我们试一下:

      varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

      上面的代码打印的log是下面这样的

      f1: prenext
      f2: prenext
      f3: prenexthelloworld
      f3: postnext
      f3: fuck
      f2: postnext
      f2: fuck
      f1: postnext
      f1: fuck

      如果非要画一个图的话,我脑海中大概长这样

      其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

      第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

      用一张图表示大概是这样的。

      middleware

      为了方便理解,伪代码大概是下面这样

      newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

      这就是最核心的思想!!!

      总结

      简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

      其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

      比如有几个中间件,mw1,mw2,mw3,mwn...

      站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

      到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

      两个重要技术点

      最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

      • Middleware - 中间件
      • Context - 上下文

      最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

      ppt:http://berwin.github.io/ppts/koa/

      转载请注明出处

      Metadata

      Metadata

      Assignees

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 深入浅出 Koa 原理 · Issue #8 · berwin/Blog · GitHub
        Skip to content

        深入浅出 Koa 原理 #8

        Description

        @berwin

        koa

        学习koa需要一些相关知识,有两个关键词

        • generator
        • promise

        本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

        如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

        koa总体流程图

        让我们从一张图开始

        上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

        那我们按照时间线说起~

        启动前

        图中有三个蓝色的方块,分别代表三个静态类

        什么是静态类?这个是我自己给起的名,哈哈

        静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

        三个静态类分别是RequestContextResponse

        Request

        Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

        Response

        Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

        Context

        Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

        this.body='hello world';

        从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

        Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

        下面是Context源码片段。

        vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

        delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

        对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

        method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

        下面是源码片段

        functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

        从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

        简单来个栗子

        varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

        我们在来看看getter方法

        Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

        可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

        我们在来看看access

        Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

        可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

        应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

        启动server

        我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

        // 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

        简单吧?

        不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

        事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

        有木有被刷新三观???

        我们看下源码片段

        module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

        从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

        Request和Response的属性和方法委托到Context中也是在这一步进行的

        并没有启动server

        我们看第二步,在看一段源码

        app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

        可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

        我们一起看一下this.callback是何方神圣

        app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
        ? compose_es7(this.middleware)
        : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

        这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

        而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

        第一部分

        先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

        童鞋,,,不要着急,听我慢慢道来~~

        我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

        很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

        那么这些中间件处理之后会变成什么样的????

        我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

        this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

        我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

        这样一来,第一部分的代码就简化成了这样

        varfn=co.wrap(compose(this.middleware));

        虽然只剩下一行代码,但不要小瞧它哦~~

        我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

        例如

        // 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

        有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

        这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

        compose是如何实现这样的功能的呢??我们看一下代码

        /** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

        这是这个模块的所有代码,很简单,逻辑是这样的

        先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

        koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

        最后,有一个非常巧妙的地方,就是最后一行return yield *next;

        这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

        现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

        我们接着说刚才没说完的

        varfn=co.wrap(compose(this.middleware));

        上面这段代码现在就可以理解成下面这样

        varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

        里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

        co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

        看下源码

        co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

        从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

        这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

        app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
        ? compose_es7(this.middleware)
        : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

        从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

        到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

        东风就是request请求~~

        接收请求

        前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

        上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

        简单回顾下

        // 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
        ? compose_es7(this.middleware)
        : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

        所以第二部分的重点就是下面段代码啦~

        returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

        我们先看这段代码

        varctx=self.createContext(req,res);

        不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

        这行代码就是创建一个最终可用版的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的原型对象

        不多说,咱们观摩下代码

        app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

        讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

        又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

        上下文到这里我们就说完啦。我们接着往下说,看下一行代码

        onFinished(res,ctx.onerror);

        这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

        源码如下:

        onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

        我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

        我们先看代码

        fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
        • 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中的值根据不同的类型来决定以什么类型响应请求

        我们来欣赏一下源码

        functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

        仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

        这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

        setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

        主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

        接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

        我们看这段代码

        fn.call(ctx)

        fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

        那中间件逻辑是什么样的呢。我们先看一下源码:

        co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

        先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

        那我们看co的源码:

        functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

        可以看到,代码并不是很多。

        首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

        我们在看这段代码

        functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

        这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

        通常在开发中间件的时候会这样写

        yieldnext;

        所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

        然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

        functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

        可以看到,逻辑是这样的

        如果中间件已经结束(没有yield了),那么调用promise的resolve。

        否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

        if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

        上面是toPromise中的一段代码

        既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

        value.then(onFulfilled,onRejected);

        所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

        我们试一下:

        varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

        上面的代码打印的log是下面这样的

        f1: prenext
        f2: prenext
        f3: prenexthelloworld
        f3: postnext
        f3: fuck
        f2: postnext
        f2: fuck
        f1: postnext
        f1: fuck

        如果非要画一个图的话,我脑海中大概长这样

        其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

        第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

        用一张图表示大概是这样的。

        middleware

        为了方便理解,伪代码大概是下面这样

        newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

        这就是最核心的思想!!!

        总结

        简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

        其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

        比如有几个中间件,mw1,mw2,mw3,mwn...

        站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

        到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

        两个重要技术点

        最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

        • Middleware - 中间件
        • Context - 上下文

        最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

        ppt:http://berwin.github.io/ppts/koa/

        转载请注明出处

        Metadata

        Metadata

        Assignees

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' 深入浅出 Koa 原理 · Issue #8 · berwin/Blog · GitHub
          Skip to content

          深入浅出 Koa 原理 #8

          Description

          @berwin

          koa

          学习koa需要一些相关知识,有两个关键词

          • generator
          • promise

          本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

          如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

          koa总体流程图

          让我们从一张图开始

          上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

          那我们按照时间线说起~

          启动前

          图中有三个蓝色的方块,分别代表三个静态类

          什么是静态类?这个是我自己给起的名,哈哈

          静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

          三个静态类分别是RequestContextResponse

          Request

          Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

          Response

          Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

          Context

          Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

          this.body='hello world';

          从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

          Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

          下面是Context源码片段。

          vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

          delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

          对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

          method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

          下面是源码片段

          functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

          从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

          简单来个栗子

          varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

          我们在来看看getter方法

          Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

          可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

          我们在来看看access

          Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

          可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

          应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

          启动server

          我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

          // 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

          简单吧?

          不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

          事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

          有木有被刷新三观???

          我们看下源码片段

          module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

          从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

          Request和Response的属性和方法委托到Context中也是在这一步进行的

          并没有启动server

          我们看第二步,在看一段源码

          app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

          可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

          我们一起看一下this.callback是何方神圣

          app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
          ? compose_es7(this.middleware)
          : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

          这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

          而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

          第一部分

          先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

          童鞋,,,不要着急,听我慢慢道来~~

          我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

          很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

          那么这些中间件处理之后会变成什么样的????

          我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

          this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

          我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

          这样一来,第一部分的代码就简化成了这样

          varfn=co.wrap(compose(this.middleware));

          虽然只剩下一行代码,但不要小瞧它哦~~

          我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

          例如

          // 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

          有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

          这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

          compose是如何实现这样的功能的呢??我们看一下代码

          /** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

          这是这个模块的所有代码,很简单,逻辑是这样的

          先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

          koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

          最后,有一个非常巧妙的地方,就是最后一行return yield *next;

          这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

          现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

          我们接着说刚才没说完的

          varfn=co.wrap(compose(this.middleware));

          上面这段代码现在就可以理解成下面这样

          varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

          里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

          co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

          看下源码

          co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

          从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

          这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

          app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
          ? compose_es7(this.middleware)
          : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

          从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

          到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

          东风就是request请求~~

          接收请求

          前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

          上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

          简单回顾下

          // 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
          ? compose_es7(this.middleware)
          : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

          所以第二部分的重点就是下面段代码啦~

          returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

          我们先看这段代码

          varctx=self.createContext(req,res);

          不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

          这行代码就是创建一个最终可用版的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的原型对象

          不多说,咱们观摩下代码

          app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

          讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

          又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

          上下文到这里我们就说完啦。我们接着往下说,看下一行代码

          onFinished(res,ctx.onerror);

          这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

          源码如下:

          onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

          我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

          我们先看代码

          fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
          • 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中的值根据不同的类型来决定以什么类型响应请求

          我们来欣赏一下源码

          functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

          仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

          这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

          setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

          主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

          接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

          我们看这段代码

          fn.call(ctx)

          fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

          那中间件逻辑是什么样的呢。我们先看一下源码:

          co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

          先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

          那我们看co的源码:

          functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

          可以看到,代码并不是很多。

          首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

          我们在看这段代码

          functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

          这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

          通常在开发中间件的时候会这样写

          yieldnext;

          所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

          然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

          functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

          可以看到,逻辑是这样的

          如果中间件已经结束(没有yield了),那么调用promise的resolve。

          否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

          if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

          上面是toPromise中的一段代码

          既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

          value.then(onFulfilled,onRejected);

          所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

          我们试一下:

          varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

          上面的代码打印的log是下面这样的

          f1: prenext
          f2: prenext
          f3: prenexthelloworld
          f3: postnext
          f3: fuck
          f2: postnext
          f2: fuck
          f1: postnext
          f1: fuck

          如果非要画一个图的话,我脑海中大概长这样

          其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

          第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

          用一张图表示大概是这样的。

          middleware

          为了方便理解,伪代码大概是下面这样

          newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

          这就是最核心的思想!!!

          总结

          简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

          其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

          比如有几个中间件,mw1,mw2,mw3,mwn...

          站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

          到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

          两个重要技术点

          最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

          • Middleware - 中间件
          • Context - 上下文

          最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

          ppt:http://berwin.github.io/ppts/koa/

          转载请注明出处

          Metadata

          Metadata

          Assignees

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 深入浅出 Koa 原理 · Issue #8 · berwin/Blog · GitHub
            Skip to content

            深入浅出 Koa 原理 #8

            Description

            @berwin

            koa

            学习koa需要一些相关知识,有两个关键词

            • generator
            • promise

            本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

            如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

            koa总体流程图

            让我们从一张图开始

            上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

            那我们按照时间线说起~

            启动前

            图中有三个蓝色的方块,分别代表三个静态类

            什么是静态类?这个是我自己给起的名,哈哈

            静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

            三个静态类分别是RequestContextResponse

            Request

            Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

            Response

            Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

            Context

            Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

            this.body='hello world';

            从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

            Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

            下面是Context源码片段。

            vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

            delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

            对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

            method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

            下面是源码片段

            functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

            从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

            简单来个栗子

            varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

            我们在来看看getter方法

            Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

            可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

            我们在来看看access

            Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

            可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

            应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

            启动server

            我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

            // 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

            简单吧?

            不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

            事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

            有木有被刷新三观???

            我们看下源码片段

            module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

            从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

            Request和Response的属性和方法委托到Context中也是在这一步进行的

            并没有启动server

            我们看第二步,在看一段源码

            app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

            可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

            我们一起看一下this.callback是何方神圣

            app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
            ? compose_es7(this.middleware)
            : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

            这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

            而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

            第一部分

            先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

            童鞋,,,不要着急,听我慢慢道来~~

            我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

            很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

            那么这些中间件处理之后会变成什么样的????

            我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

            this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

            我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

            这样一来,第一部分的代码就简化成了这样

            varfn=co.wrap(compose(this.middleware));

            虽然只剩下一行代码,但不要小瞧它哦~~

            我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

            例如

            // 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

            有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

            这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

            compose是如何实现这样的功能的呢??我们看一下代码

            /** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

            这是这个模块的所有代码,很简单,逻辑是这样的

            先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

            koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

            最后,有一个非常巧妙的地方,就是最后一行return yield *next;

            这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

            现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

            我们接着说刚才没说完的

            varfn=co.wrap(compose(this.middleware));

            上面这段代码现在就可以理解成下面这样

            varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

            里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

            co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

            看下源码

            co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

            从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

            这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

            app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
            ? compose_es7(this.middleware)
            : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

            从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

            到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

            东风就是request请求~~

            接收请求

            前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

            上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

            简单回顾下

            // 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
            ? compose_es7(this.middleware)
            : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

            所以第二部分的重点就是下面段代码啦~

            returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

            我们先看这段代码

            varctx=self.createContext(req,res);

            不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

            这行代码就是创建一个最终可用版的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的原型对象

            不多说,咱们观摩下代码

            app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

            讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

            又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

            上下文到这里我们就说完啦。我们接着往下说,看下一行代码

            onFinished(res,ctx.onerror);

            这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

            源码如下:

            onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

            我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

            我们先看代码

            fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
            • 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中的值根据不同的类型来决定以什么类型响应请求

            我们来欣赏一下源码

            functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

            仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

            这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

            setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

            主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

            接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

            我们看这段代码

            fn.call(ctx)

            fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

            那中间件逻辑是什么样的呢。我们先看一下源码:

            co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

            先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

            那我们看co的源码:

            functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

            可以看到,代码并不是很多。

            首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

            我们在看这段代码

            functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

            这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

            通常在开发中间件的时候会这样写

            yieldnext;

            所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

            然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

            functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

            可以看到,逻辑是这样的

            如果中间件已经结束(没有yield了),那么调用promise的resolve。

            否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

            if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

            上面是toPromise中的一段代码

            既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

            value.then(onFulfilled,onRejected);

            所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

            我们试一下:

            varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

            上面的代码打印的log是下面这样的

            f1: prenext
            f2: prenext
            f3: prenexthelloworld
            f3: postnext
            f3: fuck
            f2: postnext
            f2: fuck
            f1: postnext
            f1: fuck

            如果非要画一个图的话,我脑海中大概长这样

            其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

            第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

            用一张图表示大概是这样的。

            middleware

            为了方便理解,伪代码大概是下面这样

            newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

            这就是最核心的思想!!!

            总结

            简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

            其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

            比如有几个中间件,mw1,mw2,mw3,mwn...

            站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

            到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

            两个重要技术点

            最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

            • Middleware - 中间件
            • Context - 上下文

            最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

            ppt:http://berwin.github.io/ppts/koa/

            转载请注明出处

            Metadata

            Metadata

            Assignees

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 深入浅出 Koa 原理 · Issue #8 · berwin/Blog · GitHub
              Skip to content

              深入浅出 Koa 原理 #8

              Description

              @berwin

              koa

              学习koa需要一些相关知识,有两个关键词

              • generator
              • promise

              本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

              如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

              koa总体流程图

              让我们从一张图开始

              上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

              那我们按照时间线说起~

              启动前

              图中有三个蓝色的方块,分别代表三个静态类

              什么是静态类?这个是我自己给起的名,哈哈

              静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

              三个静态类分别是RequestContextResponse

              Request

              Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

              Response

              Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

              Context

              Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

              this.body='hello world';

              从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

              Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

              下面是Context源码片段。

              vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

              delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

              对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

              method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

              下面是源码片段

              functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

              从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

              简单来个栗子

              varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

              我们在来看看getter方法

              Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

              可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

              我们在来看看access

              Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

              可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

              应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

              启动server

              我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

              // 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

              简单吧?

              不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

              事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

              有木有被刷新三观???

              我们看下源码片段

              module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

              从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

              Request和Response的属性和方法委托到Context中也是在这一步进行的

              并没有启动server

              我们看第二步,在看一段源码

              app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

              可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

              我们一起看一下this.callback是何方神圣

              app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
              ? compose_es7(this.middleware)
              : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

              这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

              而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

              第一部分

              先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

              童鞋,,,不要着急,听我慢慢道来~~

              我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

              很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

              那么这些中间件处理之后会变成什么样的????

              我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

              this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

              我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

              这样一来,第一部分的代码就简化成了这样

              varfn=co.wrap(compose(this.middleware));

              虽然只剩下一行代码,但不要小瞧它哦~~

              我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

              例如

              // 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

              有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

              这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

              compose是如何实现这样的功能的呢??我们看一下代码

              /** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

              这是这个模块的所有代码,很简单,逻辑是这样的

              先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

              koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

              最后,有一个非常巧妙的地方,就是最后一行return yield *next;

              这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

              现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

              我们接着说刚才没说完的

              varfn=co.wrap(compose(this.middleware));

              上面这段代码现在就可以理解成下面这样

              varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

              里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

              co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

              看下源码

              co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

              从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

              这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

              app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
              ? compose_es7(this.middleware)
              : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

              从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

              到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

              东风就是request请求~~

              接收请求

              前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

              上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

              简单回顾下

              // 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
              ? compose_es7(this.middleware)
              : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

              所以第二部分的重点就是下面段代码啦~

              returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

              我们先看这段代码

              varctx=self.createContext(req,res);

              不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

              这行代码就是创建一个最终可用版的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的原型对象

              不多说,咱们观摩下代码

              app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

              讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

              又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

              上下文到这里我们就说完啦。我们接着往下说,看下一行代码

              onFinished(res,ctx.onerror);

              这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

              源码如下:

              onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

              我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

              我们先看代码

              fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
              • 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中的值根据不同的类型来决定以什么类型响应请求

              我们来欣赏一下源码

              functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

              仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

              这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

              setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

              主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

              接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

              我们看这段代码

              fn.call(ctx)

              fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

              那中间件逻辑是什么样的呢。我们先看一下源码:

              co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

              先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

              那我们看co的源码:

              functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

              可以看到,代码并不是很多。

              首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

              我们在看这段代码

              functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

              这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

              通常在开发中间件的时候会这样写

              yieldnext;

              所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

              然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

              functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

              可以看到,逻辑是这样的

              如果中间件已经结束(没有yield了),那么调用promise的resolve。

              否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

              if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

              上面是toPromise中的一段代码

              既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

              value.then(onFulfilled,onRejected);

              所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

              我们试一下:

              varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

              上面的代码打印的log是下面这样的

              f1: prenext
              f2: prenext
              f3: prenexthelloworld
              f3: postnext
              f3: fuck
              f2: postnext
              f2: fuck
              f1: postnext
              f1: fuck

              如果非要画一个图的话,我脑海中大概长这样

              其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

              第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

              用一张图表示大概是这样的。

              middleware

              为了方便理解,伪代码大概是下面这样

              newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

              这就是最核心的思想!!!

              总结

              简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

              其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

              比如有几个中间件,mw1,mw2,mw3,mwn...

              站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

              到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

              两个重要技术点

              最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

              • Middleware - 中间件
              • Context - 上下文

              最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

              ppt:http://berwin.github.io/ppts/koa/

              转载请注明出处

              Metadata

              Metadata

              Assignees

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); 深入浅出 Koa 原理 · Issue #8 · berwin/Blog · GitHub
                Skip to content

                深入浅出 Koa 原理 #8

                Description

                @berwin

                koa

                学习koa需要一些相关知识,有两个关键词

                • generator
                • promise

                本文主要针对koa的原理进行讨论,属于深度篇,并不会对koa的使用过多介绍。

                如果在阅读过程中,发现有哪些地方我写的不太清楚,不容易理解,希望能提出,我会参考建议并进行修改~~

                koa总体流程图

                让我们从一张图开始

                上图中,详细说明了koa从启动server之前,到接受请求在到响应请求的过程中,经历了哪些步骤。

                那我们按照时间线说起~

                启动前

                图中有三个蓝色的方块,分别代表三个静态类

                什么是静态类?这个是我自己给起的名,哈哈

                静态类就是程序运行前就存在的方法集合,动态类就是通过代码生成出的方法集合。额,都是我自己起的名,概念也是我自己琢磨的,就是简单归个类。

                三个静态类分别是RequestContextResponse

                Request

                Request中包含了一些操作 Node原生请求对象的非常有用的方法。例如获取query数据,获取请求url等,更多方法去查API

                Response

                Response中包含了一些用于设置状态码啦,主体数据啦,header啦,等一些用于操作响应请求的方法。更多方法去查API

                Context

                Context是koa中最重要的概念之一,Context字面意思是上下文,也有环境等意思,koa中的操作都是基于这个context进行的,例如

                this.body='hello world';

                从前面的图中,启动前的三个蓝色方块可以看到,左边的Request和右边的Response各有一个箭头指向Context,表示Request和Response自身的方法会委托到Context中。

                Context中有两部分,一部分是自身属性,主要是应用于框架内部使用,一部分是Request和Response委托的操作方法,主要为提供给用户更方便从Request获取想要的参数和更方便的设置Response内容。

                下面是Context源码片段。

                vardelegate=require('delegates');varproto=module.exports={};// 一些自身方法,被我删了/** * Response delegation. */delegate(proto,'response').method('attachment').method('redirect').method('remove').method('vary').method('set').method('append').access('status').access('message').access('body').access('length').access('type').access('lastModified').access('etag').getter('headerSent').getter('writable');/** * Request delegation. */delegate(proto,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is').access('querystring').access('idempotent').access('socket').access('search').access('method').access('query').access('path').access('url').getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host').getter('hostname').getter('header').getter('headers').getter('secure').getter('stale').getter('fresh').getter('ips').getter('ip');

                delegates是第三方npm包,功能就是把一个对象上的方法,属性委托到另一个对象上

                对了,你猜对了,上面那一排方法,都是Request和Response静态类中的方法,有点看目录的感觉~

                method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

                下面是源码片段

                functionDelegator(proto,target){if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);this.proto=proto;this.target=target;this.methods=[];this.getters=[];this.setters=[];this.fluents=[];}Delegator.prototype.method=function(name){varproto=this.proto;vartarget=this.target;this.methods.push(name);proto[name]=function(){returnthis[target][name].apply(this[target],arguments);};returnthis;};

                从上面的代码中可以看到,它其实是在proto上新建一个与Request和Response上的方法名一样的函数,然后执行这个函数的时候,这个函数在去Request和Response上去找对应的方法并执行。

                简单来个栗子

                varproto={};varRequest={test: function(){console.log('test');}};varname='test';proto[name]=function(){returnRequest[name].apply(Request,arguments);};

                我们在来看看getter方法

                Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(name);proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};

                可以看到,在proto上绑定个getter函数,当函数被触发的时候去,会去对应的request或response中去读取对应的属性,这样request或response的getter同样会被触发~

                我们在来看看access

                Delegator.prototype.access=function(name){returnthis.getter(name).setter(name);};

                可以看到,这个方法是getter+setter,getter上面刚说过,setter与getter同理,不多说了,心好累....

                应用启动前的内容到现在就说完了,接下来我们看看使用koa来启动一个app的时候,koa内部会发生什么呢?

                启动server

                我们使用koa来启动server的时候有两个步骤。第一步是init一个app对象,第二步是用app对象监听下端口号,一个server就启动好了。

                // 第一步 - 初始化app对象varkoa=require('koa');varapp=koa();// 第二步 - 监听端口app.listen(1995);

                简单吧?

                不了解内部机制的同学,通常会认为server是在koa()这个时候启动的,app.listen只是监听下端口而已~

                事实上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。并不是。

                有木有被刷新三观???

                我们看下源码片段

                module.exports=Application;functionApplication(){if(!(thisinstanceofApplication))returnnewApplication;this.env=process.env.NODE_ENV||'development';this.subdomainOffset=2;this.middleware=[];this.proxy=false;this.context=Object.create(context);this.request=Object.create(request);this.response=Object.create(response);}

                从源码中可以看到,执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。

                Request和Response的属性和方法委托到Context中也是在这一步进行的

                并没有启动server

                我们看第二步,在看一段源码

                app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};

                可以看到,在执行app.listen(1995)的时候,启动了一个server,并且监听端口。熟悉nodejs的同学知道http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

                我们一起看一下this.callback是何方神圣

                app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
                ? compose_es7(this.middleware)
                : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

                这个方法其实可以分成两部分,一部分是执行函数的那一瞬间所执行的代码,另一部分是接收请求的时候所执行的代码。

                而前一部分就是总体流程图中,启动server这个时间段,黄色椭圆形所执行的那一部分,初始化中间件!!!

                第一部分

                先说第一部分,很明显,这环节是在初始化中间件,那为什么要初始化中间件呢?处理后的中间件与处理之前的中间件又有什么不同呢????

                童鞋,,,不要着急,听我慢慢道来~~

                我们添加中间的时候使用app.use方法,其实这个方法只是把中间件push到一个数组,然后就没有然后了。。(⊙﹏⊙)

                很明显,所有中间件都在数组中,那么它们之间是没有联系的,如果没有联系,就不可能实现流水线这样的功能。。。。

                那么这些中间件处理之后会变成什么样的????

                我们先看代码,上面的代码中用this.experimental这个属性做了一个判断。这个属性是什么鸟。

                this.experimental 关于这个属性我并没有在官方文档上看到说明,但以我对koa的了解,这个方法是为了判断是否支持es7,默认是不支持的,如果想支持,需要在代码中明确指定this.experimental = true,开启这个属性之后,中间件可以传入async函数。

                我想说的是,无论是否开启ES7,原理都是相同的,只是因为语法特性的不同,需要不同的处理,核心思想不会因为不同的语言特性而改变,支持ES7显然处理起来更方便,因为默认不开启this.experimental,所以这里我们针对不开启的情况进行讨论~

                这样一来,第一部分的代码就简化成了这样

                varfn=co.wrap(compose(this.middleware));

                虽然只剩下一行代码,但不要小瞧它哦~~

                我们先看compose(this.middleware)这部分,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。。

                例如

                // 有3个中间件this.middlewares=[function*m1(){},function*m2(){},function*m3(){}];// 通过compose转换varmiddleware=compose(this.middlewares);// 转换后得到的middleware是这个样子的function*(){yield*m1(m2(m3(noop())))}

                有木有很神奇的感觉??更神奇的是,generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。

                这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next

                compose是如何实现这样的功能的呢??我们看一下代码

                /** * Expose compositor. */module.exports=compose;/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}returnyield*next;}}/** * Noop. * * @api private */function*noop(){}

                这是这个模块的所有代码,很简单,逻辑是这样的

                先把中间件从后往前依次执行,并把每一个中间件执行后得到的generator对象赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第一个参数。这样就保证前一个中间件的参数是下一个中间件生成的generator对象,第一次执行的时候next为noopnoop是空的generator函数。

                koa的中间件必须为generator函数(就是带星号的函数),否则无法顺利的执行中间件逻辑

                最后,有一个非常巧妙的地方,就是最后一行return yield *next;

                这行代码可以实现把compose执行后return的函数变成第一个中间件,也就是说,执行compose之后会得到一个函数,执行这个函数就与执行第一个中间件的效果是一模一样的,这主要依赖了generator函数的yield *语句的特性。

                现在中间件的状态就已经从不可用变成可用了。不可用的中间件是一个数组,可用的中间件是一个generator函数

                我们接着说刚才没说完的

                varfn=co.wrap(compose(this.middleware));

                上面这段代码现在就可以理解成下面这样

                varfn=co.wrap(function*(){yield*m1(m2(m3(noop())))});

                里面的函数刚刚已经说过是可用状态的中间件,那么co.wrap是干什么用的呢??

                co是TJ大神基于Generator开发的一款流程控制模块,白话文就是:就是把异步变成同步的模块。。。(感觉逼格瞬间拉低了。。。)

                看下源码

                co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

                从源码中可以看到,它接收一个参数,这个参数就是可用状态下的中间件,返回一个函数createPromise,当执行createPromise这个函数的时候,调用co并传入一个参数,这个参数是中间件函数执行后生成的Generator对象。

                这意味着,返回的这个函数是触发执行中间件逻辑的关键,一旦这个函数被执行,那么就会开始执行中间件逻辑

                app.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
                ? compose_es7(this.middleware)
                : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

                从源码中,可以看到这个函数赋值给fn,fn是在下面那个函数中执行的,下面那个函数是接下来要说的内容~

                到现在,我们的koa已经处于一种待机状态,所有准备都以准备好(中间件和context),万事俱备,只欠东风。。。。。。

                东风就是request请求~~

                接收请求

                前面说了启动前的一些准备工作和启动时的初始化工作,现在最后一步就是接收请求的时候,koa要做的事情了,这部分也是koa中难度最大的一部分。不过认真阅读下去会有收获的。。

                上面我们说this.callback这个方法有两个部分,第一个部分是初始化中间件,而另一部分就是接收请求时执行的函数啦。

                简单回顾下

                // 创建server并监听端口app.listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};// 这个方法返回的函数会被传递到http.createServer中,http.createServer这个方法的作用是每当服务器接收到请求的时候,都会执行第一个参数,并且会传递request和responseapp.callback=function(){if(this.experimental){console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')}varfn=this.experimental
                ? compose_es7(this.middleware)
                : co.wrap(compose(this.middleware));varself=this;if(!this.listeners('error').length)this.on('error',this.onerror);returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}};

                所以第二部分的重点就是下面段代码啦~

                returnfunction(req,res){res.statusCode=404;varctx=self.createContext(req,res);onFinished(res,ctx.onerror);fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);}

                我们先看这段代码

                varctx=self.createContext(req,res);

                不知道各位童鞋还记不记得文章一开始的时候那个总体流程图下面的那个类似于八卦一样的东西???

                这行代码就是创建一个最终可用版的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的原型对象

                不多说,咱们观摩下代码

                app.createContext=function(req,res){// 继承varcontext=Object.create(this.context);varrequest=context.request=Object.create(this.request);varresponse=context.response=Object.create(this.response);// 往context,request,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.onerror=context.onerror.bind(context);context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys: this.keys,secure: request.secure});context.accept=request.accept=accepts(req);context.state={};// 最后返回完整版contextreturncontext;};

                讲到这里其实我可以很明确的告诉大家,,,koa中的this其实就是app.createContext方法返回的完整版context

                又由于这段代码的执行时间是接受请求的时候,所以表明每一次接受到请求,都会为该请求生成一个新的上下文

                上下文到这里我们就说完啦。我们接着往下说,看下一行代码

                onFinished(res,ctx.onerror);

                这行代码其实很简单,就是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

                源码如下:

                onerror: function(err){// don't do anything if there is no error.// this allows you to pass `this.onerror`// to node-style callbacks.if(null==err)return;if(!(errinstanceofError))err=newError('non-error thrown: '+err);// delegatethis.app.emit('error',err,this);// nothing we can do here other// than delegate to the app-level// handler and log.if(this.headerSent||!this.writable){err.headerSent=true;return;}// unset all headersthis.res._headers={};// force text/plainthis.type='text';// ENOENT supportif('ENOENT'==err.code)err.status=404;// default to 500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;// respondvarcode=statuses[err.status];varmsg=err.expose ? err.message : code;this.status=err.status;this.length=Buffer.byteLength(msg);this.res.end(msg);}

                我们接着说,还有最后一个知识点,也是本章最复杂的知识点,关于中间件的执行流程,这里会说明为什么koa的中间件可以回逆。

                我们先看代码

                fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);
                • 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中的值根据不同的类型来决定以什么类型响应请求

                我们来欣赏一下源码

                functionrespond(){// allow bypassing koaif(false===this.respond)return;varres=this.res;if(res.headersSent||!this.writable)return;varbody=this.body;varcode=this.status;// ignore bodyif(statuses.empty[code]){// strip headersthis.body=null;returnres.end();}if('HEAD'==this.method){if(isJSON(body))this.length=Buffer.byteLength(JSON.stringify(body));returnres.end();}// status bodyif(null==body){this.type='text';body=this.message||String(code);this.length=Buffer.byteLength(body);returnres.end(body);}// responsesif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);// body: jsonbody=JSON.stringify(body);this.length=Buffer.byteLength(body);res.end(body);}

                仔细阅读的童鞋会发现,咦,,,,为毛没有设置status和header等信息的代码逻辑?这不科学啊。我分明记得状态码是rs.statusCode = 400这样设置的,为啥代码中没有??

                这就要从最开始的上下文说起了。为什么Response静态类中添加req和res属性?就是因为添加了req和res之后,response和request类就可以直接操作req和res啦。。我们看一段源码就明白了

                setstatus(code){assert('number'==typeofcode,'status code must be a number');assert(statuses[code],'invalid status code: '+code);this._explicitStatus=true;this.res.statusCode=code;this.res.statusMessage=statuses[code];if(this.body&&statuses.empty[code])this.body=null;},

                主要是this.res.statusCode = code;this.res.statusMessage = statuses[code];这两句,statusCodestatusMessage都是nodejs原生api。有兴趣可以自行查看~

                接下来我们开始说说koa的中间件为什么可以回逆,为什么koa的中间件必须使用generator,yield next又是个什么鬼?

                我们看这段代码

                fn.call(ctx)

                fn刚刚上面说过,就是co.wrap返回的那个函数,上面也说过,一旦这个函数执行,就会执行中间件逻辑,并且通过.callctx设为上下文,也就是this。

                那中间件逻辑是什么样的呢。我们先看一下源码:

                co.wrap=function(fn){createPromise.__generatorFunction__=fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};

                先回顾下,createPromise就是fn,每当执行createPromise的时候,都会执行co,中间件是基于co实现的、所以我们接下来要说的是co的实现逻辑。而执行co所传递的那个参数,我们给它起个名,就叫中间件函数吧,中间件函数也是一个generator函数,因为在执行co的时候执行了这个中间件函数,所以实际上真正传递给co的参数是一个generator对象,为了方便理解,我们先起个名叫中间件对象吧

                那我们看co的源码:

                functionco(gen){varctx=this;varargs=slice.call(arguments,1)// we wrap everything in a promise to avoid promise chaining,// which leads to memory leak errors.// see https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/** * @param {Mixed} res * @return {Promise} * @api private */functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/** * @param {Error} err * @return {Promise} * @api private */functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}/** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});}

                可以看到,代码并不是很多。

                首先执行co会返回一个promise,koa会对这个promise的成功和失败都准备了不同的处理,上面已经说过。

                我们在看这段代码

                functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}

                这个函数最重要的作用是运行gen.next来执行中间件中的业务逻辑。

                通常在开发中间件的时候会这样写

                yieldnext;

                所以ret中包含下一个中间件对象(还记得上面我们初始化中间件的时候中间件的参数是什么了吗??)

                然后把下一个中间件对象传到了next(ret)这个函数里,next函数是干什么的?我们看看

                functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}

                可以看到,逻辑是这样的

                如果中间件已经结束(没有yield了),那么调用promise的resolve。

                否则的话把ret.value(就是下一个中间件对象),用co在包一层toPromise.call(ctx, ret.value);

                if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);

                上面是toPromise中的一段代码

                既然是用co又执行了一遍,那么co是返回promise的。所以返回的这个value就分别被监听了成功和失败的不同处理。

                value.then(onFulfilled,onRejected);

                所以我们可以看到,如果第二个中间件里依然有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处继续执行),所以便又一次跳入上一个中间件来执行。以此类推。

                我们试一下:

                varkoa=require('koa');varapp=koa();app.use(function*f1(next){console.log('f1: pre next');yieldnext;console.log('f1: post next');yieldnext;console.log('f1: fuck');});app.use(function*f2(next){console.log(' f2: pre next');yieldnext;console.log(' f2: post next');yieldnext;console.log(' f2: fuck');});app.use(function*f3(next){console.log(' f3: pre next');yieldnext;console.log(' f3: post next');yieldnext;console.log(' f3: fuck');});app.use(function*(next){console.log('hello world')this.body='hello world';});app.listen(3000);

                上面的代码打印的log是下面这样的

                f1: prenext
                f2: prenext
                f3: prenexthelloworld
                f3: postnext
                f3: fuck
                f2: postnext
                f2: fuck
                f1: postnext
                f1: fuck

                如果非要画一个图的话,我脑海中大概长这样

                其实刚刚那么一通复杂的逻辑下来,好多同学都会懵逼,那么我用白话文来说一下中间件的逻辑,大概是这样的

                第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束

                用一张图表示大概是这样的。

                middleware

                为了方便理解,伪代码大概是下面这样

                newPromise(function(resolve,reject){// 我是中间件1yieldnewPromise(function(resolve,reject){// 我是中间件2yieldnewPromise(function(resolve,reject){// 我是中间件3yieldnewPromise(function(resolve,reject){// 我是body});// 我是中间件3});// 我是中间件2});// 我是中间件1});

                这就是最核心的思想!!!

                总结

                简单总结一下,其实也很简单,只是第一次接触的同学可能暂时没有理解透彻。

                其实就是通过generator来暂停函数的执行逻辑来实现等待中间件的效果,通过监听promise来触发继续执行函数逻辑,所谓的回逆也不过就是同步执行了下一个中间件罢了。

                比如有几个中间件,mw1,mw2,mw3,mwn...

                站在mw1的角度上看,它是不需要关系mw2里面有没有mw3,它只需要关心mw2何时执行完毕即可,当mw2执行完毕mw1继续执行yield之后的代码逻辑。其实很简单,callback也是这个原理,当mw2执行完毕执行下callback,mw1是不需要关心mw2里面究竟是怎样运行的,只要知道mw2执行完会执行回调就行了。mw2也是同样的道理不需要关心mw3。

                到这里,关于koa我们就已经差不多都说完了。当然还有一些细节没有说,比如koa中的错误处理,但其实都是小问题啦,关于generator的错误处理部分弄明白了,自然就明白koa的错误处理是怎样的。这里就不在针对这些讲述了,一次写这么多确实有点累,或许后期会补充进来吧。。

                两个重要技术点

                最后,如果认真阅读下来的同学能感觉出来,koa中有两个最重要的地方,无论是使用上,还是思想上,这两个点都非常重要,koa也只有这两个概念

                • Middleware - 中间件
                • Context - 上下文

                最后说一些自己对koa的感觉,真他妈的是赏心悦目啊,真他妈的是优雅啊!!!每一行代码都浓缩了很多层含义,通过最少的代码实现最复杂的功能,对于我这种追求代码的极致优雅的人,看完koa之后,真的是感触良多,泪流满面啊。。。。

                ppt:http://berwin.github.io/ppts/koa/

                转载请注明出处

                Metadata

                Metadata

                Assignees

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions