Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js 环境。它具有以下主要特点:
支持 Promise API,拦截请求和响应,转换请求和响应数据,自动转换 JSON 数据,客户端支持防御 XSRF,取消请求,超时处理,支持并发请求等
接下来我们对axios进行封装,以便在项目中使用
1.配置环境变量
在封装请求工具前,我们需要配置及导出环境变量
在项目根目录下创建.env文件,内容如下:
# api url
VITE_API_URL=https://dev.api.example.com
创建 src\config\configure.ts 文件,将环境变量导出
export const apiUrl = import.meta.env.VITE_API_URL
关于 vite .env 文件的说明和使用,可查看其它文章
2.安装 Axios
npm install axios
3.Axios类库
我们讲axios的创建封装为一个类,这样的好处是我们可以根据不同的情况创建多个axios实例
在 src\server\http\request\axios.ts
目录下创建axios类
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse, CreateAxiosDefaults } from 'axios'
// 获取请求ID
function uuid(config: AxiosRequestConfig): string {
if (config == undefined || config == null) {
return ''
}
const { method, url, params, data } = config;
return `${method}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
}
// 请求拦截器对象
interface RequestInterceptors<T> {
// 请求拦截
requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
requestInterceptorsCatch?: (err: any) => any
// 响应拦截
responseInterceptors?: (config: T) => T
responseInterceptorsCatch?: (err: any) => any
}
// 创建实例传递的参数
interface CreateRequestConfig<T = AxiosResponse> extends CreateAxiosDefaults {
interceptors?: RequestInterceptors<T>
}
/**
* 请求类
* 拦截器执行顺序 全局请求拦截 -> 实例请求拦截 -> 接口请求transformRequest处理 -> 接口响应transformResponse -> 实例响应 -> 全局响应
*/
class Request {
// axios 实例
instance: AxiosInstance
// 取消请求控制器
abortController: Map<string, AbortController>
constructor(config: CreateRequestConfig) {
this.instance = axios.create(config)
// 初始化存放取消请求控制器Map
this.abortController = new Map();
// 请求拦截-实例拦截器
this.instance.interceptors.request.use(
config.interceptors?.requestInterceptors,
config.interceptors?.requestInterceptorsCatch,
)
// 请求拦截-全局
this.instance.interceptors.request.use(
(req: InternalAxiosRequestConfig) => {
const controller = new AbortController()
req.signal = controller.signal
this.abortController.set(uuid(req), controller)
return req
},
(err: any) => {
return Promise.reject(err);
},
)
// 响应拦截-实例拦截器
this.instance.interceptors.response.use(
config.interceptors?.responseInterceptors,
config.interceptors?.responseInterceptorsCatch,
)
// 响应拦截-全局响应拦截器,保证最后执行
this.instance.interceptors.response.use(
(res: AxiosResponse) => {
this.abortController.delete(uuid(res.config))
return res
},
(err: any) => {
return Promise.reject(err);
},
)
}
/**
* 取消指定的请求
* @param url 待取消的请求URL
*/
cancelRequest(config: AxiosRequestConfig) {
const key = uuid(config);
this.abortController.get(key)?.abort()
this.abortController.delete(key)
}
/**
* 取消全部请求
*/
cancelAllRequest() {
for (const [, controller] of this.abortController) {
controller.abort()
}
this.abortController.clear()
}
}
export { Request }
export type { RequestInterceptors, CreateRequestConfig }
4.基于xhr的自定义Upload方法
在路径 src\server\http\request\upload.ts 下导出上传文件方法,该上传方法是基于xhr的,只适合小文件上传
// 上传策略
export interface UploadRequestPolicy {
// 服务类型,file:本地存储,aliyun:阿里云存储,tencent:腾讯云存储
service: string,
// 文件上传地址
uploadUrl: string,
// 前缀
prefix: string,
// 文件名称键值
key: string,
// 文件大小限制
maxSize: number,
// 隐藏文件后缀
hideExtension: number,
// 默认后缀处理参数
defaultExtensionProcessParam: string,
// 文件域名
accessFileDomain: string,
// 允许的文件后缀
allowFileExtension: FileExtension[]
}
// 上传文件请求配置
export interface UploadRequestConfig {
policy: UploadRequestPolicy,
onProgress: (percent: string, file: Blob | File, data: any, e: ProgressEvent<EventTarget>) => any,
fail: (code: string, data ? :any) => any,
success: (data: any) => any,
}
// 文件后缀
export interface FileExtension {
code: string,
mime: string,
extension: string
}
// 上传结果
export interface UploadResult {
ext: string | undefined,
key: string,
url: string
}
// 错误代码
export const UploadErrorCode = {
FileIsNullOrUndefined: 'FileIsNullOrUndefined',
FileSizeExceedsLimit: 'FileSizeExceedsLimit',
UnSupportedFileExtensionType: 'UnSupportedFileExtensionType',
CancelUpload: 'CancelUpload',
RequestTimeout: 'RequestTimeout',
UploadError: 'UploadError',
UploadFailedWithUnknownError: 'UploadFailedWithUnknownError',
UploadFailedAndReturnedContentIsEmpty: 'UploadFailedAndReturnedContentIsEmpty',
}
// 格式化文件大小
export function formatStorage(size: number) {
if (0 === size) {
return '0 B'
};
const k = 1024;
const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(size) / Math.log(k));
return (size / Math.pow(k, i)).toPrecision(3) + ' ' + unit[i];
}
function empty(v : any): boolean {
if (v == undefined || v == null || v === '') {
return true;
}
try {
const json = JSON.stringify(v);
if (json === '{}' || json === '[]') {
return true;
}
} catch (e) {}
return false;
}
function random(count : number): string {
return count + '';
}
// 文件上传方法
export function upload(file: Blob | File, conf: UploadRequestConfig) : XMLHttpRequest | undefined {
const config = {... {
policy: {
key: 'key',
service: '',
uploadUrl: '',
accessFileDomain: ''
},
onProgress: function() {},
fail: function() {},
success: function() {}
},
... {
...conf,
policy: {
...conf.policy
}
}
}
// 判断文件是否为空
if (file == null || file == undefined) {
config.fail(UploadErrorCode.FileIsNullOrUndefined);
return;
}
// 判断大小
if (file.size > config.policy.maxSize) {
config.fail(UploadErrorCode.FileSizeExceedsLimit, formatStorage(config.policy.maxSize));
return;
}
// 判断后缀,先判断扩展名,如果扩展名为空,则判断文件mime类型
let ext: string | undefined;
let fileExtension!: FileExtension;
if (file instanceof File && !empty(file.name)) {
ext = file.name.split('.').pop()?.toLowerCase();
for (let i = 0; i < config.policy.allowFileExtension.length; i++) {
const item = config.policy.allowFileExtension[i];
if (ext == item.extension) {
fileExtension = item;
break;
}
}
} else {
if (!empty(file.type)) {
for (let i = 0; i < config.policy.allowFileExtension.length; i++) {
const item = config.policy.allowFileExtension[i];
if (file['type'] == item['mime']) {
ext = item['extension'];
fileExtension = item;
break;
}
}
}
}
if (empty(ext) || fileExtension == undefined) {
config.fail(UploadErrorCode.UnSupportedFileExtensionType);
return;
}
// 新建表单
const formData = new FormData();
// 追加请求参数
formData.append('service', config.policy.service);
formData.append('key', config.policy.key);
formData.append('maxSize', `${config.policy.maxSize}`);
formData.append('accessFileDomain', config.policy.accessFileDomain);
// 重新命名文件
let suffix = '';
const mimeTypeCode = fileExtension.code;
if (1 != config.policy.hideExtension) {
suffix = '.' + ext;
}
const filePath = config.policy.prefix + mimeTypeCode + random(12) + suffix;
formData.append('k', filePath);
formData.append(config.policy.key, filePath);
// 追加上传文件
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', config.policy.uploadUrl, true);
// 监听上传进度
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = ((e.loaded / e.total) * 100).toFixed(2);
config.onProgress(percent, file, formData, e);
}
};
// 监听请求结果
xhr.onload = function() {
// 需要优化,根据不同的服务平台,分别判断
const res:UploadResult = {
ext: ext,
key: filePath,
url: config.policy.accessFileDomain + filePath + ((1 == config.policy.hideExtension) ? ('-' + config.policy.defaultExtensionProcessParam + '.' + ext) : ''),
};
const data = xhr.responseText;
// 返回data为空
if (empty(data)) {
if ('aliyun' == config.policy.service || 'tencent' == config.policy.service) {
config.success(res);
} else {
config.fail(UploadErrorCode.UploadFailedAndReturnedContentIsEmpty);
}
} else {
let parse = undefined;
try {
parse = JSON.parse(data)
} catch(e) {
console.log('Parse upload result json error:' + e);
}
if (typeof(parse) == 'undefined') {
config.fail(UploadErrorCode.UploadFailedWithUnknownError);
} else {
if (1 == parse.code) {
// 如果仓库类型为私有仓库,则必须使用服务器返回的url路径
config.success({
...res,
...parse.data.ext
});
} else {
config.fail(UploadErrorCode.UploadFailedWithUnknownError, empty(parse.info) ? "": parse.info);
}
}
}
};
// 取消上传
xhr.onabort = function() {
config.fail(UploadErrorCode.CancelUpload);
}
// 发生错误
xhr.onerror = function() {
config.fail(UploadErrorCode.UploadError);
}
// 请求超时
xhr.ontimeout = function() {
config.fail(UploadErrorCode.RequestTimeout);
}
// 发送请求
xhr.send(formData);
return xhr;
}
5.创建 Axios 实例,导出api请求方法
创建 src\server\http\index.ts 文件,在该文件内定义响应体规范,响应分页规范
import { Request } from './request/axios'
import { AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
import { apiUrl } from '@/config/configure'
import { upload, UploadRequestConfig } from './request/upload'
// 返回实体规范,参考ant design pro
export interface ResponseEntity<T> {
success: boolean;
data: T;
errorCode?: string; // code for errorType
errorMessage?: string; // message display to user
showType?: number; // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
host?: string; // onvenient for backend Troubleshooting: host of current access server
traceId?: string; // Convenient for back-end Troubleshooting: unique request ID
}
// 返回分页规范
export interface ResponsePageEntity<T> {
list: T[];
total?: number; // 数据总数
current?: number; // 当前页码
pageSize?: number; // 每页数量
}
// 实例化类
const server = new Request({
baseURL: apiUrl,
timeout: 1000 * 60 * 5,
interceptors: {
// 请求拦截器
requestInterceptors: (req: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('token') || 'token123456';
// 添加 token
req.headers = req.headers || {};
req.headers['x-access-token'] = token;
return req;
},
},
})
// 请求封装
const request = <D = any, T = any>(config: AxiosRequestConfig<D>): Promise<ResponseEntity<T>> => {
return new Promise((resolve, reject) => {
try {
return server.instance.request<ResponseEntity<T>>(config)
.then(res => {
// 因为我们接口的数据都在res.data下,所以我们直接返回res.data
resolve(res.data)
})
.catch((err) => {
reject(err)
})
} catch (err) {
return reject(err)
}
})
}
/* 导出封装的请求方法 */
const http = {
request: request,
get<D = any, T = any>(url: string, params?: D, config?: AxiosRequestConfig<D>): Promise<ResponseEntity<T>> {
return request<D, T>({ url: url, method: 'GET', params: params, ...config })
},
post<D = any, T = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<ResponseEntity<T>> {
return request<D, T>({ url: url, method: 'POST', data: data, ...config })
},
put<D = any, T = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<ResponseEntity<T>> {
return request<D, T>({ url: url, method: 'PUT', data: data, ...config })
},
delete<D = any, T = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<ResponseEntity<T>> {
return request<D, T>({ url: url, method: 'DELETE', data: data, ...config })
},
upload(file: Blob | File, config: UploadRequestConfig): XMLHttpRequest | undefined {
return upload(file, config);
},
cancelRequest(config: AxiosRequestConfig) {
return server.cancelRequest(config)
},
cancelAllRequest() {
return server.cancelAllRequest()
},
}
export { server, http };
6. 在组件中使用
创建api文件,在 src\apis\demo.ts 创建请求api接口
import { ResponseEntity, http } from "@/server/http";
// 请求类型
export interface GetUserInfoReq {
id: string
}
// 响应类型
export interface UserInfo {
id: number,
name: string,
subject: Subject[]
}
export interface Subject {
id: number,
name: string
}
// 返回类型 Promise<ResponseEntity<fetchOkResp>> 可以省略,因为可以自动推断出来
export const getUserInfo = async (req: string) => {
return http.get<{id: string}, UserInfo>(
"http://demo.com/api.php",
{
id: req
}
);
};
// 完整示例,增加transform拦截
export const getUserInfoWithTransform = async (
req: GetUserInfoReq
): Promise<ResponseEntity<UserInfo>> => {
return http.get<GetUserInfoReq, UserInfo>(
"http://demo.com/api.php?sleep=0",
req,
{
transformRequest: [
(data, headers) => {
data = data || {};
// 修改请求数据
data.age = 30; // 修改 age 字段
headers["token"] = "tokentokentokentoken"; // 修改请求头
headers["custom-lang"] = "zh-s"; // 修改请求头
return data; // 将对象转换为 JSON 字符串
},
],
transformResponse: [
(data) => {
// 修改响应数据
const parsedData = JSON.parse(data); // 将 JSON 字符串转换为对象
parsedData.message = "Hello, World!"; // 添加 message 字段
return parsedData;
},
],
}
);
};
在app组件中使用
import { RouterProvider } from 'react-router-dom';
import router from './routes';
import { useEffect, useState } from 'react';
import { getUserInfo, getUserInfoWithTransform, UserInfo } from './apis/demo';
function App() {
const [user, setUser] = useState<UserInfo|undefined>(undefined);
const [userWithTransform, setUserWithTransform] = useState<UserInfo|undefined>(undefined);
useEffect(() => {
getUserInfo('1').then((res) => {
console.log(res.data.name)
setUser(res.data)
})
getUserInfoWithTransform({
id: "1"
}).then((res) => {
console.log(res.data.name)
setUserWithTransform(res.data)
})
}, []);
return (
<>
<h1>Vite + React</h1>
<p>user: {JSON.stringify(user)}</p>
<p>userWithTransform: {JSON.stringify(userWithTransform)}</p>
<RouterProvider router={router} />
</>
);
}
export default App;