您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關async如何在Express4.x中使用,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
為了能夠更好地處理異步流程,一般開發者會選擇 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 方法性價比比較低,其它三種方法都需要根據實際情況去取舍,舉個栗子:
如果你需要寫一個 Express 中間件提供給各個團隊使用,那么修改源碼的方式肯定走不通,而 AOP 的方式對于你的風險太大,相比較下,第二種方案是最佳的實踐方案。
以上就是async如何在Express4.x中使用,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。