详解如何让Express支持async/await


Posted in Javascript onOctober 09, 2017

随着 Node.js v8 的发布,Node.js 已原生支持 async/await 函数,Web 框架 Koa 也随之发布了 Koa 2 正式版,支持 async/await 中间件,为处理异步回调带来了极大的方便。

既然 Koa 2 已经支持 async/await 中间件了,为什么不直接用 Koa,而还要去改造 Express 让其支持 async/await 中间件呢?因为 Koa 2 正式版发布才不久,而很多老项目用的都还是 Express,不可能将其推倒用 Koa 重写,这样成本太高,但又想用到新语法带来的便利,那就只能对 Express 进行改造了,而且这种改造必须是对业务无侵入的,不然会带来很多的麻烦。

直接使用 async/await

让我们先来看下在 Express 中直接使用 async/await 函数的情况。

const express = require('express');
const app = express();
const { promisify } = require('util');
const { readFile } = require('fs');
const readFileAsync = promisify(readFile);
  
app.get('/', async function (req, res, next){
 const data = await readFileAsync('./package.json');
 res.send(data.toString());
});
// Error Handler
app.use(function (err, req, res, next){
 console.error('Error:', err);
 res.status(500).send('Service Error');
});
  
app.listen(3000, '127.0.0.1', function (){
 console.log(`Server running at http://${this.address().address }:${this.address().port }/`);
});

上面是没有对 Express 进行改造,直接使用 async/await 函数来处理请求,当请求 http://127.0.0.1:3000/ 时,发现请求能正常请求,响应也能正常响应。这样似乎不对 Express 做任何改造也能直接使用 async/await 函数,但如果 async/await 函数里发生了错误能不能被我们的错误处理中间件处理呢?现在我们去读取一个不存在文件,例如将之前读取的 package.json 换成 age.json 。

app.get('/', async function (req, res, next){
 const data = await readFileAsync('./age.json');
 res.send(data.toString());
});

现在我们去请求 http://127.0.0.1:3000/ 时,发现请求迟迟不能响应,最终会超时。而在终端报了如下的错误:

详解如何让Express支持async/await

发现错误并没有被错误处理中间件处理,而是抛出了一个 unhandledRejection 异常,现在如果我们用 try/catch 来手动捕获错误会是什么情况呢?

app.get('/', async function (req, res, next){
 try {
  const data = await readFileAsync('./age.json');
  res.send(datas.toString());
 } catch(e) {
  next(e);
 }
});

发现请求被错误处理中间件处理了,说明我们手动显式的来捕获错误是可以的,但是如果在每个中间件或请求处理函数里面加一个 try/catch 也太不优雅了,对业务代码有一定的侵入性,代码也显得难看。所以通过直接使用 async/await 函数的实验,我们发现对 Express 改造的方向就是能够接收 async/await 函数里面抛出的错误,又对业务代码没有侵入性。

改造 Express

在 Express 中有两种方式来处理路由和中间件,一种是通过 Express 创建的 app,直接在 app 上添加中间件和处理路由,像下面这样:

const express = require('express');
const app = 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(`Server running at http://${this.address().address }:${this.address().port }/`);
});

另外一种是通过 Express 的 Router 创建的路由实例,直接在路由实例上添加中间件和处理路由,像下面这样:

const express = require('express');
const app = express();
const router = new express.Router();
app.use(router);
  
router.get('/', function (req, res, next){
 res.send('hello, world');
});
router.post('/', function (req, res, next){
 res.send('hello, world');
});
  
app.listen(3000, '127.0.0.1', function (){
 console.log(`Server running at http://${this.address().address }:${this.address().port }/`);
});

这两种方法可以混合起来用,现在我们思考一下怎样才能让一个形如 app.get('/', async function(req, res, next){}) 的函数,让里面的 async 函数抛出的错误能被统一处理呢?要让错误被统一的处理当然要调用 next(err) 来让错误被传递到错误处理中间件,又由于 async 函数返回的是 Promise,所以肯定是形如这样的 asyncFn().then().catch(function(err){ next(err) }) ,所以按这样改造一下就有如下的代码:

app.get = function (...data){
 const params = [];
 for (let item of data) {
  if (Object.prototype.toString.call(item) !== '[object AsyncFunction]') {
   params.push(item);
   continue;
  }
  const handle = function (...data){
   const [ req, res, next ] = data;
   item(req, res, next).then(next).catch(next);
  };
  params.push(handle);
 }
 app.get(...params)
}

上面的这段代码中,我们判断 app.get() 这个函数的参数中,若有 async 函数,就采用 item(req, res, next).then(next).catch(next); 来处理,这样就能捕获函数内抛出的错误,并传到错误处理中间件里面去。但是这段代码有一个明显的错误就是最后调用 app.get(),这样就递归了,破坏了 app.get 的功能,也根本处理不了请求,因此还需要继续改造。

我们之前说 Express 两种处理路由和中间件的方式可以混用,那么我们就混用这两种方式来避免递归,代码如下:

const express = require('express');
const app = express();
const router = new express.Router();
app.use(router);
  
app.get = function (...data){
 const params = [];
 for (let item of data) {
  if (Object.prototype.toString.call(item) !== '[object AsyncFunction]') {
   params.push(item);
   continue;
  }
  const handle = function (...data){
   const [ req, res, next ] = data;
   item(req, res, next).then(next).catch(next);
  };
  params.push(handle);
 }
 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)
   return this.set(path);
  }
  
  this.lazyrouter();
  
  var route = this._router.route(path);
  route[method].apply(route, slice.call(arguments, 1));
  return this;
 };
});

所以在改造时,我们也需要对 app.get 做特殊处理。在实际的应用中我们不仅有 get 请求,还有 post、put 和 delete 等请求,所以我们最终改造的代码如下:

