随着 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 处理请求,对业务也毫无侵入性,抛出的错误也能传递到错误处理中间件。