隨着 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
讓咱們先來看下在 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.json
。json
app.get('/', async function (req, res, next) { const data = await readFileAsync('./age.json'); res.send(data.toString()); });
如今咱們去請求http://127.0.0.1:3000/
時,發現請求遲遲不能響應,最終會超時。而在終端報了以下的錯誤:
發現錯誤並無被錯誤處理中間件處理,而是拋出了一個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 建立的 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 處理請求,對業務也毫無侵入性,拋出的錯誤也能傳遞到錯誤處理中間件。