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的原因。
|
|