前言上一篇介绍了express中间件的使用以及如何自己实现中间件。还介绍了express中间件的实现依赖于Connect中间件框架。在3.0版本之前,express框架的中间件完全由另一个框架Connect实现。虽然现在已经去掉了对Connect的依赖,但是Connect也是在express中实现的。可以说中间件让express变得更加灵活,尤其是next的设计真是别出心裁。下面通过源码简单分析一下内部实现。首先安装connectnpminstallconnect然后看一个官方示例代码varconnect=require('connect');varhttp=require('http');varapp=connect();//gzip/deflateoutgoingresponsesvarcompression=require('compression');app.use(compression());//在浏览器中存储会话状态cookievarcookieSession=require('cookie-session');app.use(cookieSession({keys:['secret1','secret2']}));//将urlencoded请求主体解析为req.bodyvarbodyParser=require('body-parser');app.use(bodyParser.urlencoded({extended:false}));//响应对所有请求app.use(function(req,res){res.end('HellofromConnect!\n');});//创建node.jshttp服务器并监听端口http.createServer(app).listen(3000);我们先看app变量的定义,然后执行connect方法,把方法的返回值给app。先不急着看返回的是什么,我们先分析一下。最后一行代码调用了http.createServer(app)。熟悉nodejs的应该都知道createServer接收一个函数,这个函数是用来响应request事件的,相当于一个function(request,response)。所以我们猜测connect()应该返回一个至少有两个参数的函数,request和response。这时候再看源码,首先看到源码中只导出了一个createServer。然后找到createServer方法的定义。现在我们知道,在调用varapp=connect()时,实际上是调用了源码中的createServer()方法。方法内部创建了一个函数对象,以及一些属性,最后返回这个函数对象。而app.handle方法是在app函数中执行的,也就是说app.handle方法是响应http.createServer()中的request事件。总结一下上面的代码,首先将app对象作为参数传递给http.createServer(app).listen(3000),然后返回服务实例并监听某个端口。此时执行所有请求(请求响应事件)的回调函数,app.handle(req,res,next)。这证明我们上面的猜测是正确的。说到这里,你可能会问app.handle是从哪里来的呢?别担心,继续阅读。第52行将proto对象上的属性和方法复制到应用程序中。第53行的app对象也继承了EventEmitter的原型。第54行route表示请求路径。55行的栈是一个数组,用来存放所有的中间件。(==注意==:merge是引入的utils-merge包的方法,作用是将源对象的属性合并到目标对象中。)merge(app,proto);这句话是将proto方法上的属性合并到app对象中。原型对象是什么?现在回答上面的问题,app.handle从哪里来,这个proto对象是什么?源码不长,粗略浏览一下就知道proto对象上有3个方法,分别是use、handle和call。这三个方法也是Connect框架实现的核心。让我们一一分析。首先找到使用方法。使用中间件时,是通过app.use()函数==proto.use==第81行——参数判断。如果实际只传入一个函数,则保存这个函数并将路由设置为“/”。87~98行——处理fn的几种情况:typeofhandle.handle==='function'等价于fn===connect();意思是当传入的fn参数也是一个中间件时,那么存储的句柄就是这个子中间件的fn.handle()方法。当传入参数fn为http.Server类的实例时,将handle值设置为请求事件的第一个监听器。第101行——如果参数route参数以“/”结尾,去掉“/”。第107行——可以看到,将按照特定格式构造的匿名对象放入栈数组中。stack是中间件容器的数组。即接下来取出中间件容器。总结:proto.use方法用于添加中间件,也可以理解为注册中间件,将构造好的对象以特定格式放入数组中。==proto.handle==handle方法是通过当前请求路径在stack数组中找到匹配的中间件,并调用call方法。大部分都是字符串匹配,一些详细的操作可以结合注释看懂。第135行-创建了下一个函数。第147行-从堆栈和索引++中取出当前项目。第150行——判断是否有中间件需要处理,如果没有则调用defer()。第160~180行——都是字符串操作。对于路由匹配,如果不满足,则执行next()跳转到下一个。注意调用next时必须传入err。183行——满足条件时执行call函数,将栈数组当前项的信息传入。总体思路是配合路由器。如果找到匹配,则通过call调用句柄,如果没有匹配,则继续执行下一条。这实际上是一个递归调用。总结:handle是整个源码中最长最核心的函数。主要功能是定义next函数,通过遍历获取当前item的匿名对象的请求地址和中间件函数。然后使用当前请求地址去匹配匿名对象的地址,匹配失败则返回next(err);继续处理栈中下一个中间件的信息。这个循环一直持续到栈中没有值或者地址匹配成功,然后调用(layer.handle,route,err,req,res,next);叫做。layer.handle是use方法中构造的匿名对象中的中间件函数。由此我们也可以猜测,中间件函数应该是在调用内部执行的。==call==上面我们猜测调用应该是执行匹配的中间件函数。call的实现比较清晰,基本可以验证我们的猜测是否正确。call的主要任务是执行handle方法中匹配的中间件函数。handle(err,req,res,next)函数会在错误发生且传递4个参数时调用,handle(req,res,next)函数在没有错误发生且传递的参数小于4、这里的句柄函数就是中间件函数。在中间件逻辑代码中,最后会调用next函数继续匹配下一个中间件,然后执行。248行——当出现错误,传入的参数小于4时,是不是进行两次判断,也就是说中间件函数不能执行,但是next(error);被直接调用。匹配下一个中间件并将错误传递给它。如果一直不能满足条件,那么就会去defer(done,err);在句柄中统一处理错误。总结Connect是一个非常简短但设计精良的框架,代码简洁易读。以上就是对Connect源码的简单分析。最重要的是proto上的三个方法。如果你通过本文对这三个方法有了一定的了解,那么你就掌握了Connect框架实现的核心。
