讓Express支持async/await

隨着 Node.js v8 的發佈,Node.js 已原生支持 async/await 函數,Web 框架 Koa 也隨之發佈了 Koa 2 正式版,支持 async/await 中間件,爲處理異步回調帶來了極大的方便。javascript

既然 Koa 2 已經支持 async/await 中間件了,爲何不直接用 Koa,而還要去改造 Express 讓其支持 async/await 中間件呢?由於 Koa 2 正式版發佈纔不久,而不少老項目用的都仍是 Express,不可能將其推倒用 Koa 重寫,這樣成本過高,但又想用到新語法帶來的便利,那就只能對 Express 進行改造了,並且這種改造必須是對業務無侵入的,否則會帶來不少的麻煩。java

直接使用 async/await

讓咱們先來看下在 Express 中直接使用 async/await 函數的狀況。express

const express = require('express');
const app = express();
const { promisify } = require('util');
const { readFile } = require('fs');
const readFileAsync = promisify(readFile);
   
app.get('/', async function (req, res, next) {
  const data = await readFileAsync('./package.json');
  res.send(data.toString());
});
// Error Handler
app.use(function (err, req, res, next) {
  console.error('Error:', err);
  res.status(500).send('Service Error');
});
   
app.listen(3000, '127.0.0.1', function () {
  console.log(`Server running at http://${ this.address().address }:${ this.address().port }/`);
});

上面是沒有對 Express 進行改造,直接使用 async/await 函數來處理請求,當請求http://127.0.0.1:3000/時,發現請求能正常請求,響應也能正常響應。這樣彷佛不對 Express 作任何改造也能直接使用 async/await 函數,但若是 async/await 函數裏發生了錯誤能不能被咱們的錯誤處理中間件處理呢?如今咱們去讀取一個不存在文件,例如將以前讀取的package.json換成age.jsonjson

app.get('/', async function (req, res, next) {
  const data = await readFileAsync('./age.json');
  res.send(data.toString());
});

如今咱們去請求http://127.0.0.1:3000/時,發現請求遲遲不能響應,最終會超時。而在終端報了以下的錯誤:
UnhandlerRejectionError
發現錯誤並無被錯誤處理中間件處理,而是拋出了一個unhandledRejection異常,如今若是咱們用 try/catch 來手動捕獲錯誤會是什麼狀況呢?app

app.get('/', async function (req, res, next) {
  try {
    const data = await readFileAsync('./age.json');
    res.send(datas.toString());
  } catch(e) {
    next(e);
  }
});

發現請求被錯誤處理中間件處理了,說明咱們手動顯式的來捕獲錯誤是能夠的,可是若是在每一箇中間件或請求處理函數裏面加一個 try/catch 也太不優雅了,對業務代碼有必定的侵入性,代碼也顯得難看。因此經過直接使用 async/await 函數的實驗,咱們發現對 Express 改造的方向就是可以接收 async/await 函數裏面拋出的錯誤,又對業務代碼沒有侵入性。框架

改造 Express

在 Express 中有兩種方式來處理路由和中間件,一種是經過 Express 建立的 app,直接在 app 上添加中間件和處理路由,像下面這樣:異步

const express = require('express');
const app = express();
   
app.use(function (req, res, next) {
  next();
});
app.get('/', function (req, res, next) {
  res.send('hello, world');
});
app.post('/', function (req, res, next) {
  res.send('hello, world');
});
   
app.listen(3000, '127.0.0.1', function () {
  console.log(`Server running at http://${ this.address().address }:${ this.address().port }/`);
});

另一種是經過 Express 的 Router 建立的路由實例,直接在路由實例上添加中間件和處理路由,像下面這樣:async

const express = require('express');
const app = express();
const router = new express.Router();
app.use(router);
   
router.get('/', function (req, res, next) {
  res.send('hello, world');
});
router.post('/', function (req, res, next) {
  res.send('hello, world');
});
   
app.listen(3000, '127.0.0.1', function () {
  console.log(`Server running at http://${ this.address().address }:${ this.address().port }/`);
});

