特点:
1、集成了网络重连、防止重复网络请求、取消网络请求等功能
2、集成了JSON、application/x-www-form-urlencoded;charset=UTF-8、multipart/form-data;charset=UTF-8(文件上传)三种类型上传;
3、为一个项目使用多个服务端请求地址提供便利,通过生成多个实例对象实现
4、与TS深度结合
Tips:
这里贴一下对三种content-type的处理方式(代码片段,详情看下面的完整实现):
// interface.ts
/**
* @description: contentType
*/
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data qs
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data upload
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}
// axios.ts
supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers;
const contentType = headers?.["Content-Type"] || headers?.["content-type"];
if (
contentType !== ContentTypeEnum.FORM_URLENCODED ||
!Reflect.has(config, "data") ||
config.method?.toUpperCase() === RequestEnum.GET
) {
return config;
}
return {
...config,
data: qs.stringify(config.data, { arrayFormat: "brackets" }),
};
}
/*
* @description: 上传文件 FormData
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
const customFilename = params.name || "file";
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
} else {
formData.append(customFilename, params.file);
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key];
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, params.data![key]);
});
}
return this.axiosInstance.request<T>({
...config,
method: "POST",
data: formData,
headers: {
"Content-type": ContentTypeEnum.FORM_DATA,
// @ts-ignore
ignoreCancelToken: true,
},
});
}
完整的代码实现:
index.ts
import { VAxios } from "./axios";
import { CreateAxiosOptions } from "./axiosTransform";
import { ContentTypeEnum} from "./interface";
import { deepMerge } from './utils'
import { useGlobSetting } from "./globalSetting";
const globSetting = useGlobSetting()
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(
// 深度合并
deepMerge(
{
authenticationScheme: 'Bearer',
timeout: 10 * 1000,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 默认将prefix 添加到url
joinPrefix: true,
// 接口地址
apiUrl: globSetting.apiUrl,
// 接口拼接地址
urlPrefix: globSetting.urlPrefix,
// 忽略重复请求
ignoreCancelToken: true,
// 是否携带token
withToken: true,
// 接口错误重试(针对http错误码)
retryRequest: {
isOpenRetry: true,
count: 5,
waitTime: 100,
},
},
},
opt || {},
),
);
}
export const defHttp = createAxios();
axios.ts
import type {
AxiosRequestConfig,
AxiosInstance,
AxiosResponse,
AxiosError,
} from "axios";
import axios from "axios";
import qs from "qs";
import type { CreateAxiosOptions } from "./axiosTransform";
import {
ContentTypeEnum,
RequestEnum,
RequestOptions,
Result,
UploadFileParams,
} from "./interface";
import { isFunction, isString } from "./utils";
import { cloneDeep } from "lodash-es";
import { AxiosRetry } from "./AxiosRetry";
import { AxiosCanceler } from "./AxiosCancel";
declare type Recordable<T = any> = Record<string, T>;
export class VAxios {
private axiosInstance: AxiosInstance;
private readonly options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) {
this.options = options;
this.axiosInstance = axios.create(options);
this.setupInterceptors();
}
getAxios(): AxiosInstance {
return this.axiosInstance;
}
/**
* @description: 设置请求头
*/
setHeader(headers: any): void {
if (!this.axiosInstance) {
return;
}
Object.assign(this.axiosInstance.defaults.headers, headers);
}
/**
* @description: 拦截器配置
*/
private setupInterceptors() {
const axiosCanceler = new AxiosCanceler();
// 请求拦截器
this.axiosInstance.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 取消重复请求
// @ts-ignore
const { ignoreCancelToken } = config.requestOptions;
const ignoreCancel =
ignoreCancelToken !== undefined
? ignoreCancelToken
: this.options.requestOptions?.ignoreCancelToken;
!ignoreCancel && axiosCanceler.addPending(config);
// 请求之前处理config
const token = "";
if (
token &&
(config as Recordable)?.requestOptions?.withToken !== false
) {
// jwt token
(config as Recordable).headers.Authorization = this.options
.authenticationScheme
? `${this.options.authenticationScheme} ${token}` : token;
}
return config;
},
undefined
);
// 响应拦截器
this.axiosInstance.interceptors.response.use(
(res: AxiosResponse<any>) => {
res && axiosCanceler.removePending(res.config);
const responseData = res.data;
const { rtnCode } = responseData;
if (rtnCode !== "10000") {
return Promise.reject(responseData);
}
// 正常流程
return Promise.resolve(responseData);
},
(error) => {
const { config } = error || {};
if (axios.isCancel(error)) {
return Promise.reject(error);
}
// 添加自动重试机制 保险起见 只针对GET请求
const retryRequest = new AxiosRetry();
const { isOpenRetry } = config.requestOptions.retryRequest;
config.method?.toUpperCase() === RequestEnum.GET &&
isOpenRetry &&
// @ts-ignore
retryRequest.retry(this.axiosInstance, error);
return Promise.reject(error);
}
);
}
// support form-data Content-Type= application/x-www-form-urlencoded;charset=UTF-8
supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers;
const contentType = headers?.["Content-Type"] || headers?.["content-type"];
if (
contentType !== ContentTypeEnum.FORM_URLENCODED ||
!Reflect.has(config, "data") ||
config.method?.toUpperCase() === RequestEnum.GET
) {
return config;
}
return {
...config,
data: qs.stringify(config.data, { arrayFormat: "brackets" }),
};
}
get<T = any>(
config: AxiosRequestConfig,
options?: RequestOptions
): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
return this.request({ ...config, method: "GET" }, options);
}
post<T = any>(
config: AxiosRequestConfig,
options?: RequestOptions
): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
return this.request({ ...config, method: "POST" }, options);
}
put<T = any>(
config: AxiosRequestConfig,
options?: RequestOptions
): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
return this.request({ ...config, method: "PUT" }, options);
}
delete<T = any>(
config: AxiosRequestConfig,
options?: RequestOptions
): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
return this.request({ ...config, method: "DELETE" }, options);
}
/*
* @description: 上传文件
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
const customFilename = params.name || "file";
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
} else {
formData.append(customFilename, params.file);
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key];
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, params.data![key]);
});
}
return this.axiosInstance.request<T>({
...config,
method: "POST",
data: formData,
headers: {
"Content-type": ContentTypeEnum.FORM_DATA,
// @ts-ignore
ignoreCancelToken: true,
},
});
}
request<T = any>(
config: AxiosRequestConfig,
options?: RequestOptions
): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
let conf: CreateAxiosOptions = cloneDeep(config);
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { apiUrl, joinPrefix, urlPrefix } = opt;
if (joinPrefix) {
conf.url = `${urlPrefix}${conf.url}`;
}
if (apiUrl && isString(apiUrl)) {
conf.url = `${apiUrl}${conf.url}`;
}
conf.requestOptions = opt;
conf = this.supportFormData(conf);
return new Promise((resolve) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
const response = res as unknown as Result<T>;
resolve([undefined, response]);
})
.catch((e: Error | AxiosError) => {
// if (axios.isAxiosError(e)) {
// rewrite error message from axios in here
// }
resolve([e, undefined]);
});
});
}
}
axiosCancel.ts
import type { AxiosRequestConfig } from 'axios';
// 用于存储请求等待队列
let pendingMap = new Map<string, AbortController>();
// 生成map的key值(get&api形式)
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
export class AxiosCanceler {
/**
* 新增请求
* @param {Object} config
*/
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const url = getPendingUrl(config);
const controller = new AbortController()
config.signal =
config.signal ||
controller.signal
if (!pendingMap.has(url)) {
// If there is no current request in pending, add it
pendingMap.set(url, controller);
}
}
/**
* @description: 清除所有请求
*/
removeAllPending() {
pendingMap.forEach((cancel) => {
cancel && cancel.abort();
});
pendingMap.clear();
}
/**
* 清除目标请求
* @param {Object} config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// 取消在等待队列中的请求
const cancel = pendingMap.get(url);
cancel && cancel.abort();
pendingMap.delete(url);
}
}
/**
* @description: 重置
*/
reset(): void {
pendingMap = new Map<string, AbortController>();
}
}
axiostRetry.ts
import { AxiosError, AxiosInstance } from 'axios';
/**
* 请求重试机制
*/
export class AxiosRetry {
/**
* 重试
*/
retry(axiosInstance: AxiosInstance, error: AxiosError) {
// @ts-ignore
const { config } = error.response;
const { waitTime, count } = config?.requestOptions?.retryRequest;
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= count) {
return Promise.reject(error);
}
config.__retryCount += 1;
//请求返回后config的header不正确造成重试请求失败,删除返回headers采用默认headers
delete config.headers;
return this.delay(waitTime).then(() => axiosInstance(config));
}
/**
* 延迟
*/
private delay(waitTime: number) {
return new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
globSetting.ts
interface IGlobSetting {
apiUrl: string,
urlPrefix: string,
uploadUrl?: string,
}
const globSetting:Readonly<{development:IGlobSetting,production:IGlobSetting}> = Object.freeze({
development: {
apiUrl: '',
urlPrefix: '',
},
production: {
apiUrl: '',
urlPrefix: '',
}
})
export const useGlobSetting = ():Readonly<IGlobSetting> => {
// vite 提供的环境变量
// @ts-ignore
const env = import.meta.env.MODE as string
return globSetting[env]
}
interface.ts
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
export type SuccessMessageMode = ErrorMessageMode;
export interface RequestOptions {
// 默认将prefix 添加到url
joinPrefix?: boolean;
// 接口地址
apiUrl?: string;
// 请求拼接路径
urlPrefix?: string;
ignoreCancelToken?: boolean;
// 是否携带token
withToken?: boolean;
// 请求重试机制
retryRequest?: RetryRequest;
}
export interface RetryRequest {
isOpenRetry: boolean;
count: number;
waitTime: number;
}
export interface Result<T = any> {
code: number;
type: 'success' | 'error' | 'warning';
message: string;
result: T;
}
declare type Recordable<T = any> = Record<string, T>;
// multipart/form-data: upload file
export interface UploadFileParams {
// 其他数据
data?: Recordable;
// File parameter interface field name
name?: string;
// 文件
file: File | Blob;
// 文件名
filename?: string;
[key: string]: any;
}
/**
* @description: contentType
*/
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data qs
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data upload
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}
/**
* @description: request method
*/
export enum RequestEnum {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
}
utils.ts
import { cloneDeep } from 'lodash-es';
export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`;
}
export function isFunction(val: unknown): val is Function {
return typeof val === 'function';
}
export function isObject(val: any): val is Record<any, any> {
return val !== null && is(val, 'Object');
}
export function isString(val: unknown): val is string {
return is(val, 'String');
}
// 深度合并
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
let key: string;
const res: any = cloneDeep(src)
for (key in target) {
res[key] = isObject(res[key]) ? deepMerge(res[key], target[key]) : (res[key] = target[key]);
}
return res;
}