前言
本篇来封装一个axios,这部分很重要。
1、createAxios函数
新建/@/utils/http/index.ts文件,这里面用来存放核心的createAxios函数。
// /@/utils/http/index.ts
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(
// 深度合并
deepMerge(
{
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
// authentication schemes,e.g: Bearer
// authenticationScheme: 'Bearer',
authenticationScheme: '',
timeout: 10 * 1000,
// 基础接口地址
// baseURL: globSetting.apiUrl,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 如果是form-data格式
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
// 数据处理方式
transform: clone(transform),
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 默认将prefix 添加到url
joinPrefix: true,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformResponse: true,
// post请求的时候添加参数到url
joinParamsToUrl: false,
// 格式化提交参数时间
formatDate: true,
// 消息提示类型
errorMessageMode: 'message',
// 接口地址
apiUrl: globSetting.apiUrl,
// 接口拼接地址
urlPrefix: urlPrefix,
// 是否加入时间戳
joinTime: true,
// 忽略重复请求
ignoreCancelToken: true,
// 是否携带token
withToken: true,
retryRequest: {
isOpenRetry: true,
count: 5,
waitTime: 100,
},
},
},
opt || {},
),
);
}
export const defHttp = createAxios();
有很多爆红,下面逐个解析。
1.1、CreateAxiosOptions类型
新建/@/utils/http/axios/axiosTransform.ts文件作为数据处理类。
// /@/utils/http/axios/axiosTransform.ts
/**
* 数据处理类,可根据项目配置
*/
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import type { RequestOptions, Result } from '/#/axios';
export interface CreateAxiosOptions extends AxiosRequestConfig {
authenticationScheme?: string;
transform?: AxiosTransform;
requestOptions?: RequestOptions;
}
export abstract class AxiosTransform {
/**
* @description: 请求前的流程配置
*/
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
/**
* @description: 处理响应数据
*/
transformResponseHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
/**
* @description: 请求失败处理
*/
requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>;
/**
* @description: 请求之前的拦截器
*/
requestInterceptors?: (
config: InternalAxiosRequestConfig,
options: CreateAxiosOptions,
) => InternalAxiosRequestConfig;
/**
* @description: 请求之后的拦截器
*/
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
/**
* @description: 请求之前的拦截器错误处理
*/
requestInterceptorsCatch?: (error: Error) => void;
/**
* @description: 请求之后的拦截器错误处理
*/
responseInterceptorsCatch?: (axiosInstance: AxiosResponse, error: Error) => void;
}
从axiosTransform数据处理类里面可以看到后续要封装的各种函数处理。 接着去全局类型里补充缺失的类型,新建/@/types/axios.d.ts文件。
// /@/types/axios.d.ts
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
export type SuccessMessageMode = ErrorMessageMode;
export interface RequestOptions {
// 将请求参数拼接到url
joinParamsToUrl?: boolean;
// 格式化请求参数时间
formatDate?: boolean;
// 是否处理请求结果
isTransformResponse?: boolean;
// 是否返回本机响应标头
// 例如:当您需要获取响应标头时,请使用此属性
isReturnNativeResponse?: boolean;
// 是否加入url
joinPrefix?: boolean;
// 接口地址,如果保留为空,请使用默认的apiUrl
apiUrl?: string;
// 请求拼接路径
urlPrefix?: string;
// 错误消息提示类型
errorMessageMode?: ErrorMessageMode;
// 成功消息提示类型
successMessageMode?: SuccessMessageMode;
// 是否添加时间戳
joinTime?: boolean;
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;
}
// multipart/form-data: upload file
// 文件上传
export interface UploadFileParams {
// 其他参数
data?: Recordable;
// 文件参数接口字段名称
name?: string;
// 文件名
file: File | Blob;
// 文件名
filename?: string;
[key: string]: any;
}
import { CreateAxiosOptions } from './axios/axiosTransform'; 这样CreateAxiosOptions类型就ok了。
1.2、VAxios类
新建/@/utils/http/axios/index.ts文件作为axios模块类。
// /@/utils/http/axios/index.ts
import type {
AxiosRequestConfig,
InternalAxiosRequestConfig,
AxiosInstance,
AxiosResponse,
AxiosError,
} from 'axios';
import type { RequestOptions, Result, UploadFileParams } from '/#/axios';
import type { CreateAxiosOptions } from './axiosTransform';
import axios from 'axios';
import qs from 'qs';
import { AxiosCanceler } from './axiosCancel';
import { isFn } from '/@/utils/is';
import { cloneDeep } from 'lodash-es';
import { ContentTypeEnum } from '/@/enums/httpEnum';
import { RequestEnum } from '/@/enums/httpEnum';
export * from './axiosTransform';
/**
* @description: axios模块
*/
export class VAxios {
private axiosInstance: AxiosInstance;
private readonly options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) {
this.options = options;
this.axiosInstance = axios.create(options);
this.setupInterceptors();
}
/**
* @description: 创建axios实例
*/
private createAxios(config: CreateAxiosOptions): void {
this.axiosInstance = axios.create(config);
}
private getTransform() {
const { transform } = this.options;
return transform;
}
getAxios(): AxiosInstance {
return this.axiosInstance;
}
/**
* @description: 重新配置axios
*/
configAxios(config: CreateAxiosOptions) {
if (!this.axiosInstance) {
return;
}
this.createAxios(config);
}
/**
* @description: 设置通用header
*/
setHeader(headers: any): void {
if (!this.axiosInstance) {
return;
}
Object.assign(this.axiosInstance.defaults.headers, headers);
}
/**
* @description: 拦截器配置
*/
private setupInterceptors() {
const transform = this.getTransform();
if (!transform) {
return;
}
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch,
} = transform;
const axiosCanceler = new AxiosCanceler();
// 请求拦截器配置处理
this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// 如果取消重复请求已打开,则禁止取消重复请求
// @ts-ignore
const { ignoreCancelToken } = config.requestOptions;
const ignoreCancel =
ignoreCancelToken !== undefined
? ignoreCancelToken
: this.options.requestOptions?.ignoreCancelToken;
!ignoreCancel && axiosCanceler.addPending(config);
if (requestInterceptors && isFn(requestInterceptors)) {
config = requestInterceptors(config, this.options);
}
return config;
}, undefined);
// 请求拦截器错误捕获
requestInterceptorsCatch &&
isFn(requestInterceptorsCatch) &&
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
// 响应结果拦截器处理
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
res && axiosCanceler.removePending(res.config);
if (responseInterceptors && isFn(responseInterceptors)) {
res = responseInterceptors(res);
}
return res;
}, undefined);
// 响应结果拦截器错误捕获
responseInterceptorsCatch &&
isFn(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, (error) => {
// @ts-ignore
return responseInterceptorsCatch(this.axiosInstance, error);
});
}
/**
* @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,
},
});
}
// 支持form-data数据格式
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<T> {
return this.request({ ...config, method: 'GET' }, options);
}
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'POST' }, options);
}
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PUT' }, options);
}
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'DELETE' }, options);
}
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config);
// cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求
if (config.cancelToken) {
conf.cancelToken = config.cancelToken;
}
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {};
if (beforeRequestHook && isFn(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
conf.requestOptions = opt;
conf = this.supportFormData(conf);
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
if (transformResponseHook && isFn(transformResponseHook)) {
try {
const ret = transformResponseHook(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('request error!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error | AxiosError) => {
if (requestCatchHook && isFn(requestCatchHook)) {
reject(requestCatchHook(e, opt));
return;
}
if (axios.isAxiosError(e)) {
// 在此处重写axios的错误消息
}
reject(e);
});
});
}
}
可以看到这个类做了axios.create()函数创建axios实例,引入axios配置参数,注册了请求拦截器、响应拦截器,支持form-data数据格式,封装get、post、put、delete这些Promise请求,基本的request函数这些。里面还有文件上传这部分,不过是最基本的配置,后面需要根据实际情况修改。 安装一下qs插件。
// package.json
"qs": "^6.11.2",
import { VAxios } from './axios'; 这样VAxios类就ok了。
1.3、deepMerge递归合并函数
这个函数在前面utils函数篇添加过。
//
/**
递归合并两个对象。
@param target 目标对象,合并后结果存放于此。
@param source 要合并的源对象。
@returns 合并后的对象。
*/
export function deepMerge<T extends object | null | undefined, U extends object | null | undefined>(
target: T,
source: U,
): T & U {
return mergeWith(cloneDeep(target), source, (objValue, srcValue) => {
if (isObj(objValue) && isObj(srcValue)) {
return mergeWith(cloneDeep(objValue), srcValue, (prevValue, nextValue) => {
return isArr(prevValue) ? prevValue.concat(nextValue) : undefined;
});
}
});
}
简单解释就是,deepMerge可以把{ a: 1,b: 2 }和{ c: 3 }合并为{ a: 1,b: 2,c: 3 }。 import { deepMerge } from '/@/utils';
1.4、ContentTypeEnum枚举。
这个枚举在前面vite配置篇里面配置过。 import { ContentTypeEnum,} from '/@/enums/httpEnum';
1.5、useGlobSetting函数
useXXX这种格式的被称作hook函数,新建/@/hooks/settings/index.ts文件存放。
// /@/hooks/settings/index.ts
import type { GlobConfig } from '/#/config';
import { warn } from '/@/utils/log';
import { getENV } from '/@/utils/env';
// 全局环境变量
export const useGlobSetting = (): Readonly<GlobConfig> => {
const ENV = getENV();
const {
VITE_GLOB_APP_TITLE,
VITE_GLOB_API_URL,
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
} = ENV;
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
warn(`VITE_GLOB_APP_SHORT_NAME变量只能是字符/下划线,请在环境变量中修改并重新运行.`);
}
// 采用全局配置
const glob: Readonly<GlobConfig> = {
title: VITE_GLOB_APP_TITLE,
apiUrl: VITE_GLOB_API_URL,
shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL,
};
return glob as Readonly<GlobConfig>;
};
这里补充一下GlobConfig类型,去config.d.ts文件里添加。
// config.d.ts
export interface GlobConfig {
// 站点名称
title: string;
// 服务接口url
apiUrl: string;
// 上传url
uploadUrl?: string;
// 服务接口url前缀
urlPrefix?: string;
// 项目简称
shortName: string;
}
从useGlobSetting函数里可以取到整个项目的glob配置。
// /@/utils/http/index.ts
...
import { useGlobSetting } from '/@/hooks/setting';
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix;
...
这样globSetting和urlPrefix就可以使用了。
2、AxiosTransform配置参数
前面createAxios创建axios实例函数里的transform还没有添加,这里实际上使用的就是AxiosTransform配置参数。
// /@/utils/http/index.ts
...
/**
* @description: 数据处理,方便区分多种处理方式
*/
const transform: AxiosTransform = {
/**
* @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误
*/
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { isTransformResponse, isReturnNativeResponse } = options;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
if (!isTransformResponse) {
return res.data;
}
const { data } = res;
if (!data) {
throw new Error('[HTTP] Request has no return value');
}
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { result } = data;
return result;
},
// 请求之前处理config
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
if (joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}
if (apiUrl && isStr(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const params = config.params || {};
const data = config.data || false;
formatDate && data && !isStr(data) && formatRequestDate(data);
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isStr(params)) {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
} else {
// 兼容restful风格
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
config.params = undefined;
}
} else {
if (!isStr(params)) {
formatDate && formatRequestDate(params);
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length > 0 || config.data instanceof FormData)
) {
config.data = data;
config.params = params;
} else {
// 非GET请求如果没有提供data,则将params视为data
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data),
);
}
} else {
// 兼容restful风格
config.url = config.url + params;
config.params = undefined;
}
}
return config;
},
/**
* @description: 请求拦截器处理
*/
requestInterceptors: (config, _options) => {
return config;
},
/**
* @description: 响应拦截器处理
*/
responseInterceptors: (res: AxiosResponse<any>) => {
return res;
},
/**
* @description: 响应错误处理
*/
responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addAjaxErrorInfo(error);
const { response, code, message, config } = error || {};
const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
const msg: string = response?.data?.error?.message ?? '';
const err: string = error?.toString?.() ?? '';
let errMessage = '';
if (axios.isCancel(error)) {
return Promise.reject(error);
}
try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
errMessage = '[HTTP] Timeout';
}
if (err?.includes('Network Error')) {
errMessage = '[HTTP] network exception';
}
// 后续根据需要添加message组件
if (errMessage) {
if (errorMessageMode === 'modal') {
throw new Error(errMessage);
} else if (errorMessageMode === 'message') {
throw new Error(errMessage);
}
return Promise.reject(error);
}
} catch (error) {
throw new Error(error as unknown as string);
}
// 根据请求状态做出对应的提示
checkStatus(error?.response?.status, msg, errorMessageMode);
// 添加自动重试机制 保险起见 只针对GET请求
const retryRequest = new AxiosRetry();
const { isOpenRetry } = config.requestOptions.retryRequest;
config.method?.toUpperCase() === RequestEnum.GET &&
isOpenRetry &&
// @ts-ignore
retryRequest.retry(axiosInstance, error);
return Promise.reject(error);
},
};
...
引入也更新一下。
// /@/utils/http/index.ts
import { AxiosTransform, CreateAxiosOptions } from './axios/axiosTransform';
import { VAxios } from './axios';
import { deepMerge, setObjToUrlParams } from '/@/utils';
import { ContentTypeEnum, RequestEnum } from '/@/enums/httpEnum';
import { useGlobSetting } from '/@/hooks/setting';
import { isStr } from '../is';
import { AxiosResponse } from 'axios';
import { RequestOptions, Result } from '/#/axios';
import { useErrorLogStoreWithOut } from '/@/stores/modules/errorLog';
import axios from 'axios';
此时还是有一些爆红,接下来逐项解析。
2.1、helper工具函数
AxiosTransform里面使用了两个工具函数joinTimestamp和formatRequestDate,它们都是用来处理参数拼接的函数。 新建/@/utils/http/helper.ts文件。
// /@/utils/http/helper.ts
import { isObj, isStr } from '/@/utils/is';
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
/**
* @description: 当前时间拼接
*/
export function joinTimestamp<T extends boolean>(
join: boolean,
restful: T,
): T extends true ? string : object;
export function joinTimestamp(join: boolean, restful = false): string | object {
if (!join) {
return restful ? '' : {};
}
const now = new Date().getTime();
if (restful) {
return `?_t=${now}`;
}
return { _t: now };
}
/**
* @description: 格式化请求参数时间
*/
export function formatRequestDate(params: Recordable) {
if (Object.prototype.toString.call(params) !== '[object Object]') {
return;
}
for (const key in params) {
const format = params[key]?.format ?? null;
if (format && typeof format === 'function') {
params[key] = params[key].format(DATE_TIME_FORMAT);
}
if (isStr(key)) {
const value = params[key];
if (value) {
try {
params[key] = isStr(value) ? value.trim() : value;
} catch (error: any) {
throw new Error(error);
}
}
}
if (isObj(params[key])) {
formatRequestDate(params[key]);
}
}
}
joinTimestamp函数是将当前时间拼接到axios参数里面,formatRequestDate是将时间转为YYYY-MM-DD HH:mm:ss格式,这里根据后续实际情况修改。 import { formatRequestDate, joinTimestamp } from './helper';
2.2、根据接口状态做出对应提示
新建/@/utils/http/axios/checkStatus.ts文件。
// /@/utils/http/axios/checkStatus.ts
import type { ErrorMessageMode } from '/#/axios';
import projectSetting from '/@/settings/projectSetting';
import { SessionTimeoutProcessingEnum } from '/@/enums/appEnum';
const stp = projectSetting.sessionTimeoutProcessing;
export function checkStatus(
status: number,
msg: string,
errorMessageMode: ErrorMessageMode = 'message',
): void {
let errMessage = '';
switch (status) {
case 400:
errMessage = `${msg}`;
break;
case 401:
// 401: 未登录
if (stp === SessionTimeoutProcessingEnum.PAGE_COVERAGE) {
// 如果超时,则根据实际情况处理超时的情况
} else {
// 如果未登录,则跳转到登录页面,并携带当前页面的路径
// 登录成功后返回当前页面。此步骤需要在登录页面上进行操作.
}
break;
// 下面可以根据实际情况处理
case 403:
errMessage = '[HTTP] 403';
break;
case 404:
errMessage = '[HTTP] 404';
break;
case 500:
errMessage = '[HTTP] 407';
break;
default:
}
// 后续根据需要添加message组件
if (errMessage) {
if (errorMessageMode === 'modal') {
throw new Error(errMessage);
} else if (errorMessageMode === 'message') {
throw new Error(errMessage);
}
}
}
2.3、axios自动重试机制
保险起见 只针对GET请求。 新建/@/utils/http/axios/axiosRetry.ts文件。
// /@/utils/http/axios/axiosRetry.ts
import { AxiosInstance } from 'axios';
/**
* 请求重试机制
*/
export class AxiosRetry {
/**
* 重试
*/
retry(axiosInstance: AxiosInstance, error: any) {
// @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));
}
}
这里逻辑比较复杂,实际上就是count控制重试次数,waitTime控制重试间隔,isOpenRetry控制是否重试。
完整代码
// /@/utils/http/index.ts
import { AxiosTransform, CreateAxiosOptions } from './axios/axiosTransform';
import { VAxios } from './axios';
import { deepMerge, setObjToUrlParams } from '/@/utils';
import { ContentTypeEnum, RequestEnum } from '/@/enums/httpEnum';
import { useGlobSetting } from '/@/hooks/setting';
import { isStr } from '../is';
import { AxiosResponse } from 'axios';
import { RequestOptions, Result } from '/#/axios';
import { useErrorLogStoreWithOut } from '/@/stores/modules/errorLog';
import axios from 'axios';
import { formatRequestDate, joinTimestamp } from './helper';
import { checkStatus } from './axios/checkStatus';
import { clone } from 'lodash-es';
import { AxiosRetry } from './axios/axiosRetry';
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix;
/**
* @description: 数据处理,方便区分多种处理方式
*/
const transform: AxiosTransform = {
/**
* @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误
*/
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { isTransformResponse, isReturnNativeResponse } = options;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
if (!isTransformResponse) {
return res.data;
}
const { data } = res;
if (!data) {
throw new Error('[HTTP] Request has no return value');
}
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { result } = data;
return result;
},
// 请求之前处理config
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
if (joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}
if (apiUrl && isStr(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const params = config.params || {};
const data = config.data || false;
formatDate && data && !isStr(data) && formatRequestDate(data);
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isStr(params)) {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
} else {
// 兼容restful风格
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
config.params = undefined;
}
} else {
if (!isStr(params)) {
formatDate && formatRequestDate(params);
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length > 0 || config.data instanceof FormData)
) {
config.data = data;
config.params = params;
} else {
// 非GET请求如果没有提供data,则将params视为data
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data),
);
}
} else {
// 兼容restful风格
config.url = config.url + params;
config.params = undefined;
}
}
return config;
},
/**
* @description: 请求拦截器处理
*/
requestInterceptors: (config, _options) => {
return config;
},
/**
* @description: 响应拦截器处理
*/
responseInterceptors: (res: AxiosResponse<any>) => {
return res;
},
/**
* @description: 响应错误处理
*/
responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addAjaxErrorInfo(error);
const { response, code, message, config } = error || {};
const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
const msg: string = response?.data?.error?.message ?? '';
const err: string = error?.toString?.() ?? '';
let errMessage = '';
if (axios.isCancel(error)) {
return Promise.reject(error);
}
try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
errMessage = '[HTTP] Timeout';
}
if (err?.includes('Network Error')) {
errMessage = '[HTTP] network exception';
}
// 后续根据需要添加message组件
if (errMessage) {
if (errorMessageMode === 'modal') {
throw new Error(errMessage);
} else if (errorMessageMode === 'message') {
throw new Error(errMessage);
}
return Promise.reject(error);
}
} catch (error) {
throw new Error(error as unknown as string);
}
// 根据请求状态做出对应的提示
checkStatus(error?.response?.status, msg, errorMessageMode);
// 添加自动重试机制 保险起见 只针对GET请求
const retryRequest = new AxiosRetry();
const { isOpenRetry } = config.requestOptions.retryRequest;
config.method?.toUpperCase() === RequestEnum.GET &&
isOpenRetry &&
// @ts-ignore
retryRequest.retry(axiosInstance, error);
return Promise.reject(error);
},
};
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(
// 深度合并
deepMerge(
{
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
// authentication schemes,e.g: Bearer
// authenticationScheme: 'Bearer',
authenticationScheme: '',
timeout: 10 * 1000,
// 基础接口地址
// baseURL: globSetting.apiUrl,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 如果是form-data格式
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
// 数据处理方式
transform: clone(transform),
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 默认将prefix 添加到url
joinPrefix: true,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformResponse: true,
// post请求的时候添加参数到url
joinParamsToUrl: false,
// 格式化提交参数时间
formatDate: true,
// 消息提示类型
errorMessageMode: 'message',
// 接口地址
apiUrl: globSetting.apiUrl,
// 接口拼接地址
urlPrefix: urlPrefix,
// 是否加入时间戳
joinTime: true,
// 忽略重复请求
ignoreCancelToken: true,
// 是否携带token
withToken: true,
retryRequest: {
isOpenRetry: true,
count: 5,
waitTime: 100,
},
},
},
opt || {},
),
);
}
export const defHttp = createAxios();
3、接口示例
正好之前使用了mock,来演示一下怎么使用mock。 新建/@/api/demo/index.ts文件。
// /@/api/demo/index.ts
import { DemoParams, DemoResultModel } from './model/listModel';
import { ErrorMessageMode } from '/#/axios';
import { defHttp } from '/@/utils/http';
enum Api {
GETlISTINFO = '/getListInfo',
POSTLISTINFO = '/postListInfo',
}
/**
* @description: 示例get请求
*/
export function getListInfo(params: DemoParams, mode: ErrorMessageMode = 'message') {
return defHttp.get<DemoResultModel>({ url: Api.GETlISTINFO, params }, { errorMessageMode: mode });
}
/**
* @description: 示例post请求
*/
export function postListInfo(params: DemoParams, mode: ErrorMessageMode = 'message') {
return defHttp.get<DemoResultModel>(
{ url: Api.POSTLISTINFO, params },
{ errorMessageMode: mode },
);
}
新建/@/api/demo/model/listModel.ts文件
// /@/api/demo/model/listModel.ts
export interface ListType {
id: number;
name: string;
createBy: string;
}
/**
* @description: 示例接口返回值
*/
export interface DemoResultModel {
content: ListType[];
page: number;
pageSize: number;
total: number;
}
/**
* @description: 示例接口参数
*/
export interface DemoParams {
field1: string;
field2: string;
}
修改一下mock文件。
// mock/demo/list.ts
import { resultSuccess, requestParams, baseUrl } from '../_util';
import { MockMethod } from 'vite-plugin-mock';
const listInfo = {
content: [
{
id: 1,
name: '示例数据1',
createBy: '示例数据创建人1',
},
{
id: 2,
name: '示例数据2',
createBy: '示例数据创建人2',
},
{
id: 3,
name: '示例数据3',
createBy: '示例数据创建人3',
},
{
id: 4,
name: '示例数据4',
createBy: '示例数据创建人4',
},
{
id: 5,
name: '示例数据5',
createBy: '示例数据创建人5',
},
{
id: 6,
name: '示例数据6',
createBy: '示例数据创建人6',
},
],
page: 1,
pageSize: 10,
total: 100,
};
export default [
{
url: `${baseUrl}/getListInfo`,
timeout: 1000,
method: 'get',
response: (request: requestParams) => {
const res: any = {
...request,
...listInfo,
};
return resultSuccess(res);
},
},
{
url: `${baseUrl}/postListInfo`,
timeout: 1000,
method: 'post',
response: (request: requestParams) => {
const res: any = {
...request,
...listInfo,
};
return resultSuccess(res);
},
},
] as MockMethod[];
然后随便找个文件调用一下, 顺便删除多余代码。
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { getListInfo } from '../api/demo';
const res = ref();
const demoParams = reactive({
field1: 'field1',
field2: 'field2',
});
async function getInfo() {
const data = await getListInfo(demoParams);
res.value = data;
}
onMounted(() => {
getInfo();
});
</script>
<template>
<div>{{ res }}</div>
</template>
也可以mock一些error接口,get请求会重复5次,重复请求次数在上面的代码里就能找到。
结语
至此,整个vue3通用项目底座完成。源码参考common-template。