说到Node.js开发,就不得不提到Express和Koa这两个最火的框架。Express是一个灵活的Node.jsWeb应用程序开发框架,保持最小化,为Web和移动应用程序提供一组强大的功能。目前有很多用户。Koa是一个新的Web框架,由Express背后的同一个人构建,旨在成为Web应用程序和API开发领域中更小、更具表现力和更健壮的基石。通过利用异步函数,Koa可以帮助您放弃回调并大大增强错误处理。Koa没有捆绑任何中间件,而是提供了一套优雅的方法来帮助您快速愉快地编写服务器端应用程序。相信对这两个框架有一定了解的人,都会或多或少的了解它们的中间件机制。Express是线性模型,而Koa是洋葱模型。本系列博客主要讲解Express和Koa的中间件机制,本文将主要讲解Express的中间件机制。Express中间件connect在express3.x之前曾经是核心,但是express4.x去掉了connect,在express本身实现了connect接口,所以我们本文的源码讲解直接使用connect源码。示例下面将使用Express实现一个简单的demo来解释中间件机制varexpress=require('express');varapp=express();app.use(function(req,res,next){console.log('第一个中间件开始');setTimeout(()=>{next();},1000)console.log('第一个中间件结束');});app.use(function(req,res,next){console.log('第二个中间件开始');setTimeout(()=>{next();},1000)console.log('第二个中间件结束');});app.use('/foo',function(req,res,next){console.log('接口逻辑开始');next();console.log('接口逻辑结束');});app.listen(4000);此时的输出更符合我们对Expresslinearity的理解。输出的是第一个中间件开始,第一个中间件结束,第二个中间件开始,第二个中间件结束界面,逻辑开始界面逻辑结束但是,如果我们取消中间件内部的异步处理,直接调用next()varexpress=require('express');varapp=express();app.use(function(req,res,next){console.log('第一个中间件启动');next()console.log('第一个中间件启动end');});app.use(function(req,res,next){console.log('第二个中间件开始');next()console.log('第二个中间件结束');});app.use('/foo',function(req,res,next){console.log('接口逻辑开始');下一个();console.log('接口逻辑结束');});app.listen(4000);输出的是firstmiddlewarestartandsecondmiddlewarestartinterfacelogicstartinterfacelogicendsecondmiddlewareendfirstmiddlewareend这个结果是不是很像koa的输出?是的,但它仍然不同于洋葱剥皮模型。其实这种结果的输出是代码同步运行造成的。这并不意味着Express不是线性模型。当我们的中间件中没有异步操作时,我们的代码最终以如下方式运行app.use(functionmiddleware1(req,res,next){console.log('第一个中间件开始')//next()(function(req,res,next){console.log('第二个中间件开始')//next()(function(req,res,next){console.log('接口逻辑开始')//next()(functionhandler(req,res,next){//dosomething})()console.log('interfacelogicend')})()console.log('secondmiddlewareend')})()console.log('第一个中间件端')})这其实就是connect的实现,接下来我们分析connect的源码源码分析connect源码只有200多行,这里只选取了一部分核心代码,不是完整代码。查看所有代码,请移步github。中间件的挂载主要依赖proto.use和proto.handle。这里我们删除了一些if判断,这样我们就可以更专注于其内部原理的实现proto.use=functionuse(route,fn){varhandle=fn;变种路径=路线;//这里是直接填回调函数的容错处理//默认路由到'/'if(typeofroute!=='string'){handle=路由;路径='/';}。..this.stack.push({route:path,handle:handle});返回这个;};proto.use主要是把我们需要挂载的中间件存放在自己的stack属性中,同时进行部分兼容处理。比较容易理解,中间件机制的核心是proto.handle内部next方法的实现。proto.handle=functionhandle(req,res,out){varindex=0;varstack=this.stack;functionnext(err){//下一个回调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);}//调用图层句柄call(layer.handle,route,err,req,res,next);}下一个();};删除部分非核心代码后,可以清楚的看出proto.handle的核心是next方法的实现和递归调用。中间件被获取并执行。这里我们可以解释一下上面异步和非异步进程输出结果的区别。当有异步代码时,会直接跳过继续执行。这个时候next方法还没有执行完,需要等待当前队列中的所有事件都执行完,所以这个时候我们输出的数据是线性的。直接执行next方法的时候,本质上所有的代码都已经同步了,所以一层层嵌套,最外层肯定在最后,输出类似洋葱皮模型的结果。总结一下connect实现的基本原理就是维护一个栈数组,将所有需要挂载的中间件处理完后压入数组,然后执行数组中的next方法,直到所有中间件挂载完毕。当然在这个过程中会做一些异常、兼容性等处理。
