如何在Express4.x中愉快地使用async的方法
前言
为了能够更好地处理异步流程,一般开发者会选择async语法。在express框架中可以直接利用async来声明中间件方法,但是对于该中间件的错误,无法通过错误捕获中间件来劫持到。
错误处理中间件
constexpress=require('express'); constapp=express(); constPORT=process.env.PORT||3000; app.get('/',(req,res)=>{ constmessage=doSomething(); res.send(message); }); //错误处理中间件 app.use(function(err,req,res,next){ returnres.status(500).send('内部错误!'); }); app.listen(PORT,()=>console.log(`applisteningonport${PORT}`));
以上述代码为例,中间件方法并没有通过async语法来声明,如果doSomething方法内部抛出异常,那么就可以在错误处理中间件中捕获到错误,从而进行相应地异常处理。
app.get('/',async(req,res)=>{ constmessage=doSomething(); res.send(message); });
而采用async语法来声明中间件时,一旦doSomething内部抛出异常,则错误处理中间件无法捕获到。
虽然可以利用process监听unhandledRejection事件来捕获,但是无法正确地处理后续流程。
try/catch
对于async声明的函数,可以通过try/catch来捕获其内部的错误,再使用next函数将错误递交给错误处理中间件,即可处理该场景:
app.get('/',async(req,res,next)=>{ try{ constmessage=doSomething(); res.send(message); }catch(err){ next(err); } });
「这种写法简单易懂,但是满屏的try/catch语法,会显得非常繁琐且不优雅。」
高阶函数
对于基础扎实的开发来说,都知道async函数最终返回一个Promise对象,而对于Promsie对象应该利用其提供的catch方法来捕获异常。
那么在将async语法声明的中间件方法传入use之前,需要包裹一层Promise函数的异常处理逻辑,这时就需要利用高阶函数来完成这样的操作。
functionasyncUtil(fn){ returnfunctionasyncUtilWrap(...args){ constfnReturn=fn(args); constnext=args[args.length-1]; returnPromise.resolve(fnReturn).catch(next); } } app.use(asyncUtil(async(req,res,next)=>{ constmessage=doSomething(); res.send(message); }));
相比较第一种方法,「高阶函数减少了冗余代码,在一定程度上提高了代码的可读性。」
上述两种方案基于扎实的JavaScript基础以及Express框架的熟练使用,接下来从源码的角度思考合适的解决方案。
中间件机制
Express中主要包含三种中间件:
- 应用级别中间件
- 路由级别中间件
- 错误处理中间件
app.use=functionuse(fn){ varpath='/'; //省略参数处理逻辑 ... //初始化内置中间件 this.lazyrouter(); varrouter=this._router; fns.forEach(function(fn){ //non-expressapp if(!fn||!fn.handle||!fn.set){ returnrouter.use(path,fn); } ... },this); returnthis; };
应用级别中间件通过app.use方法注册,「其本质上也是调用路由对象上的中间件注册方法,只不过其默认路由为'/'」。
proto.use=functionuse(fn){ varoffset=0; varpath='/'; //省略参数处理逻辑 ... varcallbacks=flatten(slice.call(arguments,offset)); for(vari=0;i') varlayer=newLayer(path,{ sensitive:this.caseSensitive, strict:false, end:false },fn); layer.route=undefined; this.stack.push(layer); } returnthis; };
中间件的所有注册方式最终会调用上述代码,根据path和中间件处理函数生成layer实例,再通过栈来维护这些layer实例。
//部分核心代码 proto.handle=functionhandle(req,res,out){ varself=this; varidx=0; varstack=self.stack; next(); functionnext(err){ varlayerError=err==='route' ?null :err; if(idx>=stack.length){ return; } varpath=getPathname(req); //findnextmatchinglayer varlayer; varmatch; varroute; while(match!==true&&idxExpress内部通过handle方法来处理中间件执行逻辑,其利用「闭包的特性」缓存idx来记录当前遍历的状态。
该方法内部又实现了next方法来匹配当前需要执行的中间件,从遍历的代码可以明白「中间件注册的顺序是非常重要的」。
如果该流程存在异常,则调用layer实例的handle.error方法,这里仍然是「遵循了Node.js错误优先的设计理念」:
Layer.prototype.handle_error=functionhandle_error(error,req,res,next){ varfn=this.handle; if(fn.length!==4){ //notastandarderrorhandler returnnext(error); } try{ fn(error,req,res,next); }catch(err){ next(err); } };「内部通过判断函数的形参个数过滤掉非错误处理中间件」。
如果next函数内部没有异常情况,则调用layer实例的handle_request方法:Layer.prototype.handle_request=functionhandle(req,res,next){ varfn=this.handle; if(fn.length>3){ //notastandardrequesthandler returnnext(); } try{ fn(req,res,next); }catch(err){ next(err); } };「handle方法初始化执行了一次next方法,但是该方法每次调用最多只能匹配一个中间件」,所以在执行handle_error和handle_request方法时,会将next方法透传给中间件,这样开发者就可以通过手动调用next方法的方式来执行接下来的中间件。
从上述中间件的执行流程中可以知晓,「用户注册的中间件方法在执行的时候都会包裹一层try/catch,但是try/catch无法捕获async函数内部的异常,这也就是为什么Express中无法通过注册错误处理中间件来拦截到async语法声明的中间件的异常的原因」。
修改源码
找到本质原因之后,可以通过修改源码的方法来进行适配:
Layer.prototype.handle_request=functionhandle(req,res,next){ varfn=this.handle; if(fn.length>3){ //notastandardrequesthandler returnnext(); } //针对async语法函数特殊处理 if(Object.prototype.toString.call(fn)==='[objectAsyncFunction]'){ returnfn(req,res,next).catch(next); } try{ fn(req,res,next); }catch(err){ next(err); } };上述代码在handle_request方法内部判断了中间件方法通过async语法声明的情况,从而采用Promise对象的catch方法来向下传递异常。
「这种方式可以减少上层冗余的代码,但是实现该方式,可能需要fork一份Express4.x的源码,然后发布一个修改之后的版本,后续还要跟进官方版本的新特性,相应的维护成本非常高。」
express5.x中将router部分剥离出了单独的路由库--router
AOP(面向切面编程)
为了解决上述方案存在的问题,我们可以尝试利用AOP技术在不修改源码的基础上对已有方法进行增强。
app.use(asyncfunction(){ constmessage=doSomething(); res.send(message); })以注册应用级别中间件为例,可以对app.use方法进行AOP增强:
constoriginAppUseMethod=app.use.bind(app); app.use=function(fn){ if(Object.prototype.toString.call(fn)==='[objectAsyncFunction]'){ constasyncWrapper=function(req,res,next){ fn(req,res,next).then(next).catch(next); } returnoriginAppUseMethod(asyncWrapper); } returnoriginAppUseMethod(fn); }前面源码分析的过程中,app.use内部是有this调用的,所以这里需要「利用bind方法来避免后续调用过程中this指向出现问题。」
然后就是利用AOP的核心思想,重写原始的app.use方法,通过不同的分支逻辑代理到原始的app.use方法上。
「该方法相比较修改源码的方式,维护成本低。但是缺点也很明显,需要重写所有可以注册中间件的方法,不能够像修改源码那样一步到位。」
写在最后
本文介绍了Express中使用async语法的四种解决方案:
- try/catch
- 高阶函数
- 修改源码
- AOP
除了try/catch方法性价比比较低,其它三种方法都需要根据实际情况去取舍,举个栗子:
如果你需要写一个Express中间件提供给各个团队使用,那么修改源码的方式肯定走不通,而AOP的方式对于你的风险太大,相比较下,第二种方案是最佳的实践方案。
到此这篇关于如何在Express4.x中愉快地使用async的方法的文章就介绍到这了,更多相关Express4.x使用async内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。