上节最后提到:
思考下,目前代码层面还有什么致命缺陷?
我们先梳理下目前接口请求后代码流动情况:
可以看到,从mod.ts开始到最终model.ts处理完业务,中间每个环节都有可能产生异常,抛开oak框架本身处理的环节(1、2、3、8、9、10),仍有4、5、6、7随便哪个阶段产生错误,我们程序都有可能崩溃。
比如我们在user.service.ts中修改getAll,抛出一句错误:
async getAll(): Promise {
throw new Error("getAll error"); // 新增这句
return this.userModel.findAll();
}
再在浏览器请求http://localhost:8000/user,你看到了什么?
打开网络,也能看到接口响应变成了500。
这并不可怕,可怕的是回到vscode的控制台,看你的服务一大段报错信息:
这是未捕获的应用错误。好在框架帮你有个默认处理,服务并没有停止。
全局中间件异常捕获
针对这种情况,我们可以选择在上层控制层(user.router.ts)中捕获错误,保障代码调用的正确。但如果控制层的代码也出错了怎么办?
所以,我们还需要有个全局的异常捕获,修改main.ts,在const app = new Application()之后添加:
app.use(async (context, next) => {
try {
await next();
} catch (error) {
context.response.status = error.status || 500;
context.response.body = error.message;
}
});
必须是第一个中间件,思考下为什么。提示:洋葱模型。
这时,再在浏览器里访问一次http://localhost:8000/user:
全局错误捕获
我们加了全局的中间件异常捕获,是不是万事大吉,可以高枕无忧了?
当然不是。
再次修改user.service.ts的getAll,延迟抛出一个错误:
async getAll(): Promise {
setTimeout(() => {
throw new Error("getAll error");
}, 0);
return this.userModel.findAll();
}
继续在浏览器请求http://localhost:8000/user。
咦,你看到接口是正常的:
不要高兴太早,再刷新一下,是不是看到浏览器在不断转圈了?
赶快看你的vscode的控制台:
这次服务是真挂了!只能重启服务了。
这是怎么回事呢?
再仔细看
try {
await next();
} catch (error) {
...
}
原来我们这段代码try/catch只是捕获next函数这个Promise结束,可以说是中间件范围内的Promise异常捕获,而setTimeout跳出了这个控制,是无法被它捕获的。
那这种情况应该怎么办呢?
Deno实现了浏览器的绝大多数API,windows.onerror、addEventListener也不例外,所以可以再加个全局监听,当然,也不能忘了阻止默认处理。
addEventListener("error", (evt) => {
evt.preventDefault(); // 这句很重要
console.error(`global`, evt.error);
});
注意:这只是一个兜底方案,正常情况下,不允许在正常代码中使用setTimeout等定时器跳出当前业务逻辑。如果是想要延时,也需要将它包装成Promise,再用await等待结果返回,又或者主动处理好错误。
全局错误捕获不了的情况
经过上边两步,你可能以为已经拥有了不灭金身:
事实打脸会很快。
开发者容易犯的一个错误是,上层调用的下层方法通常是一个Promise,但忘了加await等待它结束。
比如这样,我们再修改getAll方法:
async getAll(): Promise {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("error"));
}, 100);
});
}
再把上层user.router.ts调用的await去掉:
刷新http://localhost:8000/user,这次不用看页面结果,直接看vscode控制台:
不出意外,服务又挂了!
在浏览器中,window.onerror
或addEventListener
监听error事件,只能捕获普通错误,对未处理的Promise是无解的,只能用另一个事件unhandledrejection
。
addEventListener("unhandledrejection", (evt) => {
evt.preventDefault();
console.error(`unhandledrejection`, evt.reason);
});
Deno直到v1.24才实现这个事件。而Node.js则有同样的事件:
process.on('unhandledRejection', (reason, promise) => {});
尽管如此,我们还是应该在平时养成良好的习惯,对使用的Promise进行异常捕获,如使用catch,又或者await。
作业
有了上面3点,已经能尽可能保障服务不宕机了(并非百分百)。但这只是一个服务最基本的需求。再想想看,我们的接口完美无缺了吗?提醒下,是不是还没有进行参数校验?