当前位置 : 首页 > 保养数据

Axios异常处理机制:网络错误、状态码异常与超时处理全解析

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们实现了`axios`的基础功能,但到目前为止,我们都只是实现了一个很简单的`axios`,一个真正库的功能远比我们目前实现的要复杂,那么从这篇文章开始,我们就来一步步完善这个`axios`,把它向真正的`axios`库迈进。 那么,这篇文章我们就来实现`axios`的异常处理机制。我们都知道,在平时开发中,我们处理`HTTP`请求时不可避免的会请求失败或者遇到异常,那么此时我们就需要对异常进行处理,而`axios`中也会抛出异常,并且我们还可以对其进行配置。接下来,我们就来实现`axios`的异常处理机制。 # 2. 需求分析 在`axios`的核心流程中,我们通过`XMLHttpRequest`对象的`onerror`事件属性可以捕获到网络异常;通过`XMLHttpRequest`对象的`onreadystatechange`事件属性可以判断`HTTP`状态码是否在`200-300`之间,如果不在,则也是发生了异常。 当捕获到异常后,我们都要`reject`回一个错误,并且返回的错误是一个`AxiosError`类实例,该实例中包含错误配置`config`、错误码`code`、`XMLHttpRequest`对象实例`request`以及响应`response`。 另外,我们还可以配置`validateStatus`属性来自定义判断`HTTP`状态码是否合法,如果不合法,也同样抛出异常。 # 3. 异常处理 根据需求分析,我们分以下几步来实现: 1. 创建`AxiosError`类; 2. 在`xhr`函数中捕获异常并抛出; 3. 添加`validateStatus`配置; ## 3.1 创建 AxiosError 类 我们先来创建`AxiosError`类,我们在`src`目录下创建`helpers`文件夹,然后在其中创建`error.ts`文件,在该文件中创建`AxiosError`类: ```typescript // src/helpers/error.ts export interface AxiosError extends Error { config: AxiosRequestConfig; code?: string; request?: any; response?: AxiosResponse; isAxiosError: boolean; } export function createError( message: string, config: AxiosRequestConfig, code?: string, request?: any, response?: AxiosResponse ): AxiosError { const error = new Error(message) as AxiosError; return extend(error, { config, code, request, response, isAxiosError: true, }); } ``` 我们首先定义了`AxiosError`接口,它继承于`Error`类型,拥有`Error`所有的属性,另外还添加了`config`、`code`、`request`、`response`、`isAxiosError`这些属性。 然后我们定义了`createError`方法,该方法用于创建`AxiosError`类的实例对象,它内部调用了`Error`类,并把我们传入的参数合并到`Error`实例中,并且还添加了`isAxiosError`属性用于标识这是一个`AxiosError`类型的错误。 ## 3.2 在 xhr 函数中捕获异常并抛出 异常分为两种:网络异常和`HTTP`状态码异常。 - 网络异常:当网络出现异常(比如断网)的时候会触发`XMLHttpRequest`对象实例的`error`事件,我们可以在`onerror`的事件回调函数中捕获此类异常。 - `HTTP`状态码异常:当`HTTP`状态码不在`200-300`之间时,我们可以在`onreadystatechange`的事件回调函数中捕获此类异常,并且我们还可以通过配置`validateStatus`属性来自定义判断`HTTP`状态码是否合法。 OK,我们接下来就改写`xhr`函数,在其中添加上异常处理逻辑。 ```typescript // src/xhr.ts import { createError } from "./helpers/error"; export default function xhr(config: AxiosRequestConfig): AxiosPromise { return new Promise((resolve, reject) => { const { data = null, url, method = "get", headers, responseType, timeout, } = config; const request = new XMLHttpRequest(); if (responseType) { request.responseType = responseType; } if (timeout) { request.timeout = timeout; } request.open(method.toUpperCase(), url, true); request.onreadystatechange = function handleLoad() { if (request.readyState !== 4) { return; } if (request.status === 0) { return; } const responseHeaders = parseHeaders(request.getAllResponseHeaders()); const responseData = responseType && responseType !== "text" ? request.response : request.responseText; const response: AxiosResponse = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config, request, }; handleResponse(response); }; request.onerror = function handleError() { reject(createError("Network Error", config, null, request)); }; request.ontimeout = function handleTimeout() { reject( createError( `Timeout of ${timeout} ms exceeded`, config, "ECONNABORTED", request ) ); }; Object.keys(headers).forEach((name) => { if (data === null && name.toLowerCase() === "content-type") { delete headers[name]; } else { request.setRequestHeader(name, headers[name]); } }); request.send(data); function handleResponse(response: AxiosResponse) { if (response.status >= 200 && response.status < 300) { resolve(response); } else { reject( createError( `Request failed with status code ${response.status}`, config, null, request, response ) ); } } }); } ``` 我们添加了`handleResponse`函数,该函数负责处理响应,如果`HTTP`状态码在`200-300`之间,则`resolve`响应`response`,否则就`reject`一个错误,该错误是通过`createError`创建,并且把响应`response`也传了进去。 另外,我们还通过`XMLHttpRequest`对象实例的`onerror`事件属性来捕获网络异常,当网络异常时,我们`reject`一个由`createError`创建的错误。 我们还添加了`ontimeout`事件属性来捕获请求超时异常,当请求超时的时候,我们同样`reject`一个由`createError`创建的错误。 注意:当网络异常的时候,`XMLHttpRequest`对象实例的`readyState`属性并不会变为`4`,所以我们在`onreadystatechange`的事件回调函数中,如果`request.readyState`不为`4`我们直接`return`,并且当`request.status`为`0`时我们也`return`,因为网络异常或者超时异常时该值都为`0`。 ## 3.3 添加 validateStatus 配置 另外,我们还希望用户可以配置`validateStatus`选项,让用户可以自定义判断`HTTP`状态码是否合法,如果在用户自定义的规则中状态码是合法的,则正常`resolve(response)`,否则才`reject`错误。 首先,我们在`src/types/index.ts`中的配置对象`AxiosRequestConfig`里添加`validateStatus`属性。 ```typescript // src/types/index.ts export interface AxiosRequestConfig { // 新增 validateStatus?: (status: number) => boolean; } ``` 然后,我们在`src/xhr.ts`中的`handleResponse`函数里使用该属性: ```typescript function handleResponse(response: AxiosResponse) { if (!validateStatus || validateStatus(response.status)) { resolve(response); } else { reject( createError( `Request failed with status code ${response.status}`, config, null, request, response ) ); } } ``` 在`handleResponse`函数中,我们判断如果用户配置了`validateStatus`并且`validateStatus`函数返回的值为`true`,或者用户没有配置`validateStatus`,但是原生的`HTTP`状态码在`200-300`之间,我们都`resolve(response)`,否则才`reject`错误。 # 4. 编写 demo 接下来,我们编写 `demo` 来测试下异常处理是否有效。 在 `examples` 目录下创建 `error` 目录,在 `error` 目录下创建 `index.`: ``` < lang="en"> error example ``` 接着再创建 `app.ts` 作为入口文件: ```typescript // examples/error/app.ts import axios from "../../src/index"; // 模拟网络错误 axios({ method: "get", url: "/error/get1", }) .then((res) => { console.log(res); }) .catch((e) => { console.log(e); }); // 模拟状态码错误 axios({ method: "get", url: "/error/get", }) .then((res) => { console.log(res); }) .catch((e) => { console.log(e); }); // 模拟超时错误 axios({ method: "get", url: "/error/timeout", timeout: 2000, }) .then((res) => { console.log(res); }) .catch((e) => { console.log(e); }); ``` 接着在 `server.js` 添加新的接口路由: ```javascript // 模拟网络错误 router.get("/error/get1", function (req, res) { res.status(500); res.end(); }); // 模拟状态码错误 router.get("/error/get", function (req, res) { if (Math.random() > 0.5) { res.json({ msg: `hello world`, }); } else { res.status(500); res.end(); } }); // 模拟超时错误 router.get("/error/timeout", function (req, res) { setTimeout(() => { res.json({ msg: `hello world`, }); }, 3000); }); ``` 最后在根目录下的 `index.` 中加上启动该 `demo` 的入口: ```
  • error
  • ``` OK, 我们在命令行中执行: ```bash # 同时开启客户端和服务端 npm run server | npm start ``` 接着打开 `chrome` 浏览器,访问 即可访问我们的 `demo` 了,我们点击 `error`,通过`F12`的控制台我们可以看到:网络错误、状态码错误以及超时错误都已经被捕获到了,并且返回的错误也都包含了错误配置`config`、错误码`code`、`XMLHttpRequest`对象实例`request`以及响应`response`等信息。 ![](~@/axios/05/01.png) # 5. 遗留问题 我们虽然已经实现了异常处理机制,但是目前还是存在一个问题:如果我们先设置请求超时时间为 2 秒,`console.log(e)` 打印出来的 `e` 是一个 `Error` 实例,但是我们再 `console.log(e.message)` 的时候却返回的是 `undefined`,这是为什么呢? 其实原因很简单,因为我们通过 `extend` 方法把 `config`、`code`、`request`、`response`、`isAxiosError` 这些属性挂载到 `Error` 实例上时,并没有把这些属性设置为实例自身的属性,而是将其设置在了实例的 `__proto__` 上,如下: ![](~@/axios/05/02.png) 而 `Error` 实例上自身属性只有 `message` 和 `stack`,所以如果我们通过 `e.message` 访问的时候,访问的是 `Error` 类实例上的 `message` 属性,而我们再创建 `AxiosError` 类实例的时候并没有把 `message` 属性挂载到 `Error` 类实例上,而是挂载到了其 `__proto__` 上,所以访问 `e.message` 的时候返回的是 `undefined`。 那么该如何解决这个问题呢?我们只需要在 `createError` 方法中,在创建好 `error` 对象后,遍历我们所要挂载的对象,将对象上的每一个属性都设置为 `error` 对象自身的属性即可,如下: ```typescript // src/helpers/error.ts export function createError( message: string, config: AxiosRequestConfig, code?: string, request?: any, response?: AxiosResponse ): AxiosError { const error = new Error(message) as AxiosError; return extend(error, { config, code, request, response, isAxiosError: true, }); } ``` 改为: ```typescript // src/helpers/error.ts export function createError( message: string, config: AxiosRequestConfig, code?: string, request?: any, response?: AxiosResponse ): AxiosError { const error = new Error(message) as AxiosError; addProperties(error, { config, code, request, response, isAxiosError: true, }); return error; } function addProperties(target: any, source: any): void { for (const key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } } ``` 我们新增了 `addProperties` 工具方法,该方法用于遍历源对象,将源对象上的属性设置为目标对象自身的属性。然后在 `createError` 方法中创建好 `error` 对象后,调用 `addProperties` 方法将我们想要挂载的对象属性设置为 `error` 对象自身的属性。 这样,我们再访问 `e.message` 的时候就可以正常访问了。 # 6. 总结 本篇文章中,我们实现了 `axios` 的异常处理机制,并且编写了 `demo` 进行了测试。异常处理机制对于一个库来说至关重要,它能够帮助我们快速定位问题所在,所以必须要做好。 下篇文章,我们将会实现 `axios` 的接口扩展功能,敬请期待。

    栏目列表