思考 Koa 和 Express 的异常捕获为什么会不同

每次对自己小脑袋突然蹦出的奇怪问题的不放弃,都使我受益良多,不止于获得知识。下面是关于为什么 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() 就是在执行中间件,也就是说是在上一个中间件中执行了下一个中间件,中间件之间是嵌套着的。我在最前边 trycatch 就相当于在对所有的中间件 trycatch 。难不成是这个原因?

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);
复制代码

终端打印结果如下:

image-20211011211219455.png

然后老师就会说,这就是洋葱模型,一层一层包裹嵌套着,但是兄弟们,在不涉及 asyncawait 的情况下,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);
复制代码

控制台打印如下:

image-20211011213742384.png

可以看到,并没有捕获到错误,只是 throw error 后面的语句没有打印,错误捕获中间件根本没有生效。我把自己的测试代码贴出来,有一点考虑,是怕自己的测试代码写错了,如果其实是可以根据前边给 Koa 写错误捕获的逻辑给 Express 写的。那就尴尬了,那后面的全都是错的,所以先留一手,也希望大家能帮我看看。

2.3 重新审视 Koa 的洋葱模型

思考不出结果,咱就找资料啊,挂个梯子,Google 一下,幸好还是找到了答案。原来,理解洋葱模型搭上响应会要好很多。

  • 在 Express 中我们是通过 res.send 来向客户端响应数据的,而且是立即响应,不能在多个中间件里面 res.send
  • 在 Koa 中我们是通过 ctx.body,你可以在多个中间件里面修改它,所有中间件都执行完了之后,才会响应给客户端

Koa 中请求和响应都在最外层,中间件在中间一层一层的处理,用洋葱来形容还是蛮合适的。附上偷来的模型图:

u=1030604743,3816430031&fm=15&fmt=auto.webp

2.4 Express 中的全局捕获

重新理解了洋葱模型,算是解决了其中一个疑惑,但是最开始的问题好像并没有得到解答,为什么 Koa 可以在最前边放一个中间件捕获错误,Express 却是不可以。我也找了资料,看在 Express 中式怎样全局捕获错误的。看到一些答案基本原理都还是对每一个中间件 trycatch,然后将错误,通过 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 里面一样去在最前面全局的捕获异常。因为在每个中间件里面,它自己是包含了一段 trycatch 的逻辑,捕获到错误之后,Express 的中间件是这样做的:

image-20211012231543317.png

可以看到,它自己捕获到异常之后,在 catch 里面就直接传递给 next 了,它默认是交给自己封装的中间件去处理的。这样你再在外面套一层 trycatch 是不会再捕获到错误的。

然后我们再来看看 Koa 的中间件里面是如何处理的:

image-20211012231324639.png

可以看到 ,虽然它里面也使用了 trycatch 来捕获异常,但是在 catch 里面它是 Promise.reject(err) 将错误再次抛了出去,外层的 trycatch 自然也就能够捕获到了。这样看来和是不是洋葱模型没有多大的关系。能够 trycatch 就是因为中间件代码结构上嵌套的关系

说到底还是那个理,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,也问了挺多地方的,但是大家好像都没有在这种小地方思考停留过,可能我的注意力比较奇怪吧,最后还是在大家一同的帮助下得以解决。非常感恩,马不停蹄写了这篇博客,向大伙交代。

参考:

koa与express的中间件机制揭秘

多维度分析 Express、Koa 之间的区别

猜你喜欢

转载自juejin.im/post/7018199317076770846