基于TypeScript从零重构axios(二)
# 基于TypeScript从零重构axios(二)
[TOC]
# 三、异常情况处理
处理 axios 请求的一些异常情况,包括网络错误、请求超时,构造一个信息增强错误处理类。
# 3.1 错误处理
# 3.1.1 需求分析
- 程序能捕获错误,做进一步的处理。
axios({
method: 'get',
url: '/error/get'
}).then((res) => {
console.log(res)
}).catch((e) => {
console.log(e)
})
2
3
4
5
6
7
8
- 如果在请求的过程中发生任何错误,可以在
reject
回调函数中捕获到。
# 3.1.2 处理
# 3.1.2.1 网络错误
- 当网络出现异常(比如不通)的时候发送请求会触发
XMLHttpRequest
对象实例的error
事件,于是可以在onerror
(opens new window) 的事件回调函数中捕获此类错误。
// src/xhr.ts
request.onerror = function handleError() {
reject(new Error('Network Error'))
}
2
3
4
# 3.1.2.2 超时错误
- 我们可以设置某个请求的超时时间
timeout
(opens new window),也就是当请求发送后超过某个时间后仍然没收到响应,则请求自动终止,并触发timeout
事件。 - 请求默认的超时时间是 0,即永不超时。现需要允许程序可以配置超时时间。
// src/types/index.ts
export interface AxiosRequestConfig {
// ...
timeout?: number
}
2
3
4
5
// src/xhr.ts
const { /*...*/ timeout } = config
if (timeout) {
request.timeout = timeout
}
request.ontimeout = function handleTimeout() {
reject(new Error(`Timeout of ${timeout} ms exceeded`))
}
2
3
4
5
6
7
8
9
10
# 3.1.2.3 非200状态码
- 对于一个正常的请求,往往会返回 200-300 之间的 HTTP 状态码或304状态码,对于不在这个区间的状态码,也作为一种错误的情况做处理。
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return
}
// 当出现网络错误或者超时错误的时候,该值都为 0
if (request.status === 0) {
return
}
// ...
handleResponse(response)
}
function handleResponse(response: AxiosResponse) {
if (response.status >= 200 && response.status < 300 || response.status === 304) {
resolve(response)
} else {
reject(new Error(`Request failed with status code ${response.status}`))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3.2 错误信息增强
# 3.2.1 需求分析
- 希望对外提供的信息不仅仅包含错误文本信息,还包括了请求对象配置
config
,错误代码code
,XMLHttpRequest
对象实例request
以及自定义响应对象response
。 - 方便应用方捕获到这些错误的详细信息,做进一步的处理。
# 3.2.2 创建AxiosError类
// src/types/index.ts
export interface AxiosError extends Error {
config: AxiosRequestConfig
code?: string
request?: any
response?: AxiosResponse
isAxiosError: boolean
}
2
3
4
5
6
7
8
// src/helpers/error.ts
import { AxiosRequestConfig, AxiosResponse } from '../types'
export class AxiosError extends Error {
isAxiosError: boolean
config: AxiosRequestConfig
code?: string | null
request?: any
response?: AxiosResponse
constructor(
message: string,
config: AxiosRequestConfig,
code?: string | null,
request?: any,
response?: AxiosResponse
) {
super(message)
this.config = config
this.code = code
this.request = request
this.response = response
this.isAxiosError = true
Object.setPrototypeOf(this, AxiosError.prototype)
}
}
// 对外暴露了一个 `createError` 的工厂方法,方便使用
export function createError(
message: string,
config: AxiosRequestConfig,
code?: string | null,
request?: any,
response?: AxiosResponse
): AxiosError {
const error = new AxiosError(message, config, code, request, response)
return error
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
使用了 Object.setPrototypeOf(this, AxiosError.prototype)
,是为了解决 TypeScript 继承一些内置对象的时候的坑,参考 (opens new window)。
# 3.2.3 createError 方法应用
// src/xhr.ts
import { createError } from './helpers/error'
request.onerror = function handleError() {
reject(createError(
'Network Error',
config,
null,
request
))
}
request.ontimeout = function handleTimeout() {
reject(createError(
`Timeout of ${config.timeout} ms exceeded`,
config,
'ECONNABORTED',
request
))
}
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
))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 3.2.4 导出类型定义
在 demo 中,TypeScript 并不能把
e
参数推断为AxiosError
类型,于是需要手动指明类型,为了让外部应用能引入AxiosError
类型,也需要把它们导出。创建
axios.ts
文件,把之前的index.ts
的代码拷贝过去,然后修改index.ts
的代码。
// src/index.ts
import axios from './axios'
export * from './types'
export default axios
2
3
4
5
6
这样就可以在servers部分引入AxiosError
类型了。
# 四、接口扩展
把 axios 从普通函数实现到通过工厂模式类的设计转变,并扩展更多调用接口,把 axios 变成了一个 混合对象,以及让响应数据支持泛型。
# 4.1 扩展接口
# 4.1.1 需求分析
为了用户更加方便地使用 axios 发送请求,可以为所有支持请求方法扩展一些接口。
axios.request(config)
- 发送请求,在config里指定请求方法。
axios.get(url[, config])
- GET请求会显示请求指定的资源,一般由于数据资源的读取操作,在url末尾可以追加查询字符串,请求的body中只能包含很少数据,依据浏览器和服务器的不同而不同。
axios.delete(url[, config])
- 删除服务器上某个资源。
axios.head(url[, config])
- HEAD与GET方法一样,只是不返回报文的主体部分,主要用于确认url的有效性及资源更新的日期时间。判断类型、查看响应中的状态码、测试资源是否被修改过、查看服务器性能。
axios.options(url[, config])
- 查询服务器支持的请求方法。
axios.post(url[, data[, config]])
- 主要用来向服务器新增数据,请求的主体理论上可以包含任意多数据。
axios.put(url[, data[, config]])
- 用来传输文件,在请求报文的主体内容中包含文件内容,保存到请求URL指定的位置,http1.1的PUT方法自身不带验证机制,任何人都可以请求,上传文件,会有安全问题。
axios.patch(url[, data[, config]])
- 与PUT请求类似,同样用于资源的更新,但有两点不同,一是PATCH一般用于资源的部分更新,而PUT用于资源的整体更新;二是当资源不存在时,PATCH会创建一个新的资源,而PUT只会对已存在的资源进行更新。
从需求上来看,
axios
不再单单是一个方法,更像是一个混合对象,本身是一个方法,又有很多方法属性。
# 4.1.2 接口类型定义
- 混合对象
axios
本身是一个函数,我们再实现一个包括它属性方法的类,然后把这个类的原型属性和自身属性再拷贝到axios
上。 - 给
axios
混合对象定义接口。
// src/types/index.ts
export interface Axios {
request(config: AxiosRequestConfig): AxiosPromise
get(url: string, config?: AxiosRequestConfig): AxiosPromise
delete(url: string, config?: AxiosRequestConfig): AxiosPromise
head(url: string, config?: AxiosRequestConfig): AxiosPromise
options(url: string, config?: AxiosRequestConfig): AxiosPromise
post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
}
// 混合类型接口 = 函数类型接口 + 继承属性方法
export interface AxiosInstance extends Axios {
(config: AxiosRequestConfig): AxiosPromise
}
export interface AxiosRequestConfig {
// 因为Axios接口的属性方法有url参数,就不一定要在config里加
url?: string
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 4.1.3 创建 Axios 类
- 创建了一个
core
目录,用来存放发送请求核心流程的代码。
// src/core/index.ts
import { AxiosRequestConfig, AxiosPromise, Method } from '../types'
import dispatchRequest from './dispatchRequest'
export default class Axios {
request(config: AxiosRequestConfig): AxiosPromise {
return dispatchRequest(config)
}
get(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('get', url, config)
}
delete(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('delete', url, config)
}
head(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('head', url, config)
}
options(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('options', url, config)
}
post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData('post', url, data, config)
}
put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData('put', url, data, config)
}
patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData('patch', url, data, config)
}
_requestMethodWithoutData(method: Method, url: string, config?: AxiosRequestConfig) {
return this.request(
// config || {} 避免没有config
Object.assign(config || {}, {
method,
url
})
)
}
_requestMethodWithData(method: Method, url: string, data?: any, config?: AxiosRequestConfig) {
return this.request(
Object.assign(config || {}, {
method,
url,
data
})
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
其中
request
方法的功能和之前的axios
函数功能是一致。axios
函数的功能就是发送请求,基于模块化编程的思想,把这部分功能抽出一个单独的模块。在
core
目录下创建dispatchRequest
方法,把之前axios.ts
的相关代码拷贝过去。
另外把
xhr.ts
文件也迁移到core
目录下。
// src/core/dispatchRequest.ts
// 移动后要修改文件路径
import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from '../types'
import xhr from './xhr'
import { buildURL } from '../helpers/url'
import { transformRequest, transformResponse } from '../helpers/data'
import { processHeaders } from '../helpers/headers'
// ...
function transformURL(config: AxiosRequestConfig): string {
const { url, params } = config
// config的url是可选属性,可能为null或undefined,但在bindURL函数中为必选参数
// url!:非空断言,非null和非undefined的类型断言
return buildURL(url!, params)
}
// ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 对于
get
、delete
、head
、options
、post
、patch
、put
这些方法,都是对外提供的语法糖,内部都是通过调用request
方法实现发送请求,只不过在调用之前对config
做了一层合并处理。
# 4.1.4 混合对象实现
- 混合对象实现思路很简单,首先这个对象是一个函数,其次这个对象要包括
Axios
类的所有原型属性和实例属性,首先来实现一个辅助函数extend
。
// src/helpers/util.ts
// 把 `from` 里的属性都扩展到 `to` 中,包括原型上的属性
// <T, U>是类型变量
export function extend<T, U>(to: T, from: U): T & U {
for (const key in from) {
// 因为用到括号,用';'表明一个新语句的开始
;(to as T & U)[key] = from[key] as any
}
return to as T & U
}
2
3
4
5
6
7
8
9
10
- 用工厂模式去创建一个
axios
混合对象。 - 通过
createInstance
工厂函数创建了axios
,当直接调用axios
方法就相当于执行了Axios
类的request
方法发送请求。
// src/axios.ts
import { AxiosInstance } from './types'
import Axios from './core/Axios'
import { extend } from './helpers/util'
function createInstance(): AxiosInstance {
const context = new Axios()
// 绑定上下文
const instance = Axios.prototype.request.bind(context)
// 通过 `extend` 方法把 `context` 中的原型方法和实例方法全部拷贝到 `instance` 上
extend(instance, context)
// `TypeScript` 不能正确推断 `instance` 的类型,故把它断言成 `AxiosInstance` 类型。
return instance as AxiosInstance
}
const axios = createInstance()
export default axios
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4.2 axios函数重载
# 4.2.1 需求分析
# 4.2.2 重载实现
# 4.3 响应数据支持泛型
# 4.3.1 需求分析
# 4.3.2 接口添加泛型参数
# 五、拦截器设计与实现
实现 axios 的拦截器功能,对整个实现做了详细的设计,最后实现拦截器管理类以及链式调用逻辑。