每次对自己小脑袋突然蹦出的奇怪问题的不放弃,都使我受益良多,不止于获得知识。下面是关于为什么 Express 不能像 Koa 一样在最前边定义中间件,全局捕获异常的思考,精神食粮,欢迎食用。
1. 观察
使用过 Express 的脚手架 express-generator
和 Koa 的 koa-generator
创建项目的同学,有没有仔细观察过,里面的错误是如何捕获的呢?什么?你没有,好吧,我刚好有。待我细细道来~
1.1 Express 的错误捕获
在 Express 里面是使用 的一个接收四个参数的中间件来处理的,如下:
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'dev' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
复制代码
这个中间件作为是最后一个中间件,其他中间件运行的时候,如果报错,将错误使用 next
,传递给错误出来,上边这个中间件就能捕获到,进行处理。
大致如下:
app.get('/', function(req, res,next){
try{
......
next();
} catch (err){
next(err)
}
});
复制代码
但是呢,这样就需要在每一个中间件里面 try
catch
,会写很多重复的代码,然后咱再看一下 Koa 里面的错误处理是咋个样子搞的,真没有对比就没有伤害
1.2 Koa 的错误捕获
koa-generator
创建的项目中使用了一个叫 koa-onerror
的工具,放在最前面,onerror(app)
,这时候真想脑袋抽筋一会,这算什么,用的别人做好的工具,我怎么观察,我就去 github 上边搜了一下,发现只有一百多 star,也没啥收获。然后我就上网查 Koa 的错误处理机制,这一查,我好像就懂了点什么.
我们可以自己写一个捕获错误的中间件,放在最前面,就像下面这样:
const catchError = async(ctx, next) => {
try{
await next();
}catch(error){
ctx.body = error.msg;
}
}
app.use(catchError)
复制代码
上边这个中间件就可以全局的捕获中间件中的错误,咱不是空口瞎说,我写过测试代码的,app.use(catchError)
能够捕获到在它后面执行的中间件中的错误,这就是为什么前面提到过两次放最前边。
1.3 产生疑惑
但我还是一脸疑惑,纳尼?凭什么这样就可以全局的捕获中间件的错误?我猜想,每个中间件之间的执行是靠 next
来衔接的,和双越老师学习过一点 Koa 的中间件原理,next
代表的其实就是下一个执行的中间件,那么 next()
就是在执行中间件,也就是说是在上一个中间件中执行了下一个中间件,中间件之间是嵌套着的。我在最前边 try
,catch
就相当于在对所有的中间件 try
,catch
。难不成是这个原因?
2. 思考
真的是上边说的这个原因吗?我又突然想到 Express 中间件之间的衔接也是使用 next
呀!中间件之间也是嵌套嵌套着的。EXpress 的错误捕获怎么不这样写?

