当前位置: 首页 > 后端技术 > Node.js

深入理解connect-express

时间:2023-04-03 19:14:09 Node.js

其实这个话题之前有提到过,我也写过一篇express和koa的对比,但是现在回过头来看,还是有不少错误,比如express和koa中间件原理的部分。一个圈套,研究了半天没看懂。关于这部分,其实是设计模式的缺失。关于中间件模式,我们不讲那么多概念或实现,而是讲代码。当然柿子是挑剔的,express的代码量不算太大,但是有一个更简单的connect,我们先从connect说起。花了点时间画原理图,之前没怎么画代码流程图,只是为了意思:代码分析先来看看connect是怎么用的:constconnect=require('connect')constapp=connect()app.use('/',function(req,res,next){console.log('全局中间件')next()console.log('执行完成')})app.use('/bar',function(req,res){console.log('secondmiddleware')res.end('end')})app.listen(8001)类似于express。它创建一个新实例并匹配路由。它非常简洁有效。上面的代码执行访问后,我们发现它实际上会在next之后回来执行下面的代码,这好像和koa的中间件有点类似,所谓的洋葱型中间件。结论是否定的,反正这不是和koa比较的。我们梳理一下代码结构:varproto={}varcreateServer=function(){}proto.use=function(){}proto.handle=function(){}proto.listen=function(){}主要是以上功能,我们将砍掉其他辅助功能。可以看到我们主要是在proto中使用connect,让我们根据代码看看我们启动一个connectserver时发生了什么。首先,我们新建一个connect实例:varapp=connect()无疑是调用了createServer,因为这个模块最终export了它,createServer部分的代码也很简单:functioncreateServer(){functionapp(req,res,next){app.handle(req,res,next);}合并(应用程序,原型);//继承原型merge(app,EventEmitter.prototype);//继承自EventEmitterapp.route='/';应用程序堆栈=[];//路由和方法暂存的地方returnapp;}上面有用的部分我都标出来了,可以看出我们常用的connect方法都是来自proto的,所以我们下面的主要工作都会围绕proto展开。app.use在我们要设置某个路由的时候会调用app.use,但是你可能不太清楚它具体做了什么,比如下面的代码:app.use('/bar',function(req,res){res.end('end')})上面说到有一个栈数组专门用来存放路由和它的方法很容易想到:app.use就是把我们要等待执行的路由和方法push.其实也是这样的:proto.use=functionuse(route,fn){varhandle=fn;变种路径=路线;//默认路由到'/'if(typeofroute!=='string'){handle=route;路径='/';}//包装子应用程序if(typeofhandle.handle==='function'){varserver=handle;服务器.route=路径;handle=function(req,res,next){server.handle(req,res,next);};}//包装vanillahttp.Serversif(handleinstanceofhttp.Server){handle=handle.listeners('request')[0];}//去除尾部斜杠if(path[path.length-1]==='/'){path=path.slice(0,-1);}//添加中间件debug('use%s%s',path||'/',handle.name||'anonymous');this.stack.push({route:path,handle:handle});返回这个;};看起来挺复杂的,我们简化一下,不考虑各种异常和兼容性,默认只有app.use(route,handle)可以调用//很好,把if全部去掉,简化2333proto.use=functionuse(route,fn){变量句柄=fn;变种路径=路线;this.stack.push({route:path,handle:handle});返回这个;};,其实就是维护数组。当然这种方式肯定有问题,我们不关心重复路由。中间件的实现之后,其实我们在使用实现之后有几点。中间件现在在栈中,所以我们执行中间件。具体路由要遍历栈,没错,就是遍历栈,但是connect中间的东西是顺序执行的。如果一一排列,所有的中间件都会被重新执行一次。可能的情况,比如说一个异常处理中间件,我只需要在出现异常的时候调用这个中间件即可。这时候,next就派上用场了。首先我们看一下proto.handle实现的几十行代码:proto.handle=functionhandle(req,res,out){varindex=0;varprotohost=getProtohost(req.url)||'';varremoved='';varslashAdded=false;varstack=this.stack;//最终函数处理程序vardone=out||finalhandler(req,res,{env:env,onerror:logerror});//存储原始URLreq.originalUrl=req.originalUrl||要求.url;functionnext(err){if(slashAdded){req.url=req.url.substr(1);斜线添加=假;}if(removed.length!==0){req.url=protohost+removed+req.url.substr(protohost.length);删除='';}//下一个回调varlayer=stack[index++];//全部完成if(!layer){defer(done,err);返回;}//路由数据varpath=parseUrl(req).pathname||'/';varroute=layer.route;//如果路由不匹配则跳过这一层if(path.toLowerCase().substr(0,route.length)!==route.toLowerCase()){returnnext(err);}//如果路由匹配没有边界“/”、“.”或结束,则跳过varc=path.length>route.length&&path[route.length];如果(c&&c!=='/'&&c!=='.'){返回下一个(错误);}//删除匹配路由的url部分if(route.length!==0&&route!=='/'){removed=route;req.url=protohost+req.url.substr(protohost.length+removed.length);//确保前导斜线if(!protohost&&req.url[0]!=='/'){req.url='/'+req.url;斜线添加=真;}}//调用图层句柄call(layer.handle,route,err,req,res,next);}next();};还是挺长的,需要简单化,我们同样把if都给去掉简单化代码:proto.handle=functionhandle(req,res,out){varindex=0;varprotohost=getProtohost(req.url)||'';varremoved='';varslashAdded=false;varstack=this.stack;//最终函数处理程序vardone=out||finalhandler(req,res,{env:env,onerror:logerror});//存储原始URLreq.originalUrl=req.originalUrl||要求.url;functionnext(err){//下一个回调varlayer=stack[index++];//全做完了如果(!layer){defer(done,err);这不行返回;}//路由数据varpath=parseUrl(req).pathname||'/';varroute=layer.route;//如果路由不匹配则跳过这一层if(path.toLowerCase().substr(0,route.length)!==route.toLowerCase()){returnnext(err);}//调用图层句柄call(layer.handle,route,err,req,res,next);}下一个();};简化之后,我们可以看到next其实是一个递归,只要满足条件,它就会不断的调用自己,也就是说,只要你在中间的组件中调用next,它就会遍历查找中间件的栈,找到就执行,找不到就defer(done),注意proto.handle定义了一个索引,是查找中间件的索引,next需要一直用到。不相关的函数这里就不说了,比如getProtohost,比如call.app.listenapp.listen其实很简单,没办法新建http.Server,代码如下:proto.listen=functionlisten(){varserver=http.createServer(this);返回server.listen.apply(server,arguments);};到这里就差不多结束了,其实我们可以知道connect/express的中间件模型是这样的:http.createServer(function(req,res){m1(req,res){m2(req,res){m3(req,res){}}}})接下来调用的时候,我们会继续寻找中间件,调用它.像这样写我自己好像也明白很多(逃