如何在Express4.x中愉快地使用async的方法


Posted in Javascript onNovember 18, 2020

前言

为了能够更好地处理异步流程,一般开发者会选择 async 语法。在 express 框架中可以直接利用 async 来声明中间件方法,但是对于该中间件的错误,无法通过错误捕获中间件来劫持到。

错误处理中间件

const express = require('express');
 const app = express();
 const PORT = process.env.PORT || 3000;

 app.get('/', (req, res) => {
  const message = doSomething();
  res.send(message);
 });

 // 错误处理中间件
 app.use(function (err, req, res, next) {
  return res.status(500).send('内部错误!');
 });

 app.listen(PORT, () => console.log(`app listening on port ${PORT}`));

以上述代码为例,中间件方法并没有通过 async 语法来声明,如果 doSomething 方法内部抛出异常,那么就可以在错误处理中间件中捕获到错误,从而进行相应地异常处理。

app.get('/', async (req, res) => {
  const message = doSomething();
  res.send(message);
 });

而采用 async 语法来声明中间件时,一旦 doSomething 内部抛出异常,则错误处理中间件无法捕获到。

虽然可以利用 process 监听 unhandledRejection 事件来捕获,但是无法正确地处理后续流程。

try/catch

对于 async 声明的函数,可以通过 try/catch 来捕获其内部的错误,再使用 next 函数将错误递交给错误处理中间件,即可处理该场景:

app.get('/', async (req, res, next) => {
  try {
   const message = doSomething();
   res.send(message);
  } catch(err) {
   next(err);
  }
 });

「 这种写法简单易懂,但是满屏的 try/catch 语法,会显得非常繁琐且不优雅。 」

高阶函数

对于基础扎实的开发来说,都知道 async 函数最终返回一个 Promise 对象,而对于 Promsie 对象应该利用其提供的 catch 方法来捕获异常。

那么在将 async 语法声明的中间件方法传入 use 之前,需要包裹一层 Promise 函数的异常处理逻辑,这时就需要利用高阶函数来完成这样的操作。

function asyncUtil(fn) {
  return function asyncUtilWrap(...args) {
   const fnReturn = fn(args);
   const next = args[args.length - 1];
   return Promise.resolve(fnReturn).catch(next);
  }
 }

 app.use(asyncUtil(async (req, res, next) => {
  const message = doSomething();
  res.send(message);
 }));

相比较第一种方法, 「 高阶函数减少了冗余代码,在一定程度上提高了代码的可读性。

上述两种方案基于扎实的 JavaScript 基础以及 Express 框架的熟练使用,接下来从源码的角度思考合适的解决方案。

中间件机制

Express 中主要包含三种中间件:

  • 应用级别中间件
  • 路由级别中间件
  • 错误处理中间件
app.use = function use(fn) {
 var path = '/';

 // 省略参数处理逻辑
 ...

 // 初始化内置中间件
 this.lazyrouter();
 var router = this._router;

 fns.forEach(function (fn) {
  // non-express app
  if (!fn || !fn.handle || !fn.set) {
   return router.use(path, fn);
  }

  ...

 }, this);

 return this;
};

应用级别中间件通过 app.use 方法注册, 「 其本质上也是调用路由对象上的中间件注册方法,只不过其默认路由为 '/' 」 。

proto.use = function use(fn) {
 var offset = 0;
 var path = '/';

 // 省略参数处理逻辑
 ...

 var callbacks = flatten(slice.call(arguments, offset));

 for (var i = 0; i < callbacks.length; i++) {
  var fn = callbacks[i];

  ...

  // add the middleware
  debug('use %o %s', path, fn.name || '<anonymous>')

  var layer = new Layer(path, {
   sensitive: this.caseSensitive,
   strict: false,
   end: false
  }, fn);

  layer.route = undefined;

  this.stack.push(layer);
 }

 return this;
};

中间件的所有注册方式最终会调用上述代码,根据 path 和中间件处理函数生成 layer 实例,再通过栈来维护这些 layer 实例。

// 部分核心代码
proto.handle = function handle(req, res, out) {
 var self = this;
 var idx = 0;
 var stack = self.stack;

 next();

 function next(err) {
  var layerError = err === 'route'
   ? null
   : err;
  
  if (idx >= stack.length) {
   return;
  }

  var path = getPathname(req);

  // find next matching layer
  var layer;
  var match;
  var route;

  while (match !== true && idx < stack.length) {
   layer = stack[idx++];
   match = matchLayer(layer, path);
   route = layer.route;

   if (match !== true) {
    continue;
   }

  }

  // no match
  if (match !== true) {
   return done(layerError);
  }

  // this should be done for the layer
  self.process_params(layer, paramcalled, req, res, function (err) {
   if (err) {
    return next(layerError || err);
   }

   if (route) {
    return layer.handle_request(req, res, next);
   }

   trim_prefix(layer, layerError, layerPath, path);
  });
 }

 function trim_prefix(layer, layerError, layerPath, path) {

  if (layerError) {
   layer.handle_error(layerError, req, res, next);
  } else {
   layer.handle_request(req, res, next);
  }
 }
};

Express 内部通过 handle 方法来处理中间件执行逻辑,其利用 「 闭包的特性 」 缓存 idx 来记录当前遍历的状态。

该方法内部又实现了 next 方法来匹配当前需要执行的中间件,从遍历的代码可以明白 「 中间件注册的顺序是非常重要的 」 。

如果该流程存在异常,则调用 layer 实例的 handle.error 方法,这里仍然是 「 遵循了 Node.js 错误优先的设计理念 」 :