2.1 另一个疑惑
这个时候我还想起了我的另一个疑惑,大家在学习 Koa 的洋葱模型的时候,老师是不是都写过类似与下面的代码来解释什么是洋葱模型
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
console.log('我是第一个中间件开始')
await next();
console.log('我是第一个中间件结束')
});
app.use(async (ctx, next) => {
console.log('我是第二个中间件开始')
await next();
console.log('我是第二个中间件结束')
});
app.use(async (ctx, next) => {
console.log('我是第三个中间件开始')
await next();
console.log('我是第三个中间件结束')
});
app.use(async ctx => {
ctx.res.end ('hello ya');
});
app.listen(8000);
复制代码
终端打印结果如下:
然后老师就会说,这就是洋葱模型,一层一层包裹嵌套着,但是兄弟们,在不涉及 async
,await
的情况下,Express 按照上边的逻辑,也是可以打印出相同的结果的,一定要自己去试一下,怎么从来没人说 Express 的中间件机制是洋葱模型。
2.2 尝试写 Express 全局错误捕获
我从来饯行实践是检验真理的唯一标准,我按照上边给 Koa 写错误捕获捕获的逻辑给 Express 写了一小段测试代码,如下:
var express = require('express');
var app = express();
const catchError = (res,req, next) => {
console.log('异常捕获开始')
try{
next();
}catch(error){
console.log('捕获到异常')
}
console.log('异常捕获结束')
}
app.use(catchError)
app.use((req,res,next) => {
console.log('第一个中间件开始')
next()
console.log('第一个中间件结束')
})
app.use((req,res,next) => {
console.log('第二个中间件开始')
var error = new Error();
error.errorCode = 1000;
error.msg = "对不起,我错了";
throw error
console.log('第二个中间件结束')
})
app.listen(3000);
复制代码
控制台打印如下:
可以看到,并没有捕获到错误,只是 throw error
后面的语句没有打印,错误捕获中间件根本没有生效。我把自己的测试代码贴出来,有一点考虑,是怕自己的测试代码写错了,如果其实是可以根据前边给 Koa 写错误捕获的逻辑给 Express 写的。那就尴尬了,那后面的全都是错的,所以先留一手,也希望大家能帮我看看。
2.3 重新审视 Koa 的洋葱模型
思考不出结果,咱就找资料啊,挂个梯子,Google 一下,幸好还是找到了答案。原来,理解洋葱模型搭上响应会要好很多。
- 在 Express 中我们是通过
res.send
来向客户端响应数据的,而且是立即响应,不能在多个中间件里面res.send
。 - 在 Koa 中我们是通过
ctx.body
,你可以在多个中间件里面修改它,所有中间件都执行完了之后,才会响应给客户端
Koa 中请求和响应都在最外层,中间件在中间一层一层的处理,用洋葱来形容还是蛮合适的。附上偷来的模型图:
2.4 Express 中的全局捕获
重新理解了洋葱模型,算是解决了其中一个疑惑,但是最开始的问题好像并没有得到解答,为什么 Koa 可以在最前边放一个中间件捕获错误,Express 却是不可以。我也找了资料,看在 Express 中式怎样全局捕获错误的。看到一些答案基本原理都还是对每一个中间件 try
,catch
,然后将错误,通过 next
传递给处理错误的中间件去处理。只是做了封装。像下面这样:
const asyncHandler = fn => (req, res, next) =>
Promise.resolve()
.then(() => fn(req, res, next))
.catch(next);
router.get('/', asyncHandler(async (req, res) => {
const user = await db.userInfo();
res.json(user);
}));
复制代码
或者是使用工具 express-async-errors
,听说它的原理也是如上,只是做到了路由层,我也不是很理解。
3. 解决
对不起,解决不了
本来这篇博客的结果应该是上边这样,但是碰上了大佬,在群里一番激烈讨论,我可能终于明白为什么了!真的,直接感动。
前边我们说过 Express 中常用 next
去传递错误,在末尾可以执行一个接收四个参数(包括 error)的中间件去处理错误。其实就是因为这个封装,导致我们不能像在 Koa 里面一样去在最前面全局的捕获异常。因为在每个中间件里面,它自己是包含了一段 try
,catch
的逻辑,捕获到错误之后,Express 的中间件是这样做的:
可以看到,它自己捕获到异常之后,在 catch
里面就直接传递给 next
了,它默认是交给自己封装的中间件去处理的。这样你再在外面套一层 try
,catch
是不会再捕获到错误的。
然后我们再来看看 Koa 的中间件里面是如何处理的:
可以看到 ,虽然它里面也使用了 try
,catch
来捕获异常,但是在 catch
里面它是 Promise.reject(err)
将错误再次抛了出去,外层的 try
,catch
自然也就能够捕获到了。这样看来和是不是洋葱模型没有多大的关系。能够 try
,catch
就是因为中间件代码结构上嵌套的关系
说到底还是那个理,Koa 比较轻量,不像 Express 那样内置了很多中间件。
下面我会附上关于 Koa 和 Express 的测试代码,感兴趣的同学,打上断点,单步执行就可以。
Express:
let express = require('express')
let app = express()
app.use(function (req,res,next) {
try {
console.log('第一个中间件开始')
next()
console.log('第一个中间件结束')
} catch(error){
console.log('对不起,我错了',error)
}
})
app.use(function(req,res,next) {
throw new Error('错了错了')//打上断点
})
app.listen(4000)
复制代码
Koa:
const koa = require("koa")
const app = new koa();
const catchError = async(ctx, next) => {
try{
await next();
}catch(error){
console.log('捕获到了:',error)
}
}
app.use(catchError)//执行中间件
app.use(async (ctx, next) => {
console.log('我是第一次开始')
await next()
console.log('我是第一层结束')
})
app.use(async (ctx, next) => {
throw Error('对不起,我错了')//打上断点
})
app.listen(3000)
复制代码
4. 总结
讲真,这个问题困惑了差不多三天,自己各种测试,百度 Google,也问了挺多地方的,但是大家好像都没有在这种小地方思考停留过,可能我的注意力比较奇怪吧,最后还是在大家一同的帮助下得以解决。非常感恩,马不停蹄写了这篇博客,向大伙交代。
参考: