配色: 字号:
KOA 与 CO 实现浅析
2016-10-28 | 阅:  转:  |  分享 
  
KOA与CO实现浅析



KOA与CO的实现都非常的短小精悍,只需要花费很短的时间就可以将源代码通读一遍。以下是一些浅要的分析。



如何用node实现一个web服务器

既然KOA实现了web服务器,那我们就先从最原始的web服务器的实现方式着手。

下面的代码中我们创建了一个始终返回请求路径的web服务器。



consthttp=require(''http'');

constserver=http.createServer((req,res)=>{

res.end(req.url);

});

server.listen(8001);

当你请求http://localhost:8001/some/url的时候,得到的响应就是/some/url。



KOA的实现

简单的说,KOA就是对上面这段代码的封装。



首先看下KOA的大概目录结构:







lib目录下只有四个文件,其中request.js和response.js是对node原生的request(req)和response(res)的增强,提供了很多便利的方法,context.js就是著名的上下文。我们暂时抛开这三个文件的细节,先看下主文件application.js的实现。



先关注两个函数:



//构造函数

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);

}

//listen方法

app.listen=function(){

debug(''listen'');

varserver=http.createServer(this.callback());

returnserver.listen.apply(server,arguments);

};

上面的这两个函数,正是完成了一个web服务器的建立过程:



constserver=newKOA();//newApplication()

server.listen(8001);

而先前http.createServer()的那个回调函数则被替换成了app.callback的返回值。



我们细看下app.callback的具体实现:



app.callback=function(){

if(this.experimental){

console.error(''ExperimentalES7AsyncFunctionsupportisdeprecated.PleaselookintoKoav2asthemiddlewaresignaturehaschanged.'')

}

varfn=this.experimental

?compose_es7(this.middleware)

:co.wrap(compose(this.middleware));

varself=this;



if(!this.listeners(''error'').length)this.on(''error'',this.onerror);



returnfunctionhandleRequest(req,res){

res.statusCode=404;

varctx=self.createContext(req,res);

onFinished(res,ctx.onerror);

fn.call(ctx).then(functionhandleResponse(){

respond.call(ctx);

}).catch(ctx.onerror);

}

};

先跳过ES7的实验功能以及错误处理,app.callback中主要做了如下几件事情:



重新组合中间件并用co包装

返回处理request的回调函数

每当服务器接收到请求时,做如下处理:



初始化上下文

调用之前co.wrap返回的函数,并做必要的错误处理

现在我们把目光集中到这三行代码中:



//中间件重组与co包装

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

//------------------------------------------

//在处理request的回调函数中

//创建每次请求的上下文

varctx=self.createContext(req,res);

//调用co包装的函数,执行中间件

fn.call(ctx).then(functionhandleResponse(){

respond.call(ctx);

}).catch(ctx.onerror);

先看第一行代码,compose实际上就是koa-compose,实现如下:



functioncompose(middleware){

returnfunction(next){

if(!next)next=noop();

vari=middleware.length;

while(i--){

next=middleware[i].call(this,next);

}

returnyieldnext;

}

}

functionnoop(){}

compose返回一个generator函数,这个generator函数中倒序依次以next为参数调用每个中间件,并将返回的generator实例重新赋值给next,最终将next返回。



这里比较有趣也比较关键的一点是:



next=middleware[i].call(this,next);

我们知道,调用generator函数返回generator实例,当generator函数中调用其他的generator函数的时候,需要通过yieldgenFunc()显式调用另一个generator函数。



举个例子:



constgenFunc1=function(){

yield1;

yieldgenFunc2();

yield4;

}

constgenFunc2=function(){

yield2;

yield3;

}

for(letdofgenFunc1()){

console.log(d);

}

执行的结果是在控制台依次打印1,2,3,4。



回到上面的compose函数,其实它就是完成上面例子中的genFunc1调用genFunc2的事情。而next的作用就是保存并传递下一个中间件函数返回的generator实例。



参考一下KOA中间件的写法以帮助理解:



function(next){

//dosth.

yieldnext;

//dosth.

}

通过compose函数,KOA把中间件全部级联了起来,形成了一个generator链。下一步就是完成上面例子中的for-of循环的事情了,而这正是co的工作。



co的原理分析

还是先看下co.wrap



co.wrap=function(fn){

createPromise.__generatorFunction__=fn;

returncreatePromise;

functioncreatePromise(){

returnco.call(this,fn.apply(this,arguments));

}

};

该函数返回一个函数createPromise,也就是KOA源码里面的fn。

当调用这个函数的时候,实际上调用的是co,只是将上下文ctx作为this传递了进来。



现在分析下co的代码:



functionco(gen){

varctx=this;

varargs=slice.call(arguments,1)

//返回一个promise

returnnewPromise(function(resolve,reject){

if(typeofgen===''function'')gen=gen.apply(ctx,args);

if(!gen||typeofgen.next!==''function'')returnresolve(gen);



onFulfilled();



functiononFulfilled(res){

varret;

try{

ret=gen.next(res);

}catch(e){

returnreject(e);

}

next(ret);

}



functiononRejected(err){

varret;

try{

ret=gen.throw(err);

}catch(e){

returnreject(e);

}

next(ret);

}



functionnext(ret){

if(ret.done)returnresolve(ret.value);

varvalue=toPromise.call(ctx,ret.value);

if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);

returnonRejected(newTypeError(''Youmayonlyyieldafunction,promise,generator,array,orobject,''

+''butthefollowingobjectwaspassed:"''+String(ret.value)+''"''));

}

});

}

co函数的参数是gen,就是之前compose函数返回的generator实例。



在co返回的Promise中,定义了三个函数onFulfilled、onRejected和next,先看下next的定义。



next的参数实际上就是gen每次gen.next()的返回值。如果gen已经执行结束,那么Promise将返回;否则,将ret.valuepromise化,并再次调用onFulfilled和onRejected函数。



onFulfilled和onRejected帮助我们推进gen的执行。



next和onFulfilled、onRejected的组合,实现了generator的递归调用。那么究竟是如何实现的呢?关键还要看toPromise的实现。



functiontoPromise(obj){

if(!obj)www.baiyuewang.netreturnobj;

if(isPromise(obj))returnobj;

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

if(''function''==typeofobj)returnthunkToPromise.call(this,obj);

if(Array.isArray(obj))returnarrayToPromise.call(this,obj);

if(isObject(obj))returnobjectToPromise.call(this,obj);

returnobj;

}

在toPromise函数中,后三个分支处理分别对thunk函数、数组和对象进行了处理,此处略去细节,只需要知道最终都调回了toPromise的前三个分支处理中。这个函数最终返回一个promise对象,这个对象的resolve和reject处理函数又分别是上一个promise中定义的onFulfilled和onRejected函数。至此,就完成了compose函数返回的generator链的推进工作。



最后还有一个问题需要明确一下,那就是KOA中的context是如何传递的。

通过观察前面的代码不难发现,每次关键节点的函数调用都是使用的xxxFunc.call(ctx)的方式,这也正是为什么我们可以在中间件中直接通过this访问context的原因。

献花(0)
+1
(本文系thedust79首藏)