基于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)
})
1
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'))
}
1
2
3
4
# 3.1.2.2 超时错误
  • 我们可以设置某个请求的超时时间 timeout (opens new window),也就是当请求发送后超过某个时间后仍然没收到响应,则请求自动终止,并触发 timeout 事件。
  • 请求默认的超时时间是 0,即永不超时。现需要允许程序可以配置超时时间。
// src/types/index.ts
export interface AxiosRequestConfig {
    // ...
    timeout?: number
}
1
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`))
}
1
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}`))
    }
}
1
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,错误代码 codeXMLHttpRequest 对象实例 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
}
1
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
}
1
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
        ))
    }
}
1
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
1
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
    // ...
}
1
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
      })
    )
  }
}
1
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)
}

// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 对于 getdeleteheadoptionspostpatchput 这些方法,都是对外提供的语法糖,内部都是通过调用 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
}
1
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
1
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 的拦截器功能,对整个实现做了详细的设计,最后实现拦截器管理类以及链式调用逻辑。

# 5.1 拦截器设计

# 5.1.1 需求分析

# 5.1.2 整体设计

# 5.2 拦截器实现

# 5.2.1 拦截器管理类实现

# 5.2.2 链式调用实现