這兩種方法能夠混合起來用,如今咱們思考一下怎樣才能讓一個形如app.get('/', async function(req, res, next){})的函數,讓裏面的 async 函數拋出的錯誤能被統一處理呢?要讓錯誤被統一的處理固然要調用 next(err) 來讓錯誤被傳遞到錯誤處理中間件,又因爲 async 函數返回的是 Promise,因此確定是形如這樣的asyncFn().then().catch(function(err){ next(err) }),因此按這樣改造一下就有以下的代碼:函數

app.get = function (...data) {
  const params = [];
  for (let item of data) {
    if (Object.prototype.toString.call(item) !== '[object AsyncFunction]') {
      params.push(item);
      continue;
    }
    const handle = function (...data) {
      const [ req, res, next ] = data;
      item(req, res, next).then(next).catch(next);
    };
    params.push(handle);
  }
  app.get(...params)
}

上面的這段代碼中,咱們判斷app.get()這個函數的參數中,如有 async 函數,就採用item(req, res, next).then(next).catch(next);來處理,這樣就能捕獲函數內拋出的錯誤,並傳到錯誤處理中間件裏面去。可是這段代碼有一個明顯的錯誤就是最後調用 app.get(),這樣就遞歸了,破壞了 app.get 的功能,也根本處理不了請求,所以還須要繼續改造。
咱們以前說 Express 兩種處理路由和中間件的方式能夠混用,那麼咱們就混用這兩種方式來避免遞歸,代碼以下:post

const express = require('express');
const app = express();
const router = new express.Router();
app.use(router);
    
app.get = function (...data) {
  const params = [];
  for (let item of data) {
    if (Object.prototype.toString.call(item) !== '[object AsyncFunction]') {
      params.push(item);
      continue;
    }
    const handle = function (...data) {
      const [ req, res, next ] = data;
      item(req, res, next).then(next).catch(next);
    };
    params.push(handle);
  }
  router.get(...params)
}

像上面這樣改造以後彷佛一切都能正常工做了,能正常處理請求了。但經過查看 Express 的源碼,發現這樣破壞了 app.get() 這個方法,由於 app.get() 不只能用來處理路由,並且還能用來獲取應用的配置,在 Express 中對應的源碼以下:

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }
    
    this.lazyrouter();
    
    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

因此在改造時,咱們也須要對 app.get 作特殊處理。在實際的應用中咱們不只有 get 請求,還有 post、put 和 delete 等請求,因此咱們最終改造的代碼以下:

const { promisify } = require('util');
const { readFile } = require('fs');
const readFileAsync = promisify(readFile);
const express = require('express');
const app = express();
const router = new express.Router();
const methods = [ 'get', 'post', 'put', 'delete' ];
app.use(router);
    
for (let method of methods) {
  app[method] = function (...data) {
    if (method === 'get' && data.length === 1) return app.set(data[0]);

    const params = [];
    for (let item of data) {
      if (Object.prototype.toString.call(item) !== '[object AsyncFunction]') {
        params.push(item);
        continue;
      }
      const handle = function (...data) {
        const [ req, res, next ] = data;
        item(req, res, next).then(next).catch(next);
      };
      params.push(handle);
    }
    router[method](...params);
  };
}
      
app.get('/', async function (req, res, next) {
  const data = await readFileAsync('./package.json');
  res.send(data.toString());
});
      
app.post('/', async function (req, res, next) {
  const data = await readFileAsync('./age.json');
  res.send(data.toString());
});
    
router.use(function (err, req, res, next) {
  console.error('Error:', err);
  res.status(500).send('Service Error');
}); 
     
app.listen(3000, '127.0.0.1', function () {
  console.log(`Server running at http://${ this.address().address }:${ this.address().port }/`);
});

如今就改造完了,咱們只須要加一小段代碼,就能夠直接用 async function 做爲 handler 處理請求,對業務也毫無侵入性,拋出的錯誤也能傳遞到錯誤處理中間件。

原文連接:讓Express支持async/await

相關文章
相關標籤/搜索