1. 程式人生 > 程式設計 >如何在Express4.x中愉快地使用async的方法

如何在Express4.x中愉快地使用async的方法

前言

為了能夠更好地處理非同步流程,一般開發者會選擇 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('/',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,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,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,function (err) {
   if (err) {
    return next(layerError || err);
   }

   if (route) {
    return layer.handle_request(req,next);
   }

   trim_prefix(layer,layerError,layerPath,path);
  });
 }

 function trim_prefix(layer,path) {

  if (layerError) {
   layer.handle_error(layerError,next);
  } else {
   layer.handle_request(req,next);
  }
 }
};

Express 內部通過 handle 方法來處理中介軟體執行邏輯,其利用 「 閉包的特性 」 快取 idx 來記錄當前遍歷的狀態。

該方法內部又實現了 next 方法來匹配當前需要執行的中介軟體,從遍歷的程式碼可以明白 「 中介軟體註冊的順序是非常重要的 」 。

如果該流程存在異常,則呼叫 layer 例項的 handle.error 方法,這裡仍然是 「 遵循了 Node.js 錯誤優先的設計理念 」 :

Layer.prototype.handle_error = function handle_error(error,next) {
 var fn = this.handle;

 if (fn.length !== 4) {
  // not a standard error handler
  return next(error);
 }

 try {
  fn(error,next);
 } catch (err) {
  next(err);
 }
};

內部通過判斷函式的形參個數過濾掉非錯誤處理中介軟體」。
如果 next 函式內部沒有異常情況,則呼叫 layer 例項的 handle_request 方法:

Layer.prototype.handle_request = function handle(req,next) {
 var fn = this.handle;

 if (fn.length > 3) {
  // not a standard request handler
  return next();
 }

 try {
  fn(req,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,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,next).catch(next);
 }

 try {
  fn(req,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,next) {
   fn(req,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內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!