# 1. 前言

在大型项目开发中,基于模块化开发的思想,我们往往不会把所有的请求操作直接写入逻辑内,而是将所有的请求按照需求不同分门别类的统一放在一个地方,例如博主在手头的项目开发中,会在项目目录下新建一个叫做api的文件夹,在该文件夹内根据业务模块的不同放置不同的请求文件,例如,关于用户增删改查的请求会在api文件夹内新建一个user.ts文件,然后将四个请求放入该文件;关于日志增删改查的请求会放入log.ts文件中,如下所示:

├── api
  ├── user.ts    // 用户相关的请求
  ├── log.ts     // 日志相关的请求
  ...
1
2
3
4

而在每个文件中,也会把每个请求抽离成单独函数导出,供需要的地方调用,如在user.ts中:

// 获取用户
export function getUser() {
  return axios
    .get("/getuser")
    .then((res) => res.data)
    .catch((err) => console.error(err));
}

// 创建用户
export function createUser(data) {
  return axios
    .post("/createUser", data)
    .then((res) => res.data)
    .catch((err) => console.error(err));
}

// 删除用户
export function deleteUser() {
  return axios
    .delete("/deleteUser")
    .then((res) => res.data)
    .catch((err) => console.error(err));
}

// ...
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

这样做的好处是,所有请求能够被集中统一的管理起来,如果日后有变动也可以快速的找到。

前言说了这么多,还是没有引入正题,其实博主是想说:当我们发出请求后,我们最关心的一个是请求是否成功,另外一个就是返回的响应数据是不是我们想要的,我们能否预先定义一个期望返回的数据类型接口,然后看返回的响应数据能否匹配预先定义的接口,就能够得知返回的数据是不是我们想要的?答案当然是可以的。

# 2. 需求分析

我们之前给所有的请求响应都规定一个类型接口,我们规定,所有的请求返回的响应都应该包含以上几个部分,如下:

export interface AxiosResponse {
  data: any; // 服务端返回的数据
  status: number; // HTTP 状态码
  statusText: string; // 状态消息
  headers: any; // 响应头
  config: AxiosRequestConfig; // 请求配置对象
  request: any; // 请求的 XMLHttpRequest 对象实例
}
1
2
3
4
5
6
7
8

但是仅仅是这样还不够,我们还希望能够细化到返回的数据data上,例如,当我们获取用户时发出getUser请求时,我们希望返回的数据data应该是{name:'难凉热血',age:'18'}这样的,由于每个请求期望返回的data不尽相同,那么我们就应该在发出请求时带上我们想要的data类型接口,当数据data返回时去匹配我们所携带的接口看是否匹配得上,进而确保是我们想要的数据。

那么,这就要求我们上面定义的AxiosResponse接收一个泛型参数,这个参数就是返回数据data参数,如下:

export interface AxiosResponse<T = any> {
  data: T; // 服务端返回的数据
  status: number; // HTTP 状态码
  statusText: string; // 状态消息
  headers: any; // 响应头
  config: AxiosRequestConfig; // 请求配置对象
  request: any; // 请求的 XMLHttpRequest 对象实例
}
1
2
3
4
5
6
7
8

这里我们给 AxiosResponse 接口添加了泛型参数 TT=any 表示泛型的类型参数默认值为 any

有了这个泛型参数以后,我们在发送请求时就可以指定返回的data的类型了,如下:

// 获取用户api
export function getUser<T>() {
  return axios
    .get<ResponseData<T>>("/getuser")
    .then((res) => res.data)
    .catch((err) => console.error(err));
}

// 调用getUser发出请求

// 期望返回data的类型
interface User {
  name: string;
  age: number;
}

async function test() {
  // user 被推断出为
  // {
  //  data: { name: string, age: number },
  //  ...
  // }
  const user = await getUser<User>();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 3. 接口添加泛型参数

接下来,我们就为之前定义好的所有接口都加上泛型参数。

// src/types/index.ts

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
36
37

我们为 AxiosPromiseAxios 以及 AxiosInstance 接口都加上了泛型参数。我们可以看到这些请求的返回类型都变成了 AxiosPromise<T>,也就是 Promise<AxiosResponse<T>>,这样我们就可以从响应中拿到了类型 T 了。

OK,响应数据的泛型参数就已经添加好了,接下里,就可以编写demo来测试一下效果。

# 4. demo 编写

examples 目录下创建 addGenericityToAxiosResponse目录,在 addGenericityToAxiosResponse目录下创建 index.html:

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

接着再创建 app.ts 作为入口文件:

import axios from "../../src/axios";

interface User {
  name: string;
  age: number;
}

function getUser<T>() {
  return axios<T>("/api/getuser")
    .then((res) => res)
    .catch((err) => console.error(err));
}

async function userList() {
  const user = await getUser<User>();
  if (user) {
    console.log(user.data.name);
  }
}

userList();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

我们看到,在编写代码的时候,TypeScript已经可以帮我们推断出user中我们预先定义好的想要的数据了。

接着在 server/server.js 添加新的接口路由:

// 响应支持泛型
router.get("/api/getuser", function(req, res) {
  res.json({
    msg: "hello world",
    data: { name: "难凉热血", age: 18 },
  });
});
1
2
3
4
5
6
7

最后在根目录下的index.html中加上启动该demo的入口:

<li>
  <a href="examples/addGenericityToAxiosResponse"
    >addGenericityToAxiosResponse</a
  >
</li>
1
2
3
4
5

OK,我们在命令行中执行:

# 同时开启客户端和服务端
npm run server | npm start
1
2

接着我们打开 chrome 浏览器,访问 http://localhost:8000/ (opens new window) 即可访问我们的 demo 了,我们点击 addGenericityToAxiosResponse,通过F12network 部分我们可以看到请求已正常发出:

OK,让响应数据支持泛型就已经实现完毕了。