随着Node.jsv8的发布,Node.js已经原生支持async/await功能,web框架Koa也发布了Koa2正式版,支持async/await中间件.处理异步回调带来了极大的便利。既然Koa2已经支持async/await中间件,为什么不直接使用Koa,而是修改Express来支持async/await中间件呢?因为Koa2正式版发布不久,很多老项目还在用Express,推倒重写Koa是不可能的,太贵了,但是如果想用新带来的便利syntax,只能对Express进行改造,而且这个改造必须是对业务无侵入的,否则会带来很多麻烦。直接使用async/await让我们先看看在Express中直接使用async/await函数。constexpress=require('快递');constapp=express();const{promisify}=require('util');const{readFile}=require('fs');constreadFileAsync=promisify(readFile);app.get('/',asyncfunction(req,res,next){constdata=awaitreadFileAsync('./package.json');res.send(data.toString());});//错误Handlerapp.use(function(err,req,res,next){console.error('Error:',err);res.status(500).send('ServiceError');});app.listen(3000,'127.0.0.1',function(){console.log(`服务器运行在http://${this.address().address}:${this.address().port}/`);});上面没有修改Express,直接使用async/await函数处理请求。在请求http://127.0.0.1:3000/时,发现请求可以正常请求,response也可以正常响应。貌似async/await函数可以不对Express做任何修改直接使用,但是如果async/await函数出错了,是不是可以通过我们的错误处理中间件来处理呢?现在我们读取一个不存在的文件,比如将之前读取的package.json替换为age.json。app.get('/',asyncfunction(req,res,next){constdata=awaitreadFileAsync('./age.json');res.send(data.toString());});现在我们去请求http://127.0.0.1:3000/的时候,发现请求长时间无法响应,最终会超时。但是在终端报如下错误:错误处理中间件没有处理错误,而是抛出了unhandledRejection异常。现在,如果我们使用try/catch手动捕获错误会发生什么?app.get('/',asyncfunction(req,res,next){try{constdata=awaitreadFileAsync('./age.json');res.send(datas.toString());}catch(e){下一个(e);}});发现请求是通过错误处理中间件处理的,也就是说我们可以手动显式捕获错误,但是如果我们在每个中间件中都加一个try/或者请求处理函数catch也太不优雅了,有点侵入性业务代码,代码看起来很难看。因此,通过直接使用async/await函数的实验,我们发现express改造的方向是能够接收到async/await函数抛出的错误,并且对业务代码没有侵入性。TransformExpress有两种方法可以处理Express中的路由和中间件。一种是通过Express创建的app直接在app上添加中间件和处理路由,如下:constexpress=require('express');constapp=express();app.use(function(req,res,next){next();});app.get('/',function(req,res,next){res.send('hello,world');});app.post('/',function(req,res,next){res.send('hello,world');});app.listen(3000,'127.0.0.1',function(){console.log(`服务器运行在http://${this.address().address}:${this.address().port}/`);});另一种是通过Express的Router创建的路由实例,直接在路由实例上添加中间件和处理路由,如下:constexpress=require('express');constapp=express();constrouter=newexpress.Router();app.use(router);router.get('/',function(req,res,next){res.send('hello,world');});router.post('/',function(req,res,next){res.发送('你好,世界');});app.listen(3000,'127.0.0.1',function(){console.log(`服务器运行在http://${this.address().address}:${this.address().port}/`);});这两种方式可以混合使用,现在我们来思考一下app.get('/',asyncfunction(req,res,next){})这样的函数如何统一处理异步函数抛出的错误呢?要让错误得到统一处理,当然要调用next(err)让错误传递给错误处理中间件,而async函数由于返回的是Promise,所以肯定是这样asyncFn().then()。catch(function(err){next(err)}),所以如果你这样修改,你将得到如下代码:app.get=function(...data){constparams=[];for(letitemofdata){if(Object.prototype.toString.call(item)!=='[objectAsyncFunction]'){params.push(item);继续;}consthandle=function(...data){const[req,res,next]=data;item(req,res,next).then(next).catch(next);};参数.push(句柄);}app.get(...params)}在上面的代码中,我们判断如果app.get()函数的参数中有async函数,我们就使用item(req,res,next).then(下一个).catch(下一个);去处理它,这样我们就可以在函数中捕获throw错误发生并传递给错误处理中间件,但是这段代码有一个明显的错误,就是最后调用了app.get(),递归,破坏了app.get的功能,根本无法处理请求。因此仍需继续改革。我们之前说过,Express处理路由和中间件的两种方式可以混合使用,所以我们将这两种方式混合使用,避免递归。代码如下:constexpress=require('express');constapp=express();constrouter=newexpress.Router();app.use(router);app.get=function(...data){常量参数=[];for(letitemofdata){if(Object.prototype.toString.call(item)!=='[objectAsyncFunction]'){参数。推(项目);继续;}consthandle=function(...data){const[req,res,next]=data;item(req,res,next).then(next).catch(next);};参数.push(句柄);}router.get(...params)}像上面这样修改后一切似乎都正常,可以正常处理请求。但是通过查看Express的源码,发现这个方法破坏了app.get()方法,因为app.get()不仅可以用来处理路由,还可以用来获取配置应用程序。Express中对应的源码如下:methods.forEach(function(method){app[method]=function(path){if(method==='get'&&arguments.length===1){//app.get(setting)returnthis.set(path);}this.lazyrouter();varroute=this._router.route(path);route[方法].apply(route,slice.call(arguments,1));返回这个;};});所以我们在转换的时候也需要对app.get做特殊处理。在实际应用中,我们不仅有get请求,还有post、put、delete请求,所以我们最终修改的代码如下:const{promisify}=require('util');const{readFile}=require('fs');constreadFileAsync=promisify(readFile);constexpress=require('快递');constapp=express();constrouter=newexpress.Router();constmethods=['get','post','put','delete'];app.use(router);for(letmethodofmethods){app[method]=function(...data){if(method==='get'&&data.length===1)returnapp.set(data[0]);}常量参数=[];for(letitemofdata){if(Object.prototype.toString.call(item)!=='[objectAsyncFunction]'){params.push(item);继续;}consthandle=function(...data){const[req,res,next]=data;item(req,res,next).then(next).catch(next);};参数.push(句柄);}路由器[方法](...参数);};}app.get('/',asyncfunction(req,res,next){constdata=awaitreadFileAsync('./package.json');res.send(data.toString());});app.post('/',asyncfunction(req,res,next){constdata=awaitreadFileAsync('./age.json');res.send(data.toString());});router.use(function(err,req,res,next){console.error('Error:',err);res.status(500).send('ServiceError');});app.listen(3000,'127.0.0.1',function(){console.log(`服务器运行在http://${this.address().address}:${this.address().port}/`);});至此修改完成,我们只需要添加一小段代码,就可以直接使用async函数作为handler来处理请求,对业务没有侵入。抛出错误也可以传递给错误处理中间件