const { promisify } = require('util');
const { readFile } = require('fs');
const readFileAsync = promisify(readFile);
const express = require('express');
const app = express();
const router = new express.Router();
const methods = [ 'get', 'post', 'put', 'delete' ];
app.use(router);
  
for (let method of methods) {
 app[method] = function (...data){
  if (method === 'get' && data.length === 1) return app.set(data[0]);

  const params = [];
  for (let item of data) {
   if (Object.prototype.toString.call(item) !== '[object AsyncFunction]') {
    params.push(item);
    continue;
   }
   const handle = function (...data){
    const [ req, res, next ] = data;
    item(req, res, next).then(next).catch(next);
   };
   params.push(handle);
  }
  router[method](...params);
 };
}
   
app.get('/', async function (req, res, next){
 const data = await readFileAsync('./package.json');
 res.send(data.toString());
});
   
app.post('/', async function (req, res, next){
 const data = await readFileAsync('./age.json');
 res.send(data.toString());
});
  
router.use(function (err, req, res, next){
 console.error('Error:', err);
 res.status(500).send('Service Error');
}); 
   
app.listen(3000, '127.0.0.1', function (){
 console.log(`Server running at http://${this.address().address }:${this.address().port }/`);
});

现在就改造完了,我们只需要加一小段代码,就可以直接用 async function 作为 handler 处理请求,对业务也毫无侵入性,抛出的错误也能传递到错误处理中间件。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery 检测元素是否存在的实例代码
Nov 19 Javascript
javascript实现在某个元素上阻止鼠标右键事件的方法和实例
Aug 12 Javascript
jquery实现textarea输入框限制字数的方法
Jan 15 Javascript
Javascript 拖拽雏形中的一些问题(逐行分析代码,让你轻松了拖拽的原理)
Jan 23 Javascript
在JavaScript应用中实现延迟加载的方法
Jun 25 Javascript
倾力总结40条常见的移动端Web页面问题解决方案
May 24 Javascript
Javascript中return的使用与闭包详解
Jan 11 Javascript
jQuery插件HighCharts绘制2D柱状图、折线图的组合双轴图效果示例【附demo源码下载】
Mar 09 Javascript
vue通过点击事件读取音频文件的方法
May 30 Javascript
vue2中,根据list的id进入对应的详情页并修改title方法
Aug 24 Javascript
浅谈Javascript常用正则表达式应用
Mar 08 Javascript
react用Redux中央仓库实现一个todolist
Sep 29 Javascript
jQuery插件artDialog.js使用与关闭方法示例
Oct 09 #jQuery
Node.JS使用Sequelize操作MySQL的示例代码
Oct 09 #Javascript
基于zepto.js实现登录界面
Oct 09 #Javascript
浅谈在koa2中实现页面渲染的全局数据
Oct 09 #Javascript
jQuery实现html双向绑定功能示例
Oct 09 #jQuery
jqueryUI tab标签页代码分享
Oct 09 #jQuery
ES6学习教程之模板字符串详解
Oct 09 #Javascript
You might like
全国FM电台频率大全 - 8 黑龙江省
2020/03/11 无线电
如何获得PHP相关资料
2006/10/09 PHP
PHP Zip解压 文件在线解压缩的函数代码
2010/05/26 PHP
phpize的深入理解
2013/06/03 PHP
采用thinkphp自带方法生成静态html文件详解
2014/06/13 PHP
客户端静态页面玩分页
2006/06/26 Javascript
比较简单的一个符合web标准的JS调用flash方法
2007/11/29 Javascript
jquery实现心算练习代码
2010/12/06 Javascript
js中各浏览器中鼠标按键值的差异
2011/04/07 Javascript
javascript仿qq界面的折叠菜单实现代码
2012/12/12 Javascript
简单的Jquery遮罩层代码实例
2013/11/14 Javascript
通过JS动态创建一个html DOM元素并显示
2014/10/15 Javascript
jQuery中ajax的load()方法用法实例
2014/12/26 Javascript
JS实现向表格中动态添加行的方法
2015/03/30 Javascript
浅谈Node.js中的定时器
2015/06/18 Javascript
多个js毫秒倒计时同时进行效果
2016/01/05 Javascript
Jquery获取当前城市的天气信息
2016/08/05 Javascript
AngularJS基于factory创建自定义服务的方法详解
2017/05/25 Javascript
vue实现a标签点击高亮方法
2018/03/17 Javascript
Node.js中读取TXT文件内容fs.readFile()用法
2018/10/10 Javascript
element vue validate验证名称重复 输入框与后台重复验证 特殊字符 字符长度 及注意事项小结【实例代码】
2018/11/20 Javascript
layui form表单提交之后重新加载数据表格的方法
2019/09/11 Javascript
js实现简单的日历显示效果函数示例
2019/11/25 Javascript
vue+elementUI实现简单日历功能
2020/09/24 Javascript
JS跨浏览器解析XML应用过程详解
2020/10/16 Javascript
在Python中操作列表之List.pop()方法的使用
2015/05/21 Python
python获取元素在数组中索引号的方法
2015/07/15 Python
在MAC上搭建python数据分析开发环境
2016/01/26 Python
Python爬取网易云音乐上评论火爆的歌曲
2017/01/19 Python
selenium+python配置chrome浏览器的选项的实现
2020/03/18 Python
python requests包的request()函数中的参数-params和data的区别介绍
2020/05/05 Python
欧洲第一的摇滚和金属乐队服装网站:EMP
2017/10/26 全球购物
善意的谎言事例
2014/02/15 职场文书
租车协议书范本2014
2014/11/17 职场文书
物业工程部岗位职责
2015/02/11 职场文书
2015年数学教师工作总结
2015/05/20 职场文书