如何在Express4.x中愉快地使用async的方法
前言
為了能夠更好地處理異步流程,一般開發(fā)者會(huì)選擇 async 語(yǔ)法。在 express 框架中可以直接利用 async 來(lái)聲明中間件方法,但是對(duì)于該中間件的錯(cuò)誤,無(wú)法通過(guò)錯(cuò)誤捕獲中間件來(lái)劫持到。
錯(cuò)誤處理中間件
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
const message = doSomething();
res.send(message);
});
// 錯(cuò)誤處理中間件
app.use(function (err, req, res, next) {
return res.status(500).send('內(nèi)部錯(cuò)誤!');
});
app.listen(PORT, () => console.log(`app listening on port ${PORT}`));
以上述代碼為例,中間件方法并沒(méi)有通過(guò) async 語(yǔ)法來(lái)聲明,如果 doSomething 方法內(nèi)部拋出異常,那么就可以在錯(cuò)誤處理中間件中捕獲到錯(cuò)誤,從而進(jìn)行相應(yīng)地異常處理。
app.get('/', async (req, res) => {
const message = doSomething();
res.send(message);
});
而采用 async 語(yǔ)法來(lái)聲明中間件時(shí),一旦 doSomething 內(nèi)部拋出異常,則錯(cuò)誤處理中間件無(wú)法捕獲到。
雖然可以利用 process 監(jiān)聽(tīng) unhandledRejection 事件來(lái)捕獲,但是無(wú)法正確地處理后續(xù)流程。
try/catch
對(duì)于 async 聲明的函數(shù),可以通過(guò) try/catch 來(lái)捕獲其內(nèi)部的錯(cuò)誤,再使用 next 函數(shù)將錯(cuò)誤遞交給錯(cuò)誤處理中間件,即可處理該場(chǎng)景:
app.get('/', async (req, res, next) => {
try {
const message = doSomething();
res.send(message);
} catch(err) {
next(err);
}
});
「 這種寫法簡(jiǎn)單易懂,但是滿屏的 try/catch 語(yǔ)法,會(huì)顯得非常繁瑣且不優(yōu)雅。 」
高階函數(shù)
對(duì)于基礎(chǔ)扎實(shí)的開發(fā)來(lái)說(shuō),都知道 async 函數(shù)最終返回一個(gè) Promise 對(duì)象,而對(duì)于 Promsie 對(duì)象應(yīng)該利用其提供的 catch 方法來(lái)捕獲異常。
那么在將 async 語(yǔ)法聲明的中間件方法傳入 use 之前,需要包裹一層 Promise 函數(shù)的異常處理邏輯,這時(shí)就需要利用高階函數(shù)來(lái)完成這樣的操作。
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);
}));
相比較第一種方法, 「 高階函數(shù)減少了冗余代碼,在一定程度上提高了代碼的可讀性。 」
上述兩種方案基于扎實(shí)的 JavaScript 基礎(chǔ)以及 Express 框架的熟練使用,接下來(lái)從源碼的角度思考合適的解決方案。
中間件機(jī)制
Express 中主要包含三種中間件:
- 應(yīng)用級(jí)別中間件
- 路由級(jí)別中間件
- 錯(cuò)誤處理中間件
app.use = function use(fn) {
var path = '/';
// 省略參數(shù)處理邏輯
...
// 初始化內(nèi)置中間件
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;
};
應(yīng)用級(jí)別中間件通過(guò) app.use 方法注冊(cè), 「 其本質(zhì)上也是調(diào)用路由對(duì)象上的中間件注冊(cè)方法,只不過(guò)其默認(rèn)路由為 '/' 」 。
proto.use = function use(fn) {
var offset = 0;
var path = '/';
// 省略參數(shù)處理邏輯
...
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;
};
中間件的所有注冊(cè)方式最終會(huì)調(diào)用上述代碼,根據(jù) path 和中間件處理函數(shù)生成 layer 實(shí)例,再通過(guò)棧來(lái)維護(hù)這些 layer 實(shí)例。
// 部分核心代碼
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 內(nèi)部通過(guò) handle 方法來(lái)處理中間件執(zhí)行邏輯,其利用 「 閉包的特性 」 緩存 idx 來(lái)記錄當(dāng)前遍歷的狀態(tài)。
該方法內(nèi)部又實(shí)現(xiàn)了 next 方法來(lái)匹配當(dāng)前需要執(zhí)行的中間件,從遍歷的代碼可以明白 「 中間件注冊(cè)的順序是非常重要的 」 。
如果該流程存在異常,則調(diào)用 layer 實(shí)例的 handle.error 方法,這里仍然是 「 遵循了 Node.js 錯(cuò)誤優(yōu)先的設(shè)計(jì)理念 」 :
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);
}
};
「內(nèi)部通過(guò)判斷函數(shù)的形參個(gè)數(shù)過(guò)濾掉非錯(cuò)誤處理中間件」。
如果 next 函數(shù)內(nèi)部沒(méi)有異常情況,則調(diào)用 layer 實(shí)例的 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 方法初始化執(zhí)行了一次 next 方法,但是該方法每次調(diào)用最多只能匹配一個(gè)中間件 」 ,所以在執(zhí)行 handle_error 和 handle_request 方法時(shí),會(huì)將 next 方法透?jìng)鹘o中間件,這樣開發(fā)者就可以通過(guò)手動(dòng)調(diào)用 next 方法的方式來(lái)執(zhí)行接下來(lái)的中間件。
從上述中間件的執(zhí)行流程中可以知曉, 「 用戶注冊(cè)的中間件方法在執(zhí)行的時(shí)候都會(huì)包裹一層 try/catch,但是 try/catch 無(wú)法捕獲 async 函數(shù)內(nèi)部的異常,這也就是為什么 Express 中無(wú)法通過(guò)注冊(cè)錯(cuò)誤處理中間件來(lái)攔截到 async 語(yǔ)法聲明的中間件的異常的原因 」 。
修改源碼
找到本質(zhì)原因之后,可以通過(guò)修改源碼的方法來(lái)進(jìn)行適配:
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
// 針對(duì) async 語(yǔ)法函數(shù)特殊處理
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 方法內(nèi)部判斷了中間件方法通過(guò) async 語(yǔ)法聲明的情況,從而采用 Promise 對(duì)象的 catch 方法來(lái)向下傳遞異常。
「 這種方式可以減少上層冗余的代碼,但是實(shí)現(xiàn)該方式,可能需要 fork 一份 Express4.x 的源碼,然后發(fā)布一個(gè)修改之后的版本,后續(xù)還要跟進(jìn)官方版本的新特性,相應(yīng)的維護(hù)成本非常高。 」
express5.x 中將 router 部分剝離出了單獨(dú)的路由庫(kù) -- router
AOP(面向切面編程)
為了解決上述方案存在的問(wèn)題,我們可以嘗試?yán)?AOP 技術(shù)在不修改源碼的基礎(chǔ)上對(duì)已有方法進(jìn)行增強(qiáng)。
app.use(async function () {
const message = doSomething();
res.send(message);
})
以注冊(cè)應(yīng)用級(jí)別中間件為例,可以對(duì) app.use 方法進(jìn)行 AOP 增強(qiáng):
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);
}
前面源碼分析的過(guò)程中,app.use 內(nèi)部是有 this 調(diào)用的,所以這里需要 「 利用 bind 方法來(lái)避免后續(xù)調(diào)用過(guò)程中 this 指向出現(xiàn)問(wèn)題。 」
然后就是利用 AOP 的核心思想,重寫原始的 app.use 方法,通過(guò)不同的分支邏輯代理到原始的 app.use 方法上。
「 該方法相比較修改源碼的方式,維護(hù)成本低。但是缺點(diǎn)也很明顯,需要重寫所有可以注冊(cè)中間件的方法,不能夠像修改源碼那樣一步到位。 」
寫在最后
本文介紹了 Express 中使用 async 語(yǔ)法的四種解決方案:
- try/catch
- 高階函數(shù)
- 修改源碼
- AOP
除了 try/catch 方法性價(jià)比比較低,其它三種方法都需要根據(jù)實(shí)際情況去取舍,舉個(gè)栗子:
如果你需要寫一個(gè) Express 中間件提供給各個(gè)團(tuán)隊(duì)使用,那么修改源碼的方式肯定走不通,而 AOP 的方式對(duì)于你的風(fēng)險(xiǎn)太大,相比較下,第二種方案是最佳的實(shí)踐方案。
到此這篇關(guān)于如何在Express4.x中愉快地使用async的方法的文章就介紹到這了,更多相關(guān)Express4.x使用async內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Nodejs 數(shù)組的隊(duì)列以及forEach的應(yīng)用詳解
這篇文章主要介紹了Nodejs 數(shù)組的隊(duì)列以及forEach的應(yīng)用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02
node.js中module模塊的功能理解與用法實(shí)例分析
這篇文章主要介紹了node.js中module模塊的功能理解與用法,結(jié)合實(shí)例形式分析了node.js module模塊的基本功能、原理、用法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2020-02-02
node.JS md5加密中文與php結(jié)果不一致的解決方法
本篇文章主要介紹了node.JS md5加密中文與php結(jié)果不一致的解決方法,具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-05-05
創(chuàng)建簡(jiǎn)單的node服務(wù)器實(shí)例(分享)
下面小編就為大家?guī)?lái)一篇?jiǎng)?chuàng)建簡(jiǎn)單的node服務(wù)器實(shí)例(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
基于Koa(nodejs框架)對(duì)json文件進(jìn)行增刪改查的示例代碼
這篇文章主要介紹了基于Koa(nodejs框架)對(duì)json文件進(jìn)行增刪改查的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
node.js實(shí)現(xiàn)微信JS-API封裝接口的示例代碼
這篇文章主要介紹了node.js實(shí)現(xiàn)微信JS-API封裝接口的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
詳解nodejs 開發(fā)企業(yè)微信第三方應(yīng)用入門教程
這篇文章主要介紹了詳解nodejs 開發(fā)企業(yè)微信第三方應(yīng)用入門教程,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03

