目录

🌘 实现接口拓展

# 实现接口拓展

# 需求分析

为了用户更加方便地使用 axios 发送请求,可以为所有支持请求方法扩展一些接口

  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])

如果使用这些方法,就不必在 config 中指定 urlmethoddata 这些属性了。

从需求上来看, axios 不再单单是一个方法,更像是一个混合对象,本身是一个方法,又有很多方法属性。

# 接口类型定义

根据需求分析,混合对象 axios 本身是一个函数,我们再实现一个包括它属性方法的类,然后把这个类的原型属性和自身属性再拷贝到 axios 上。

先给 axios 混合对象定义接口类型:

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
}

export interface AxiosInstance extends Axios {
  (config: AxiosRequestConfig): AxiosPromise
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

首先定义一个 Axios 类型接口,它描述了 Axios 类中的公共方法,接着定义了 AxiosInstance 接口继承 Axios ,它就是一个混合类型的接口。

另外 AxiosRequestConfig 类型接口中的 url 属性变成了可选属性。

export interface AxiosRequestConfig {
  url?: string
  // ... 
}
1
2
3
4

# 创建 Axios

创建一个 Axios 类,来实现接口定义的公共方法。创建了一个 core 目录,用来存放发送请求核心流程的代码。创建 Axios.ts 文件:

src/core/Axios.ts

import { AxiosPromise, AxiosRequestConfig, 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(
      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

其中 request 方法的功能和我们之前的 axios 函数功能是一致。 axios 函数的功能就是发送请求。

基于模块化编程的思想,我们把这部分功能抽出一个单独的模块,在 core 目录下创建 dispatchRequest 方法,把之前 axios.ts 的相关代码拷贝过去。

core/dispatchRequest.ts

import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from '../types'
import { buildURL } from '../helpers/url'
import { transformRequest, transformResponse } from '../helpers/data'
import { processHeaders } from '../helpers/headers'
import xhr from './xhr'

export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
  processConfig(config)
  return xhr(config).then(res => {
    return transformResponseData(res)
  })
}

function processConfig(config: AxiosRequestConfig): void {
  config.url = transformURL(config)
  config.headers = transformRequestHeader(config)
  config.data = transformRequestData(config)
}

function transformURL(config: AxiosRequestConfig): any {
  const { url, params } = config
  return buildURL(url, params)
}

function transformRequestData(config: AxiosRequestConfig): any {
  return transformRequest(config.data)
}

function transformRequestHeader(config: AxiosRequestConfig): any {
  const { headers = {}, data } = config
  return processHeaders(headers, data)
}

function transformResponseData(res: AxiosResponse): AxiosResponse {
  res.data = transformResponse(res.data)
  return res
}
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

回到 Axios.ts 文件,对于 getdeleteheadoptionspostpatchput 这些方法,都是对外提供的语法糖,内部都是通过调用 request 方法实现发送请求,只不过在调用之前对 config 做了一层合并处理。

# 混合对象实现

要实现混合对象,首先这个对象是一个函数,其次这个对象要包括 Axios 类的所有原型属性和实例属性,我们首先来实现一个辅助函数 extend

src/helpers/util.ts

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

extend 方法的实现使用了 交叉类型,并且用到了 类型断言。最终目的是把 from 里面的 属性拓展到 to 中,包括原型上的属性;

接下来对 axios.ts 文件修改,用工厂模式创建一个 axios 混合对象:

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(instance, context)

  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

createInstance 工厂函数的内部,首先实例化了 Axios 实例 context ,接着创建 instance 指向 Axios.prototype.request 方法,并绑定了上下文 context ;接着通过 extend 方法把 context 中的原型方法和实例方法全部拷贝到 instance 上,这样就实现了一个混合对象: instance 本身是一个函数,又拥有了 Axios 类的所有原型和实例属性,最终返回这个 instance

由于 TypeScript 不能正确推断 instance 的类型,所以需要断言成 AxiosInstance 类型。

这样就可以通过 createInstance 工厂函数创建 axios ,直接调用 axios 方法相当于 执行了 Axios 类的 request 方法发送请求,同样可以调用 axios.getaxios.post 等方法;

# 编写测试 DEMO

创建 DEMO 文件:

examples/extend/index.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <title>base example</title>
</head>
<body>
<script src='/__build__/base.js'></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10

examples/extend/app.ts

import axios from '../../src'

axios({
  url: '/extend/post',
  method: 'post',
  data: {
    msg: 'hi'
  }
})

axios.request({
  url: '/extend/post',
  method: 'post',
  data: {
    msg: 'hi'
  }
})


axios.get('/extend/get')

axios.options('/extend/options')

axios.delete('/extend/delete')

axios.head('/extend/head')

axios.post('/extend/post', { msg: 'post' })

axios.put('/extend/put', { msg: 'put' })

axios.patch('/extend/put', { msg: 'patch' })
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

# 实现 axios 函数重载

# 需求分析

目前的 axios 只支持传入一个参数,即 config

axios({
  url: '/extend/post',
  method: 'post',
  data: {
    msg: 'hi'
  }
})
1
2
3
4
5
6
7

如果要实现同时支持传入两个参数,第一个参数为 url 第二个参数为 config ,如:

axios('/extend/post', {
  method: 'post',
  data: {
    msg: 'hello'
  }
})
1
2
3
4
5
6

有点类似 axios.get 的参数类型,不同的是如果需要指定 HTTP 方法,仍然需要在 configmethod 中指定。

需要用到 函数的重载 的知识。

# 实现重载

修改 AxiosInstnace 的定义:

src/types/index.ts

export interface AxiosInstance extends Axios {
  (config: AxiosRequestConfig): AxiosPromise

  (url: string, config?: AxiosRequestConfig): AxiosPromise
}
1
2
3
4
5

增加一种函数的定义,支持两个参数,其中 url 是是必选参数, config 是可选参数。

由于 axios 函数实际上指向的是 request 函数,所以修改 request 函数。

src/core/Axios.ts

request(url: any, config?: any): AxiosPromise {
    if(typeof url === 'string') {
      if(!config) {
        config = {}
      }
      config.url = url
    } else {
      config = url
    }
    return dispatchRequest(config)
  }
1
2
3
4
5
6
7
8
9
10
11

request 函数的参数改成 2 个, urlconfig 都是 any 类型, config 还是可选参数。

接着在函数体中判断 url 是否为字符串类型,如果为字符串类型,则继续对 config 判断,如果判断 config 为空则构造空对象,将 url 添加到 config.url 。如果 url 不为字符串类型,则说明传入的就是单个参数( config ),且 url 就是 config ,因此把 url 赋值给 config

注意,虽然修改了 request 的实现,支持两个参数,但是对外提供的 request 接口仍然保持不变,可以理解为这仅仅是 内部的实现的修改,与外部接口不必一致,只要保留兼容接口即可。

# 编写测试 DEMO

axios({
  url: '/extend/post',
  method: 'post',
  data: {
    msg: 'hi'
  }
})

axios('/extend/post', {
  method: 'post',
  data: {
    msg: 'hello'
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 实现响应数据支持泛型

# 需求分析

通常情况,会将后端返回的数据格式单独放入一个接口中:

// 请求接口数据
export interface ResponseData<T = any> {
  /**
   * 状态码
   * @type { number }
   */
  code: number

  /**
   * 数据
   * @type { T }
   */
  result: T

  /**
   * 消息
   * @type { string }
   */
  message: string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

可以将 API 抽离成单独的模块,例如获取用户的信息:

import { ResponseData } from './interface.ts';

export function getUser<T>() {
  return axios.get<ResponseData<T>>('/somepath')
    .then(res => res.data)
    .catch(err => console.error(err))
}
1
2
3
4
5
6
7

接着写入返回的数据类型 User ,可以让 TypeScript 顺利推断出想要的类型:

interface User {
  name: string
  age: number
}

async function test() {
  // user 被推断出为
  // {
  //  code: number,
  //  result: { name: string, age: number },
  //  message: string
  // }
  const user = await getUser<User>()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

一般用于 Vue Store 中异步执行请求数据。

# 接口添加泛型参数

根据需求分析,需要给相关的接口定义添加泛型参数。

修改 type/index.tsdata 的类型:

export interface AxiosResponse<T = any> {
  data: T
  status: number
  statusText: string
  headers: any
  config: AxiosRequestConfig
  request: any
}

export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}

export interface Axios {
  request<T = any>(config: AxiosRequestConfig): AxiosPromise<T>

  get<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  delete<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  head<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  options<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>

  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>

  patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
}

export interface AxiosInstance extends Axios {
  <T = any>(config: AxiosRequestConfig): AxiosPromise<T>

  <T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
}
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

首先给 AxiosResponse 接口添加泛型参数 TT = any 表示泛型的参数默认值为 any

然后为 AxiosPromiseAxiosAxiosInstance 接口添加泛型参数。可以看到这些请求的返回类型都变成了 AxiosPromise<T> ,也就是 Promise<AxiosResponse<T>> 。这样就可以从响应中获取到类型 T 了。

# 编写测试 DEMO

examples/extend/app.ts

interface ResponseData<T = any> {
  code: number
  result: T
  message: string
}

interface User {
  name: string
  age: number
}

function getUser<T>() {
  return axios<ResponseData<T>>('/extend/user')
    .then(res => res.data)
    .catch(err => console.error(err))
}

async function test() {
  const user = await getUser<User>()
  if (user) {
    console.log(user.result)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

当调用 getUser<User> 时,相当于调用了 axios<ResponseData<User>> ,也就是传入给 axios 函数的类型 TResponseData<User> ;相当于返回值 AxiosPromise<T> 中的 T ,实际上也是 Promise<AxiosResponse<T>> 中的 T 的类型是 ResponseData<User> 。所以相应数据中的 data 类型就是 ResponseData<User> ,如下:

{
  code: number
  result: User
  message: string
}
1
2
3
4
5

这个也是 const user = await getUser<User>() 的返回值 user 的数据类型,所以 TypeScript 能正确推断出 user 的类型。

📢 上次更新: 2022/09/02, 10:18:16