Layer.prototype.handle_error = function handle_error(error, req, res, next) {
 var fn = this.handle;

 if (fn.length !== 4) {
  // not a standard error handler
  return next(error);
 }

 try {
  fn(error, req, res, next);
 } catch (err) {
  next(err);
 }
};

内部通过判断函数的形参个数过滤掉非错误处理中间件」。
如果 next 函数内部没有异常情况,则调用 layer 实例的 handle_request 方法:

Layer.prototype.handle_request = function handle(req, res, next) {
 var fn = this.handle;

 if (fn.length > 3) {
  // not a standard request handler
  return next();
 }

 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 = function handle(req, res, next) {
 var fn = this.handle;

 if (fn.length > 3) {
  // not a standard request handler
  return next();
 }
 // 针对 async 语法函数特殊处理
 if (Object.prototype.toString.call(fn) === '[object AsyncFunction]') {
  return fn(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(async function () {
 const message = doSomething();
 res.send(message);
})

以注册应用级别中间件为例,可以对 app.use 方法进行 AOP 增强:

const originAppUseMethod = app.use.bind(app);
app.use = function (fn) {
 if (Object.prototype.toString.call(fn) === '[object AsyncFunction]') {
  const asyncWrapper = function(req, res, next) {
   fn(req, res, next).then(next).catch(next);
  }
  return originAppUseMethod(asyncWrapper);
 }
 return originAppUseMethod(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内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JS多物体 任意值 链式 缓冲运动
Aug 10 Javascript
JS中自定义定时器让它在某一时刻执行
Sep 02 Javascript
Js+php实现异步拖拽上传文件
Jun 23 Javascript
jQuery跨域问题解决方案
Aug 03 Javascript
跟我学习javascript的全局变量
Nov 16 Javascript
JS跨域解决方案之使用CORS实现跨域
Apr 14 Javascript
JS前端笔试题分析
Dec 19 Javascript
详解Vue.js分发之作用域槽
Jun 13 Javascript
JSON 数据格式详解
Sep 13 Javascript
vue+swiper实现侧滑菜单效果
Dec 28 Javascript
layui 实现table翻页滚动条位置保持不变的例子
Sep 05 Javascript
Element Breadcrumb 面包屑的使用方法
Jul 26 Javascript
微信小程序实现通讯录列表展开收起
Nov 18 #Javascript
关于JavaScript中异步/等待的用法与理解
Nov 18 #Javascript
微信小程序实现多行文字滚动
Nov 18 #Javascript
微信小程序实现自定义底部导航
Nov 18 #Javascript
微信小程序绘制半圆(弧形)进度条
Nov 18 #Javascript
微信小程序实现加入购物车滑动轨迹
Nov 18 #Javascript
Map与WeakMap类型在JavaScript中的使用详解
Nov 18 #Javascript
You might like
php urlencode()与urldecode()函数字符编码原理详解
2011/12/06 PHP
解析php中memcache的应用
2013/06/18 PHP
php判断用户是否关注微信公众号
2016/07/22 PHP
Yii2压缩PHP中模板代码的输出问题
2018/08/28 PHP
PHP iconv()函数字符编码转换的问题讲解
2019/03/22 PHP
PHP基于ip2long实现IP转换整形
2020/12/11 PHP
jquery删除指定的html标签并保留标签内文本内容的方法
2014/04/02 Javascript
javascript实现点击提交按钮后显示loading的方法
2015/07/03 Javascript
详解AngularJS中ng-src指令的使用
2016/09/07 Javascript
vue单页应用中如何使用jquery的方法示例
2017/07/27 jQuery
Angular4开发解决跨域问题详解
2017/08/28 Javascript
vue history 模式打包部署在域名的二级目录的配置指南
2019/07/02 Javascript
[01:02:09]Liquid vs TNC 2019国际邀请赛淘汰赛 胜者组 BO3 第二场 8.21
2020/07/19 DOTA
Python3实现将文件归档到zip文件及从zip文件中读取数据的方法
2015/05/22 Python
Python中elasticsearch插入和更新数据的实现方法
2018/04/01 Python
Python实现定期检查源目录与备份目录的差异并进行备份功能示例
2019/02/27 Python
Tornado实现多进程/多线程的HTTP服务详解
2019/07/25 Python
python根据时间获取周数代码实例
2019/09/30 Python
django框架cookie和session用法实例详解
2019/12/10 Python
详解用Python进行时间序列预测的7种方法
2020/03/13 Python
python3 循环读取excel文件并写入json操作
2020/07/14 Python
Python confluent kafka客户端配置kerberos认证流程详解
2020/10/12 Python
美国销售第一的智能手机和平板电脑保护壳:OtterBox
2017/12/21 全球购物
婚礼证婚人证婚词
2014/01/08 职场文书
运动会开幕式邀请函
2014/01/22 职场文书
餐厅采购员岗位职责
2014/03/06 职场文书
大学理论知识学习自我鉴定
2014/04/28 职场文书
做一个有道德的人演讲稿
2014/05/14 职场文书
电子专业自荐信
2014/07/01 职场文书
2014党员干部四风问题对照检查材料思想汇报
2014/09/24 职场文书
党员教师群众路线个人整改措施
2014/10/28 职场文书
2014年体育工作总结
2014/11/24 职场文书
护理专业自我评价
2015/03/11 职场文书
2015年禁毒宣传活动总结
2015/03/25 职场文书
学校食堂管理制度
2015/08/04 职场文书
实验室安全管理制度
2015/08/05 职场文书