让Express支持async/await

极客逐星号说
• 阅读 9358

随着 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 处理请求,对业务也毫无侵入性,抛出的错误也能传递到错误处理中间件。

原文链接:让Express支持async/await

点赞
收藏
评论区
推荐文章
亚瑟 亚瑟
4年前
Python Sanic 高并发服务开发指南
技术基础AsyncIOPython3.4开始引入AsyncIO(https://docs.python.org/3/library/asyncio.html)模块,使得Python也支持异步IO。3.5版本里添加了async/await关键字,使得异步IO代码编写更加方便。3.6和3.7版本继续进行了完善
郜小超 郜小超
4年前
用 async/await 来处理异步
一级标题昨天看了一篇vue的教程,作者用async/await来发送异步请求,从服务端获取数据,代码很简洁,同时async/await已经被标准化,是时候学习一下了。先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思,异步函数也就意味着该函数的执行不会阻塞后面代码的执行。写一个async
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Souleigh ✨ Souleigh ✨
4年前
理解 Javascript 中的 Async / Await
在本文中,我们将探讨async/await,对于每个Javascript开发人员来说,是异步编程的首选工具。如果您不熟悉javascript,请不要担心,本文将帮助您async/await从头开始理解。介绍async/await是javascript中的一种模式,可使您的代码以同步方式执行,但又不影响javascript的异步行为。定义异步功能要定义一
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Karen110 Karen110
4年前
盘点JavaScript中async/await知识
大家好,我是进阶学习者。一、前言Async/await是以更舒适的方式使用promise的一种特殊语法,同时它也非常易于理解和使用。二、Asyncfunction让以async这个关键字开始。它可以被放置在一个函数前面。如下所示:asyncfunctionf()return1;在函数前面的“async”这个单词表达了一个简单的
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
C#异步编程 Task await的理解
async/await是C5.0中推出的,先上用法:staticvoidMain(stringargs){Console.WriteLine("主线程启动");Task<inttaskGetStrLengthAsync();Conso
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
3年前
Python's Async and Await 异步
Python'sAsyncandAwait异步展开function\_typeof(e){returne&&"undefined"!typeofSymbol&&e.constructorSymbol?"symbol":typeofe;}!function(e){i
极客逐星号说
极客逐星号说
Lv1
待到重阳日,还来就菊花。
文章
5
粉丝
0
获赞
0