自从上次写了一篇《如何正确处理 promise、await 产生的错误》之后,有很多童鞋问我,如何正确的封装请求,以及如何处理请求的错误。今天就来聊聊这个问题。
本文以 axios 为例,以一般网络上的封装为例,来说明例子中的问题。
一个错误的封装
我们假设 api 的地址地址为:api.my.com 且返回的数据格式为:
typescript
interface ResponseBody<T> {
code: number;
data: T;
message: string;
}
我们假设 code
为 0 时,表示请求成功,否则表示请求失败。 并且假设引入 element-ui 的 Message 组件,用于显示错误信息。
typescript
import axios from "axios";
import { Message } from "element-ui";
const request = axios.create({
baseURL: "https://api.my.com",
});
request.interceptors.response.use(
(response) => {
const responseBody = response.data;
if (responseBody.code === 0) {
return responseBody.data;
} else {
Message.errror(responseBody.message);
}
},
(error) => {
Message.error(error.message);
}
);
export default request;
上述代码,我们在其他地方使用的时候,就可以这样使用:
typescript
import request from "./request";
async function getUserInfo() {
const userInfo = await request.get("/user/info");
console.log(userInfo);
}
问题
上述代码,看起来没有什么问题,但是,实际上,这个封装是有问题的。
问题在于,我们在 request
中,对于错误的处理,是通过 Message.error
来处理的。这样的话,就会导致,我们在使用 request
的时候,无法捕获错误。在后续的代码中,我们会得到一个 undefined
的结果。
这种情况,在《如何正确处理 promise、await 产生的错误》中,已经提到过了。我叫这种情况为:错误转移。
因为我们由于错误的处理,导致错误信息与堆栈丢失,从而导致错误的转移。这种情况,是非常严重的,因为我们无法定位错误的位置,也无法定位错误的原因。
具体分析,我们可以查看《如何正确处理 promise、await 产生的错误》中的分析。
另一个小问题是,对于axios里边,get和post等方法,返回的是一个 Promise<Response<T>>
, 而不是一个 Promise<T>
。 如果我们想类型推导正确。我们最好把 response
返回,而不是 response.data
。 当然,我们可以覆写 axios
的类型,但是这个问题,就不在本文的讨论范围内了。
正确的封装
我们需要把错误转移的问题,交给调用者来处理。我们只需要把错误抛出即可。
typescript
request.interceptors.response.use(
(response) => {
const responseBody = response.data;
if (responseBody.code === 0) {
response.data = responseBody.data;
return response;
} else {
throw new Error(responseBody.message);
// 或者
// return Promise.reject(new Error(responseBody.message));
}
},
(error) => {
throw error;
// 或者
// return Promise.reject(error);
}
);
这种情况下,错误可以在上层调用者中捕获到。一般情况下,我们会在最顶层的调用者中捕获错误,而不是在每一个调用者中捕获错误。 其中在浏览器中,我们可以在程序入口中捕获,或者对于未捕获的错误,我们可以通过 window.onunhandledrejection
来捕获。
其实一些主流请求库中,拦截器的作用,是为了将程序正确的流程,与错误的流程分开。让服务端返回的,应该表达出来的错误,能够正确的表达出来。而不是用于错误处理。
在程序入口中捕获
在不同的框架中,程序入口的位置不同。在 vue 中,我们可以在 errorHandler
中捕获错误, 对于 vue-router 中的错误,我们可以在 router.onError
中捕获错误。
typescript
import Vue from "vue";
import router from "./router";
Vue.config.errorHandler = (err, vm, info) => {
// 用于开发者工具的错误提示
console.error(err);
// 用于用户的错误提示
Message.errror(responseBody.message);
};
router.onError((err) => {
console.error(err);
Message.errror(responseBody.message);
});
通过 onunhandledrejection 捕获
以下例子是通过 window.onunhandledrejection
来捕获错误:
typescript
window.onunhandledrejection = (event) => {
console.error(event.reason);
};
如果在 nodejs 中,我们可以通过 process.on('unhandledRejection', (reason, promise) => {})
来捕获。
错误的可能性
在上述例子中,如果我们的 api 返回错误,我们可能得到以下几种错误:
- 在错误拦截器中,我们主动抛出的错误
throw new Error(responseBody.message);
- 在
axios
中,axios自动抛出的错误new AxiosError(message, config, code, request, response);
,这种错误,在状态码异常的时候,会自动抛出。具体可以查看axios的源码。
错误处理器
在《如何正确处理 promise、await 产生的错误》中,我们提到了错误处理器的概念。错误处理器,是一个函数,它接受一个错误,然后对错误进行处理。
在上述的例子中,我们可以看到,我们在最顶层的调用者中捕获错误,console.error
充当了我们最简单的错误处理器。
但是,如果我们想要更加复杂的错误处理,我们可以进一步封装。如果童鞋们感兴趣,我们可以在下一篇文章中,来聊聊错误处理器的封装。
总结
本文,我们聊了聊,如何正确的封装请求,以及如何处理请求的错误。总结起来,就是:
- 不要在拦截器中,对错误进行处理(包括log,toast等),而是应该抛出错误。
- 在最顶层的调用者中,捕获错误。
还是引用那就话
我不喜欢别人将await/proimise跟错误处理捆在一起讲,这显得很菜。
同理,我也不喜欢别人将请求封装跟错误处理捆在一起讲,因为请求错误只是错误的一种,而我们的错误处理器,应该是一个通用的错误处理器,而不是针对某一种错误的处理器。