如何在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定时调用方法成功后并停止调用示例
Apr 08 Javascript
Javascript实现计算个人所得税
May 10 Javascript
JS截取与分割字符串常用技巧总结
Nov 10 Javascript
livereload工具实现前端可视化开发【推荐】
Dec 23 Javascript
js实现带缓动动画的导航栏效果
Jan 16 Javascript
从对象列表中获取一个对象的方法,依据关键字和值
Sep 20 Javascript
JS中使用textPath实现线条上的文字
Dec 25 Javascript
jQuery实现的下雪动画效果示例【附源码下载】
Feb 02 jQuery
vue实现图片滚动的示例代码(类似走马灯效果)
Mar 03 Javascript
vue监听对象及对象属性问题
Aug 20 Javascript
详解JavaScript修改注册表的方法
Jan 05 Javascript
vue代码分块和懒加载非必要资源文件
Apr 11 Vue.js
微信小程序实现通讯录列表展开收起
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实现插入排序
2015/03/29 PHP
js 编程笔记 无名函数
2011/06/28 Javascript
非常好用的JsonToString 方法 简单实例
2013/07/18 Javascript
js实现的捐赠管理完整实例
2015/01/20 Javascript
jquery实现带缩略图的全屏图片画廊效果实例
2015/06/25 Javascript
jQuery on()绑定动态元素出现的问题小结
2016/02/19 Javascript
JavaScript图像延迟加载库Echo.js
2016/04/05 Javascript
jQuery中$.each()函数的用法引申实例
2016/05/12 Javascript
Bootstrap中的表单验证插件bootstrapValidator使用方法整理(推荐)
2016/06/21 Javascript
详解jQuery uploadify文件上传插件的使用方法
2016/12/16 Javascript
无循环 JavaScript(map、reduce、filter和find)
2017/04/08 Javascript
JavaScript实现图片本地预览功能【不用上传至服务器】
2017/09/20 Javascript
简单实现jQuery弹窗效果
2017/10/30 jQuery
vue2实现可复用的轮播图carousel组件详解
2017/11/27 Javascript
微信小程序实现MUI数字输入框效果
2018/01/31 Javascript
JS获取input[file]的值并显示在页面的实现方法
2018/03/09 Javascript
Vue条件循环判断+计算属性+绑定样式v-bind的实例
2018/09/18 Javascript
vue实现移动端悬浮窗效果
2018/12/01 Javascript
JS使用对象的defineProperty进行变量监控操作示例
2019/02/02 Javascript
深入学习JavaScript中的bom
2019/05/27 Javascript
详解Typescript里的This的使用方法
2021/01/08 Javascript
Python实现计算两个时间之间相差天数的方法
2017/05/10 Python
python实现京东秒杀功能
2018/07/30 Python
Python3爬虫学习之MySQL数据库存储爬取的信息详解
2018/12/12 Python
Django框架基础模板标签与filter使用方法详解
2019/07/23 Python
Python通过两个dataframe用for循环求笛卡尔积
2020/04/29 Python
网络、C以及其他硬件方面的面试题
2016/08/23 面试题
如何在.net Winform里面显示PDF文档
2012/09/11 面试题
外语学院毕业生的自我鉴定
2013/11/28 职场文书
房产委托公证书样本
2014/04/04 职场文书
工地安全质量标语
2014/06/07 职场文书
2014年接待工作总结
2014/11/26 职场文书
信用卡收入证明范本
2015/06/12 职场文书
小学新课改心得体会
2016/01/22 职场文书
小学三年级数学教学反思
2016/02/16 职场文书
《青山不老》教学反思
2016/02/22 职场文书