nodejs模块学习之connect解析
nodejs发展很快,从npm上面的包托管数量就可以看出来。不过从另一方面来看,也是反映了nodejs的基础不稳固,需要开发者创造大量的轮子来解决现实的问题。
知其然,并知其所以然这是程序员的天性。所以把常用的模块拿出来看看,看看高手怎么写的,学习其想法,让自己的技术能更近一步。
引言
express是nodejs中最流行的web框架。express中对http中的request和response的处理,还有以中间件为核心的处理流程,非常灵活,足以应对任何业务的需求。
而connect曾经是express3.x之前的核心,而express4.x已经把connect移除,在express中自己实现了connect的接口。可以说connect造就了express的灵活性。
因此,我很好奇,connect是怎么写的。
争取把每一行代码都弄懂。
connect解析
我们要先从connect的官方例子开始
varconnect=require('connect');
varhttp=require('http');
varapp=connect();
//gzip/deflateoutgoingresponses
varcompression=require('compression');
app.use(compression());
//storesessionstateinbrowsercookie
varcookieSession=require('cookie-session');
app.use(cookieSession({
keys:['secret1','secret2']
}));
//parseurlencodedrequestbodiesintoreq.body
varbodyParser=require('body-parser');
app.use(bodyParser.urlencoded({extended:false}));
//respondtoallrequests
app.use(function(req,res){
res.end('HellofromConnect!\n');
});
//createnode.jshttpserverandlistenonport
http.createServer(app).listen(3000);
从示例中可以看到一个典型的connect的使用:
varapp=connect()//初始化
app.use(function(req,res,next){
//dosomething
})
//http服务器,使用
http.createServer(app).listen(3000);
先倒着看,从调用的地方更能看出来,模块怎么使用的。我们就先从 http.createServer(app) 来看看。
从nodejsdoc的官方文档中可以知, createServer 函数的参数是一个回调函数,这个回调函数是用来响应 request 事件的。从这里看出,示例代码中 app 中函数签就是 (req,res),也就是说 app 的接口为 function(req,res)。
但是从示例代码中,我们也可以看出 app 还有一个 use 方法。是不是觉得很奇怪,js中函数实例上,还以带方法,这在js中就叫函数对象,不仅能调用,还可以带实例变量。给个例子可以看得更清楚:
functionhandle(){
functionapp(req,res,next){app.handle(req,res,next)}
app.handle=function(req,res,next){
console.log(this);
}
app.statck=[];
returnapp;
}
varapp=handle();
app()//==>{[Function:app]handle:[Function],stack:[]}
app.apply({})//==>{[Function:app]handle:[Function],stack:[]}
可以看出:函数中的实例函数中的this就是指当前的实例,不会因为你使用apply进行环境改变。
其他就跟对象没有什么区别。
再次回到示例代码,因该可以看懂了, connect 方法返回了一个函数,这个函数能直接调用,有use方法,用来响应http的request事件。
到此为此,示例代码就讲完了。我们开始进入到connect模块的内部。
connect只有一个导出方法。就是如下:
varmerge=require('utils-merge');
module.exports=createServer;
varproto={};
functioncreateServer(){
//函数对象,这个对象能调用,能加属性
functionapp(req,res,next){app.handle(req,res,next);}
merge(app,proto);//===等于调用Object.assign
merge(app,EventEmitter.prototype);//===等于调用Object.assign
app.route='/';
app.stack=[];
returnapp;
}
从代码中可以看出,createServer函数把app函数返回了,app函数有三个参数,多了一个next(这个后面讲),app函数把proto的方法合并了。还有EventEmitter的方法也合并了,还增加了route和stack的属性。
从前面代码来看,响应request的事件的函数,是app.handle方法。这个方法如下:
proto.handle=functionhandle(req,res,out){
varindex=0;
varprotohost=getProtohost(req.url)||'';//获得http://www.baidu.com
varremoved='';
varslashAdded=false;
varstack=this.stack;
//finalfunctionhandler
vardone=out||finalhandler(req,res,{
env:env,
onerror:logerror
});//接口done(err);
//storetheoriginalURL
req.originalUrl=req.originalUrl||req.url;
functionnext(err){
if(slashAdded){
req.url=req.url.substr(1);//除掉/之后的字符串
slashAdded=false;//已经拿掉
}
if(removed.length!==0){
req.url=protohost+removed+req.url.substr(protohost.length);
removed='';
}
//nextcallback
varlayer=stack[index++];
//alldone
if(!layer){
defer(done,err);//没有中间件,调用finalhandler进行处理,如果err有值,就返回404进行处理
return;
}
//routedata
varpath=parseUrl(req).pathname||'/';
varroute=layer.route;
//skipthislayeriftheroutedoesn'tmatch
if(path.toLowerCase().substr(0,route.length)!==route.toLowerCase()){
returnnext(err);//执行下一个
}
//skipifroutematchdoesnotborder"/",".",orend
varc=path[route.length];
if(c!==undefined&&'/'!==c&&'.'!==c){
returnnext(err);//执行下一个
}
//trimoffthepartoftheurlthatmatchestheroute
if(route.length!==0&&route!=='/'){
removed=route;
req.url=protohost+req.url.substr(protohost.length+removed.length);
//ensureleadingslash
if(!protohost&&req.url[0]!=='/'){
req.url='/'+req.url;
slashAdded=true;
}
}
//callthelayerhandle
call(layer.handle,route,err,req,res,next);
}
next();
};
代码中有相应的注释,可以看出,next方法就是一个递归调用,不断的对比route是否匹配,如果匹配则调用handle,如果不匹配,则调用下一个handle.
call函数的代码如下:
functioncall(handle,route,err,req,res,next){
vararity=handle.length;
varerror=err;
varhasError=Boolean(err);
debug('%s%s:%s',handle.name||'',route,req.originalUrl);
try{
if(hasError&&arity===4){
//error-handlingmiddleware
handle(err,req,res,next);
return;
}elseif(!hasError&&arity<4){
//request-handlingmiddleware
handle(req,res,next);
return;
}
}catch(e){
//replacetheerror
error=e;
}
//continue
next(error);
}
可以看出一个重点:对错误处理,connect的要求是函数必须是四个参数,而express也是如此。如果有错误,中间件没有一个参数的个数是4,就会错误一直传下去,直到后面的 defer(done,err); 进行处理。
还有app.use添加中间件:
proto.use=functionuse(route,fn){
varhandle=fn;//fn只是一个函数的话三种接口//1.err,req,res,next2.req,res,3,req,res,next
varpath=route;
//defaultrouteto'/'
if(typeofroute!=='string'){
handle=route;
path='/';
}
//wrapsub-apps
if(typeofhandle.handle==='function'){//自定义中的函数对象
varserver=handle;
server.route=path;
handle=function(req,res,next){//req,res,next中间件
server.handle(req,res,next);
};
}
//wrapvanillahttp.Servers
if(handleinstanceofhttp.Server){
handle=handle.listeners('request')[0];//(req,res)//最后的函数
}
//striptrailingslash
if(path[path.length-1]==='/'){
path=path.slice(0,-1);
}
//addthemiddleware
debug('use%s%s',path||'/',handle.name||'anonymous');
this.stack.push({route:path,handle:handle});
returnthis;
};
从代码中,可以看出,use方法添加中间件到this.stack中,其中fn中间件的形式有两种:function(req,res,next)和handle.handle(req,res,next)这两种都可以。还有对fn情况进行特殊处理。
总的处理流程就是这样,用use方法添加中间件,用next编历中间件,用finalHandle进行最后的处理工作。
在代码中还有一个函数非常奇怪:
/*istanbulignorenext*/
vardefer=typeofsetImmediate==='function'
?setImmediate
:function(fn){process.nextTick(fn.bind.apply(fn,arguments))}
defer 函数中的 fn.bind.apply(fn,arguments),这个方法主要解决了,一个问题,不定参的情况下,第一个参数函数,怎样拿到的问题,为什么这样说呢?如果中我们要达到以上的效果,需要多多少行代码?
function(){
varcb=Array.from(arguments)[0];
varargs=Array.from(arguments).splice(1);
process.nextTick(function(){
cb.apply(null,args);
})
}
这还是connect兼容以前的es5之类的方法。如果在es6下面,方法可以再次简化
function(..args){process.nextTick(fn.bind(...args))}
总结
connect做为http中间件模块,很好地解决对http请求的插件化处理的需求,把中间件组织成请求上的一个处理器,挨个调用中间件对http请求进行处理。
其中connect的递归调用,和对js的函数对象的使用,让值得学习,如果让我来写,就第一个调个的地方,就想不到使用函数对象来进行处理。
而且next的设计如此精妙,整个框架的使用和概念上,对程序员基本上没有认知负担,这才是最重要的地方。这也是为什么express框架最受欢迎。koa相比之下,多几个概念,还使用了不常用的yield方法。
connect的设计理念可以用在,类似http请求模式上,如rpc,tcp处理等。
我把connect的设计方法叫做中间件模式,对处理流式模式,会有较好的效果。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。