前言
为了能够更好地处理异步流程,一般开发者会选择 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' "htmlcode">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 的方式对于你的风险太大,相比较下,第二种方案是最佳的实践方案。
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
- 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
- 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
- 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
- 群星《2024好听新歌42》AI调整音效【WAV分轨】
- 王思雨-《思念陪着鸿雁飞》WAV
- 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
- 李健《无时无刻》[WAV+CUE][590M]
- 陈奕迅《酝酿》[WAV分轨][502M]
- 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
- 群星《吉他王(黑胶CD)》[WAV+CUE]
- 齐秦《穿乐(穿越)》[WAV+CUE]
- 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
- 邝美云《邝美云精装歌集》[DSF][1.6G]
- 吕方《爱一回伤一回》[WAV+CUE][454M]