如何在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 相关文章推荐
Javascript读取cookie函数代码
Oct 16 Javascript
jquery配合css简单实现返回顶部效果
Sep 30 Javascript
jquery实现邮箱自动补全功能示例分享
Feb 17 Javascript
JavaScript实现找出数组中最长的连续数字序列
Sep 03 Javascript
javascript实现分栏显示小技巧附图
Oct 13 Javascript
最全面的百度地图JavaScript离线版开发
Sep 10 Javascript
实现一个简单的vue无限加载指令方法
Jan 10 Javascript
node.js中fs文件系统目录操作与文件信息操作
Feb 24 Javascript
jQuery+Datatables实现表格批量删除功能【推荐】
Oct 24 jQuery
利用Promise自定义一个GET请求的函数示例代码
Mar 20 Javascript
详解JavaScript对数组操作(添加/删除/截取/排序/倒序)
Apr 28 Javascript
微信小程序实现选项卡滑动切换
Oct 22 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&amp;&amp;mysql)三
2006/10/09 PHP
《PHP边学边教》(02.Apache+PHP环境配置――上篇)
2006/12/13 PHP
input file获得文件根目录简单实现
2013/04/26 PHP
Laravel5中contracts详解
2015/03/02 PHP
orm获取关联表里的属性值
2016/04/17 PHP
php实现微信支付之企业付款
2018/05/30 PHP
Javascript的IE和Firefox兼容性汇编
2006/07/01 Javascript
javascript 简单高效判断数据类型 系列函数 By shawl.qiu
2007/03/06 Javascript
javascript生成/解析dom的CDATA类型的字段的代码
2007/04/22 Javascript
关于js内存泄露的一个好例子
2013/12/09 Javascript
jquery单行文字向上滚动效果示例
2014/03/06 Javascript
javascipt:filter过滤介绍及使用
2014/09/10 Javascript
jQuery里filter()函数与find()函数用法分析
2015/06/24 Javascript
jquery带翻页动画的电子杂志代码分享
2015/08/21 Javascript
纯JS代码实现气泡效果
2016/05/04 Javascript
深入分析javascript中的错误处理机制
2016/07/17 Javascript
Bootstrap禁用响应式布局的实现方法
2017/03/09 Javascript
js+canvas实现滑动拼图验证码功能
2018/03/26 Javascript
jQuery插件simplePagination的使用方法示例
2020/04/28 jQuery
[01:12:53]完美世界DOTA2联赛PWL S2 Forest vs SZ 第一场 11.25
2020/11/26 DOTA
浅谈Python中函数的参数传递
2016/06/21 Python
Python中字符串的处理技巧分享
2016/09/17 Python
简单谈谈Python流程控制语句
2016/12/04 Python
Python 3.8正式发布重要新功能一览
2019/10/17 Python
numpy创建单位矩阵和对角矩阵的实例
2019/11/29 Python
如何基于python操作json文件获取内容
2019/12/24 Python
基于Python-Pycharm实现的猴子摘桃小游戏(源代码)
2021/02/20 Python
html5-Canvas可以在web中绘制各种图形
2012/12/26 HTML / CSS
如何让Java程序执行效率更高
2014/06/25 面试题
超市业务员岗位职责
2013/12/05 职场文书
大专会计自我鉴定
2014/02/06 职场文书
作文评语集锦
2014/12/25 职场文书
婚礼父母答谢词
2015/01/04 职场文书
2016春季幼儿园开学寄语
2015/12/03 职场文书
Win10鼠标轨迹怎么开 Win10显示鼠标轨迹方法
2022/04/06 数码科技
图神经网络GNN算法
2022/05/11 Python