206 Star 10K Fork 708

VTJ.PRO/VTJ

加入 Gitee
与超过 1400万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
request.ts 14.71 KB
一键复制 编辑 原始数据 按行查看 历史
“chenhuachun” 提交于 2026-04-22 23:53 +08:00 . fix: 🐛 API代理
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
import { ref, type Ref } from 'vue';
import axios from 'axios';
import type {
AxiosInstance,
CreateAxiosDefaults,
AxiosResponse,
AxiosRequestConfig,
RawAxiosRequestHeaders,
InternalAxiosRequestConfig,
CancelTokenSource
} from 'axios';
import {
merge,
omit,
debounce,
throttle,
uuid,
pathToRegexpCompile,
isUrl
} from '@vtj/base';
const TYPES = {
form: 'application/x-www-form-urlencoded',
json: 'application/json',
data: 'multipart/form-data'
};
const DATA_METHODS = ['put', 'post', 'patch'];
const LOCAL_REQUEST_ID = 'Local-Request-Id';
const LOADING_DELAY = 100;
const ERROR_DELAY = 300;
const PROXY_PATH = '/api/proxy';
export interface IRequestSkipWarn {
// 处理程序
executor: (
resolve: (value: unknown) => void,
reject: (reason?: any) => void
) => void;
// 响应编码
code: string | number;
// 参数名称
name?: string;
// 响应数据成功回调
callback?: (res: AxiosResponse) => void;
// 请求完成回调
complete?: () => void;
}
export interface IRequestSkipWarnResponse extends AxiosResponse {
promise: any;
}
export interface IResultWrapper<T = any> {
code: number;
data: T;
msg: string;
success: boolean;
}
export type RequestOriginResponse<R = any, D = any> = AxiosResponse<
IResultWrapper<R>,
D
>;
export interface IRequestSettings {
/**
* 发送数据类型
*/
type?: 'form' | 'json' | 'data';
/**
* 是否注入自定义的请求头
*/
injectHeaders?: boolean;
/**
* 自定义请求头
*/
headers?:
| RawAxiosRequestHeaders
| ((
id: string,
config: AxiosRequestConfig,
settings: IRequestSettings
) => RawAxiosRequestHeaders);
/**
* 是否显示 loading
*/
loading?: boolean;
/**
* 显示 loading
*/
showLoading?: () => void;
/**
* 关闭 loading
*/
hideLoading?: () => void;
/**
* 显示失败提示
*/
failMessage?: boolean;
/**
* 自定义失败提示
*/
showError?: (msg: string, e: any) => void;
/**
* 返回原始 axios 响应对象
*/
originResponse?: boolean;
/**
* 校验响应成功
*/
validSuccess?: boolean;
/**
* 自定义校验方法
*/
validate?: (res: AxiosResponse) => boolean;
/**
* 请求响应警告执行程序插件
*/
skipWarn?: IRequestSkipWarn;
/**
* 是否开启代理
*/
proxy?: boolean;
/**
* 代理服务
*/
proxyPath?: string;
/**
* 其他自定义扩展参数
*/
[index: string]: any;
}
export interface IRequestOptions extends CreateAxiosDefaults {
settings?: IRequestSettings;
}
export interface IRequestConfig<D = any> extends AxiosRequestConfig<D> {
settings?: IRequestSettings;
// url path 参数对象
query?: Record<string, any>;
}
export interface IRequestRecord {
settings: IRequestSettings;
config: AxiosRequestConfig;
source: CancelTokenSource;
}
export type IRequest<R = any, D = any> = (
config?: IRequestConfig<D>
) => Promise<R>;
export class Request {
axios: AxiosInstance;
settings: IRequestSettings;
records: Record<string, IRequestRecord> = {};
isLoading: boolean = false;
private stopSkipWarn?: () => void;
private showLoading: (settings: IRequestSettings) => void;
private showError: (settings: IRequestSettings, e: any) => void;
constructor(options: IRequestOptions = {}) {
this.settings = Object.assign({ type: 'form' }, options.settings || {});
const defaults = omit<IRequestOptions, CreateAxiosDefaults>(options, [
'settings',
'query'
]);
this.axios = axios.create(
merge(
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 2 * 60 * 1000
},
defaults
)
);
this.setupSkipWarn(this.settings);
this.showLoading = debounce(this.openLoading.bind(this), LOADING_DELAY);
this.showError = throttle(this._showError.bind(this), ERROR_DELAY, {
leading: true,
trailing: false
});
}
setConfig(options: IRequestOptions = {}) {
this.settings = merge(this.settings, options.settings || {});
const defaults = omit<IRequestOptions, CreateAxiosDefaults>(options, [
'settings',
'query'
]);
this.axios.defaults = merge(this.axios.defaults, defaults);
this.setupSkipWarn(this.settings);
}
cancel(id?: string, message: string = '请求已取消') {
// 取消对应id单独请求
if (id) {
const record = this.records[id];
if (!record) return;
record.source.cancel(message);
} else {
// 不指定id,取消全部未完成的请求
for (const record of Object.values(this.records)) {
record.source.cancel(message);
}
}
}
private createHeaders(
id: string,
settings: IRequestSettings,
config: AxiosRequestConfig
) {
const injectHeaders = settings.injectHeaders
? typeof settings.headers === 'function'
? settings.headers(id, config, settings)
: settings.headers || {}
: {};
const headers: RawAxiosRequestHeaders = {
'Content-Type': TYPES[settings.type || 'form'],
...config.headers,
...injectHeaders
};
if (settings.skipWarn) {
headers[LOCAL_REQUEST_ID] = id;
}
return headers;
}
private isJsonType(headers: RawAxiosRequestHeaders) {
return Object.entries(headers).some(([k, v]) => {
return (
k.toLowerCase() === 'content-type' &&
String(v).includes('application/json')
);
});
}
private toFormData<T extends object>(
data: T,
type: string = 'data'
): FormData | URLSearchParams {
if (data instanceof FormData || data instanceof URLSearchParams)
return data;
const params = type === 'data' ? new FormData() : new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
params.append(key, value);
});
return params;
}
private createSendData(
settings: IRequestSettings,
config: AxiosRequestConfig,
headers: RawAxiosRequestHeaders,
isSkipWarn: boolean,
query: Record<string, any> = {}
) {
const { type, skipWarn } = settings;
const { name = 'skipWarn' } = skipWarn || {};
let { data, params = {}, method = 'get' } = config;
const skip = isSkipWarn ? { [name]: true } : {};
if (DATA_METHODS.includes(method.toLowerCase())) {
data = Object.assign(data || {}, skip);
data =
type !== 'json' || !this.isJsonType(headers)
? this.toFormData(data, type)
: data;
params = {
...query
};
} else {
if (type === 'form') {
params = {
...(data || {}),
...query,
...skip
};
} else {
if (data && (type !== 'json' || !this.isJsonType(headers))) {
data = this.toFormData(data, type);
}
params = {
...query,
...skip
};
}
}
return {
data,
params
};
}
private createUrl(config: AxiosRequestConfig) {
let { url, params } = config;
if (url) {
let origin = isUrl(url) ? new URL(url).origin : '';
const path = origin ? url.replace(origin, '') : url;
try {
const toPath = pathToRegexpCompile(path, {
encode: encodeURIComponent
});
return origin + toPath(params || {});
} catch (e) {
console.warn('createUrl', 'pathToRegexpCompile error', url);
}
}
return url;
}
private createProxy(
settings: IRequestSettings,
url: string,
headers: RawAxiosRequestHeaders
) {
if (!settings.proxy) {
return {
url,
headers
};
}
const path = settings.proxyPath || PROXY_PATH;
const { protocol, host } = location || {};
const target = isUrl(url) ? url : `${protocol}//${host}${url}`;
headers.Target = target;
return {
url: path,
headers
};
}
private openLoading(settings: IRequestSettings) {
const { loading, showLoading } = settings;
if (!loading) return;
if (showLoading) {
const records = Object.keys(this.records);
if (records.length > 0) {
this.isLoading = true;
showLoading();
}
}
}
private closeLoading(settings: IRequestSettings) {
const { loading, hideLoading } = settings;
if (!loading) return;
this.isLoading = false;
const records = Object.keys(this.records);
if (hideLoading && records.length === 0) {
this.isLoading = false;
hideLoading();
}
}
private _showError(settings: IRequestSettings, e: any) {
const { failMessage, showError } = settings;
if (failMessage && showError) {
const data = e?.response?.data;
const msg =
data?.message || data?.msg || e?.message || e?.msg || '未知错误';
showError(msg, e);
}
}
private validResponse(settings: IRequestSettings, res: AxiosResponse) {
const { validSuccess, validate } = settings;
if (validSuccess && validate) {
return !!validate(res);
}
return true;
}
private isSkipWarnResponse(res: any): res is IRequestSkipWarnResponse {
return !!res.promise;
}
send<R = any, D = any>(
options: IRequestConfig<D> = {},
isSkipWarn: boolean = false
) {
const settings = merge({}, this.settings, options.settings || {});
const query = options.query || {};
const config = omit<IRequestConfig<D>, AxiosRequestConfig<D>>(options, [
'settings',
'query'
]);
const id = uuid(false);
const source = axios.CancelToken.source();
this.records[id] = { settings, config, source };
const _url = this.createUrl(config) || '';
const _headers = this.createHeaders(id, settings, config);
const { url, headers } = this.createProxy(settings, _url, _headers);
const { data, params } = this.createSendData(
settings,
config,
headers,
isSkipWarn,
query
);
this.showLoading(settings);
return new Promise<R>((resolve, reject) => {
this.axios({
cancelToken: source.token,
...config,
url,
headers,
data,
params
})
.then((res) => {
if (this.isSkipWarnResponse(res)) {
return resolve(res.promise);
}
if (this.validResponse(settings, res)) {
return resolve(settings.originResponse ? res : res.data?.data);
} else {
this.showError(settings, res.data);
return reject(res.data);
}
})
.catch((e) => {
this.showError(settings, e);
return reject(e);
})
.finally(() => {
delete this.records[id];
this.closeLoading(settings);
});
});
}
useResponse(
onFulfilled?:
| ((value: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>)
| null,
onRejected?: ((error: any) => any) | null
) {
const { response } = this.axios.interceptors;
const id = response.use(onFulfilled, onRejected);
return () => response.eject(id);
}
useRequest(
onFulfilled?:
| ((
value: InternalAxiosRequestConfig
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>)
| null,
onRejected?: ((error: any) => any) | null
) {
const { request } = this.axios.interceptors;
const id = request.use(onFulfilled, onRejected);
return () => request.eject(id);
}
private setupSkipWarn(settings: IRequestSettings) {
if (this.stopSkipWarn) {
this.stopSkipWarn();
this.stopSkipWarn = undefined;
}
if (!settings.skipWarn) return;
const { code, executor, callback, complete } = settings.skipWarn;
this.stopSkipWarn = this.useResponse((res) => {
const headers: Record<string, any> = res.config.headers || {};
const id = headers[LOCAL_REQUEST_ID] as string;
const requestCache = this.records[id];
if (!requestCache) return res;
const { data } = res;
if (!data || typeof data !== 'object') return res;
if (data?.code === code) {
callback && callback(res);
const promise = new Promise(executor).then(() => {
return this.send(
{
...requestCache.config,
settings: requestCache.settings
},
true
);
});
promise
.catch((e) => e)
.finally(() => {
complete && complete();
});
(res as IRequestSkipWarnResponse).promise = promise;
}
return res;
});
}
}
export interface IStaticRequest<R = any, D = any> extends Request {
(options: IRequestConfig<D>): Promise<R>;
instance: Request;
}
export function createRequest(options: IRequestOptions = {}): IStaticRequest {
const request = new Request(options);
const send = request.send.bind(request);
const cancel = request.cancel.bind(request);
const setConfig = request.setConfig.bind(request);
const useRequest = request.useRequest.bind(request);
const useResponse = request.useResponse.bind(request);
return Object.assign(send, {
...request,
instance: request,
send,
cancel,
setConfig,
useRequest,
useResponse
}) as IStaticRequest;
}
export const request: IStaticRequest = createRequest({
settings: {
injectHeaders: true,
loading: true,
originResponse: true
}
});
export function createApi<R = any, D = any>(
config: string | IRequestConfig,
req: IStaticRequest = request
) {
const _conifg: IRequestConfig =
typeof config === 'string' ? { url: config } : config;
return (data?: D, opts?: IRequestConfig) =>
req.send<R, D>(merge({}, _conifg, opts || {}, { data }));
}
export interface IApiMap {
[name: string]: string | IRequestConfig;
}
export function createApis(map: IApiMap, req: IStaticRequest = request) {
const apis: Record<string, ReturnType<typeof createApi>> = {};
for (const [name, opts] of Object.entries(map)) {
apis[name] = createApi(opts, req);
}
return apis;
}
export interface UseApiReturn<R = any> {
data: Ref<R | null>;
error: Ref<any>;
loading: Ref<boolean>;
reload: () => void;
}
export function useApi<R = any>(
loader: Promise<R> | (() => Promise<R>),
transform?: (res: any) => R
): UseApiReturn<R> {
const data: Ref<R | null> = ref(null);
const error = ref<any>();
const loading = ref<boolean>(true);
const reload = () => {
loading.value = true;
error.value = undefined;
const api = typeof loader === 'function' ? loader() : loader;
api
.then((res: any) => {
data.value = transform ? transform(res) : res;
})
.catch((err) => {
error.value = err;
})
.finally(() => {
loading.value = false;
});
};
reload();
return {
data,
error,
loading,
reload
};
}
export {
axios,
LOCAL_REQUEST_ID,
type AxiosRequestConfig,
type AxiosResponse,
type RawAxiosRequestHeaders
};
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
TypeScript
1
https://gitee.com/newgateway/vtj.git
git@gitee.com:newgateway/vtj.git
newgateway
vtj
VTJ
master

搜索帮助