前提

封装 Axios

请求响应拦截器(错误抛出的弹窗, 自定义处理)

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
58
59
60
61
62
63
64
65
66
67
// 请求拦截器(全局配置)
axios.interceptors.request.use(
(config: any) => {
// 这里可以做一些请求拦截,比如请求头携带 token

// @ts-ignore (防止下面报错)
// config.headers.Authorization = localStorage.getItem("token");

return config;
},
(error: AxiosError) => {
// 发送请求时出了点问题,比如网络错误 https://segmentfault.com/q/1010000020659252
console.log('请求发起错误 -- ${error.message}');
return Promise.reject(error);
}
);

// 响应拦截器(全局配置)
axios.interceptors.response.use(
(response: AxiosResponse<IResponseData>) => {
// status >= 200 && status < 300 (HTTP 成功)
const {
data: { code, msg },
config,
} = response;
const { isShowFailMsg: isShowFailToast, isThrowError } = config as IRequestOption; // 请求配置

if (code == 0) {
// 业务成功 (后端定义的成功)
// console.log("请求成功!");
} else {
// 业务失败 (后端定义的失败)
isShowFailToast && console.log(msg);
if (isThrowError) throw new Error(`后端返回的错误信息-- ${msg}`); // 抛出错误, 阻止程序向下执行 (默认配置)
}

return response;
},
(error: AxiosError) => {
// HTTP 失败
const { response, config } = error;
const { url, isShowFailMsg: isShowFailToast, isThrowError } = config as IRequestOption;

let errMsg = ''; // 错误信息

if (response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
const { status, data } = response as AxiosResponse;
errMsg = data.msg || `url:${(url || '').toString()},statusCode:${status}`;

if (status == 401) {
// 跳转登录
localStorage.clear();
setTimeout(() => {
window.location.href = '/login';
}, 2000);
}
} else {
// 请求已经成功发起,但没有收到响应
errMsg = '请求超时或服务器异常,请检查网络或联系管理员!';
}

isShowFailToast && console.log(errMsg);

return Promise.reject(isThrowError ? new Error(`请求失败 -- ${errMsg}`) : 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/** 自定义请求体 */
export interface IRequestData {
[key: string]: any;
}

/** 自定义响应体 */
export interface IResponseData<T = any> {
code: number;
msg: string;
data: T;
total?: number;
}

/** 自定义配置项 */
export interface IRequestOption extends Partial<AxiosRequestConfig<IRequestData>> {
/**
* 是否显示失败Toast弹框
* @default true
*/
isShowFailMsg?: boolean;

/**
* 是否抛出错误 (阻止代码的继续运行)
* @default true
*/
isThrowError?: boolean;
}

// 封装请求类
class Http {
defaultOptions: IRequestOption = {
// 自定义配置项默认值
isShowFailMsg: true,
isThrowError: true,
};

// 请求配置 https://www.axios-http.cn/docs/req_config
request(options: IRequestOption): Promise<AxiosResponse<IResponseData>> {
// 请求头中的 Content-Type , axios 默认会自动设置合适的 Content-Type
const withCredentials = true; // 是否携带cookie (放到实例配置中)
const { url: requestUrl, params: requestData } = this.transformParam(options, options.data, options.url || '');
const requestOptions = { ...this.defaultOptions, ...options };
const config = { withCredentials, url: requestUrl, data: requestData, ...requestOptions };
return axios.request(config);
}

/** 处理请求参数 */
transformParam(options: IRequestOption, param: any, url: string) {
if (options.method == 'GET' || options.method == 'DELETE') {
let paramStr = '';
for (const i in param) {
// 防止特殊字符
if (paramStr === '') paramStr += '?' + i + '=' + encodeURIComponent(param[i]);
else paramStr += '&' + i + '=' + encodeURIComponent(param[i]);
}
return { url: url + paramStr, params: {} };
} else {
return { url: url, params: param };
}
}
}

全部代码 [http.ts]

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import axios, { AxiosError } from 'axios';
import { AxiosRequestConfig, AxiosResponse } from 'axios';

/**
* 错误处理: https://www.axios-http.cn/docs/handling_errors
* ① 发送请求时出了点问题,比如网络错误
* ② 请求已经成功发起,但没有收到响应
* ③ 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
*/

// Axios全局配置 https://axios-http.com/docs/config_defaults
axios.defaults.baseURL = 'http://127.0.0.1:8082/api';

axios.interceptors.request.use(
(config: any) => {
// 这里可以做一些请求拦截,比如请求头携带 token

// @ts-ignore (防止下面报错)
// config.headers.Authorization = localStorage.getItem("token");

return config;
},
(error: AxiosError) => {
// 发送请求时出了点问题,比如网络错误 https://segmentfault.com/q/1010000020659252
console.log('请求发起错误 -- ${error.message}');
return Promise.reject(error);
}
);

// 响应拦截器(全局配置)
axios.interceptors.response.use(
(response: AxiosResponse<IResponseData>) => {
// status >= 200 && status < 300 (HTTP 成功)
const {
data: { code, msg },
config,
} = response;
const { isShowFailMsg: isShowFailToast, isThrowError } = config as IRequestOption; // 请求配置

if (code == 0) {
// 业务成功 (后端定义的成功)
// console.log("请求成功!");
} else {
// 业务失败 (后端定义的失败)
isShowFailToast && console.log(msg);
if (isThrowError) throw new Error(`后端返回的错误信息-- ${msg}`); // 抛出错误, 阻止程序向下执行 (默认配置)
}

return response;
},
(error: AxiosError) => {
// HTTP 失败
const { response, config } = error;
const { url, isShowFailMsg: isShowFailToast, isThrowError } = config as IRequestOption;

let errMsg = ''; // 错误信息

if (response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
const { status, data } = response as AxiosResponse;
errMsg = data.msg || `url:${(url || '').toString()},statusCode:${status}`;

if (status == 401) {
// 跳转登录
localStorage.clear();
setTimeout(() => {
window.location.href = '/login';
}, 2000);
}
} else {
// 请求已经成功发起,但没有收到响应
errMsg = '请求超时或服务器异常,请检查网络或联系管理员!';
}

isShowFailToast && console.log(errMsg);

return Promise.reject(isThrowError ? new Error(`请求失败 -- ${errMsg}`) : error);
}
);

/** 自定义请求体 */
export interface IRequestData {
[key: string]: any;
}

/** 自定义响应体 */
export interface IResponseData<T = any> {
code: number;
msg: string;
data: T;
total?: number;
}

/** 自定义配置项 */
export interface IRequestOption extends Partial<AxiosRequestConfig<IRequestData>> {
/**
* 是否显示失败Toast弹框
* @default true
*/
isShowFailMsg?: boolean;

/**
* 是否抛出错误 (阻止代码的继续运行)
* @default true
*/
isThrowError?: boolean;
}

// 封装请求类
class Http {
defaultOptions: IRequestOption = {
// 自定义配置项默认值
isShowFailMsg: true,
isThrowError: true,
};

// 请求配置 https://www.axios-http.cn/docs/req_config
request(options: IRequestOption): Promise<AxiosResponse<IResponseData>> {
// 请求头中的 Content-Type , axios 默认会自动设置合适的 Content-Type
// const withCredentials = true; // 是否携带cookie (放到实例配置中)
const { url: requestUrl, params: requestData } = this.transformParam(options, options.data, options.url || '');
const requestOptions = { ...this.defaultOptions, ...options };
const config = { withCredentials, url: requestUrl, data: requestData, ...requestOptions };
return axios.request(config);
}

/** 处理请求参数 */
transformParam(options: IRequestOption, param: any, url: string) {
if (options.method == 'GET' || options.method == 'DELETE') {
let paramStr = '';
for (const i in param) {
// 防止特殊字符
if (paramStr === '') paramStr += '?' + i + '=' + encodeURIComponent(param[i]);
else paramStr += '&' + i + '=' + encodeURIComponent(param[i]);
}
return { url: url + paramStr, params: {} };
} else {
return { url: url, params: param };
}
}
}

const http = new Http();
export default http;

封装 useSWR

根据官网的示例

1
2
3
4
5
6
7
8
import axios from 'axios';

const fetcher = (url) => axios.get(url).then((res) => res.data);

function App() {
const { data, error } = useSWR('/api/data', fetcher);
// ...
}

完整代码[index.ts]

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
import http, { IRequestOption, IResponseData } from '@/request/http';

/**
* 自定义axios参数类型
*/
export interface IReqOption extends Partial<IRequestOption> {
/**
* 是否启动请求
* @default true
*/
isReq?: boolean;
}

/**
* 自定义useSWR参数类型
*/
export interface ISwrOption extends Partial<SWRConfiguration> {
/**
* 是否禁用自动重新请求
* @default true
*/
isImmutable?: boolean;

/**
* 启用/禁用错误重试 (SWRConfiguration中官方属性)
* @default false
*/
shouldRetryOnError?: boolean;
}

/**
* 自定义请求返回数据类型
*/
export interface Return extends SWRResponse {
/**
* 请求返回数据
*/
repsonse?: IResponseData;

/**
* 请求key (全局 mutate 中使用)
*/
requestKey?: string | null;
}

/**
* swr请求封装
* @param reqtOption 请求参数
* @param swrOption swr配置
* @param axiosFunc 自定义axios请求函数
* @returns
*/
export default function useRequest(reqtOption: IRequestOption, swrOption: ISwrOption, axiosFunc?: () => any): Return {
const allReqOption = { isReq: true, ...reqtOption }; // 默认启动请求
const allSwrOption = { isImmutable: true, shouldRetryOnError: false, ...swrOption }; // 默认禁用自动重新请求, 禁用错误重试

let requestKey = null; // 生成请求key

if (axiosFunc) requestKey = matchURLMethod(axiosFunc?.toString()); // 取自定义axios请求函数的url和method作为请求key

if (allReqOption.isReq) requestKey = [allReqOption.url, allReqOption.method]; // 启动请求时, 取url和method作为请求key

if (allSwrOption.isImmutable) {
// 禁用自动重新请求
allSwrOption.revalidateIfStale = false;
allSwrOption.revalidateOnFocus = false;
allSwrOption.revalidateOnReconnect = false;
}

// 这里可以简写 { data, ...rest }
const { data, error, mutate, isLoading, isValidating } = useSWR(requestKey, axiosFunc || (() => http.request({ ...allReqOption })), { ...allSwrOption });

return { data, error, mutate, isLoading, isValidating, repsonse: data?.data, requestKey };
}

开始使用

创建请求

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
import axios from 'axios';
import useRequest, { IReqOption, ISwrOption } from '../request';

enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
}

/**
* 创建通用请求hook
* 不想每次都写 reqtOption?: IReqOption, swrOption?: ISwrOption 😅
*/
export const createApiHook = (defaultReq: IReqOption = {}, defaultSWR: ISwrOption = {}) => {
return (reqtOption?: IReqOption, swrOption?: ISwrOption) => {
return useRequest({ ...defaultReq, ...reqtOption }, { ...defaultSWR, ...swrOption });
};
};

// 原始用法1 eg: export const useTestApi = (reqtOption?: IReqOption, swrOption?: ISwrOption) => useRequest({ ...reqtOption }, { ...swrOption });

// 原始用法2 eg: export const useTestApi = (reqtOption?: IReqOption, swrOption?: ISwrOption) => useRequest({}, {}, axios.get(xxx));

// 自定义hook eg: export const useTestApi = createApiHook({ url: "/test", method: Method.GET }, { revalidateOnMount: false });

/** 测试 */
export const useTestApi = createApiHook({
url: '/hello.json',
method: Method.GET,
});

/** 用户登录 */
export const useLoginApi = createApiHook({
url: '/login.json',
method: Method.POST,
});

/** 使用 Axios 原始用法 */
export const useHitokotoApi = (reqtOption?: IReqOption, swrOption?: ISwrOption) => useRequest({}, {}, () => axios.get('https://api.wrdan.com/hitokoto'));

组件中使用

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import './App.css';
import { useState } from 'react';
import { useTestApi, useLoginApi, useHitokotoApi } from '../apis/index';

function App() {
const [tabId, setTabId] = useState(0);

const [user, setUser] = useState({
username: 'admin',
password: '123456',
});

const { repsonse: testData } = useTestApi();

/**
* 1. 页面渲染时不请求, 点击按钮 才会发起请求
* 2. 这里不能使用 { isReq: false }, isReq 把全局key置为null, 点击请求时 useSWR 找不到这个key, 导致无法请求
*/
const { mutate: loginMutate } = useLoginApi(
{}, // { data: { ...user } }, // 请求携带参数
{ revalidateOnMount: false }
);

/**
* 1. 切换tab 或 点击按钮 才会发起请求
* 2. repsonse 是最新的请求的值
*/
const { repsonse: hitokoto, mutate: hitokotoMutate } = useHitokotoApi({ isReq: tabId == 1, isShowFailMsg: false }, {});

const handleLogin = async () => {
// 这里可以直接使用 useLoginApi 的 repsonse, 参考 useHitokotoApi 的 repsonse
const {
data: { data },
}: any = await loginMutate();
setUser(data);
};

const handleRefresh = () => {
hitokotoMutate();
};

return (
<div className="page">
<div className="title">
<h2>{testData?.data}</h2>

<div className="btn" onClick={() => setTabId(1 - tabId)}>
切换
</div>
</div>

{tabId == 0 ? (
<div className="login">
<input type="text" placeholder="请输入账号" value={user.username} onChange={(e) => setUser({ ...user, username: e.target.value })} />
<input type="password" placeholder="请输入密码" value={user.password} onChange={(e) => setUser({ ...user, password: e.target.value })} />
<div className="btn" onClick={handleLogin}>
登录
</div>
</div>
) : (
<div className="hitokoto">
{hitokoto?.text && (
<div className="text">
{hitokoto?.text} 出自 {hitokoto?.source}
</div>
)}

<div className="btn" onClick={handleRefresh}>
刷新
</div>
</div>
)}
</div>
);
}

export default App;

项目示例