302 lines
10 KiB
TypeScript
302 lines
10 KiB
TypeScript
import deepMerge from '../function/deepMerge';
|
||
|
||
/**
|
||
* 请求配置项Meta类型定义
|
||
*/
|
||
export interface RequestMeta {
|
||
toast?: boolean;
|
||
loading?: boolean;
|
||
originalData?: boolean;
|
||
[key: string]: any;
|
||
}
|
||
|
||
/**
|
||
* 请求配置项类型定义
|
||
*/
|
||
export interface RequestConfig {
|
||
baseUrl?: string;
|
||
header?: Record<string, any>;
|
||
method?: string;
|
||
dataType?: string;
|
||
responseType?: string;
|
||
timeout?: number;
|
||
meta?: RequestMeta;
|
||
[key: string]: any;
|
||
}
|
||
|
||
/**
|
||
* 忽略的请求参数类型定义
|
||
*/
|
||
const IGNORE_REQUEST_KEYS = ['baseUrl', 'meta'];
|
||
|
||
/**
|
||
* 请求拦截器类型定义
|
||
*/
|
||
export interface RequestInterceptor {
|
||
request?: ((options: RequestOptions) => RequestOptions | false) | null;
|
||
response?: ((response: any) => any | false) | null;
|
||
}
|
||
|
||
/**
|
||
* 请求参数类型定义
|
||
*/
|
||
export interface RequestOptions {
|
||
url: string;
|
||
header: Record<string, any>;
|
||
method: 'GET' | 'POST' | 'OPTIONS' | 'HEAD' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT';
|
||
data?: any;
|
||
dataType?: string;
|
||
responseType?: string;
|
||
params?: Record<string, any>;
|
||
complete?: (response: any) => void;
|
||
meta?: RequestMeta;
|
||
[key: string]: any;
|
||
}
|
||
|
||
export class Request {
|
||
public config: RequestConfig;
|
||
public interceptor: RequestInterceptor;
|
||
public options?: RequestOptions;
|
||
|
||
constructor() {
|
||
this.config = {
|
||
baseUrl: '', // 请求的根域名
|
||
header: {}, // 默认的请求头
|
||
method: 'POST', // 请求方式
|
||
dataType: 'json', // 设置为json,返回后uni.request会对数据进行一次JSON.parse
|
||
responseType: 'text', // 此参数无需处理,因为5+和支付宝小程序不支持,默认为text即可
|
||
timeout: 60000,
|
||
meta: {
|
||
originalData: true, // 是否在拦截器中返回服务端的原始数据,见文档说明
|
||
toast: false, // 是否在请求出错时,弹出toast
|
||
loading: false // 是否显示加载中
|
||
}
|
||
};
|
||
this.interceptor = {
|
||
request: null,
|
||
response: null
|
||
};
|
||
}
|
||
/**
|
||
* 将全局配置合并到本次请求的 options 中
|
||
* - 忽略 IGNORE_REQUEST_KEYS 中的字段(如 meta)
|
||
* - 对 header 使用深合并(全局 header 为默认,options.header 优先)
|
||
* - 对对象类型的字段尝试深合并,基础类型以 options 值优先
|
||
* - 处理 baseUrl:若存在全局 baseUrl 且 options.url 非完整 url(非 http 开头),则合并成完整 URL
|
||
*/
|
||
private mergeGlobalConfigToOptions(options: RequestOptions): RequestOptions {
|
||
const mergedOptions: RequestOptions = { ...options };
|
||
for (const key of Object.keys(this.config)) {
|
||
if (IGNORE_REQUEST_KEYS.includes(key)) {
|
||
continue;
|
||
}
|
||
const cfgVal = this.config[key];
|
||
const optVal = options[key];
|
||
|
||
// 跳过未设置的全局配置
|
||
if (cfgVal === undefined) continue;
|
||
|
||
// header 需要做深合并,且以 options.header 为准覆盖同名属性
|
||
if (key === 'header') {
|
||
mergedOptions.header = deepMerge(cfgVal || {}, optVal || {});
|
||
continue;
|
||
}
|
||
|
||
// 针对 method 等枚举字符串,优先使用 options 中的值,否则使用全局配置
|
||
if (typeof cfgVal === 'string' || typeof cfgVal === 'number' || typeof cfgVal === 'boolean') {
|
||
mergedOptions[key] = optVal !== undefined ? optVal : cfgVal;
|
||
continue;
|
||
}
|
||
|
||
// 对对象类型的配置(如自定义扩展)尝试做深合并
|
||
if (typeof cfgVal === 'object' && !Array.isArray(cfgVal)) {
|
||
mergedOptions[key] = deepMerge(cfgVal || {}, optVal || {});
|
||
continue;
|
||
}
|
||
|
||
// 其他类型,若 options 未传入则使用全局配置
|
||
if (optVal === undefined) {
|
||
mergedOptions[key] = cfgVal;
|
||
}
|
||
}
|
||
// 如果存在 baseUrl,并且 options.url 为相对地址,则拼接成完整 url
|
||
const baseUrl = this.config.baseUrl;
|
||
if (
|
||
baseUrl &&
|
||
mergedOptions.url &&
|
||
typeof mergedOptions.url === 'string' &&
|
||
mergedOptions.url.indexOf('http') !== 0
|
||
) {
|
||
mergedOptions.url =
|
||
baseUrl + (mergedOptions.url.indexOf('/') === 0 ? mergedOptions.url : `/${mergedOptions.url}`);
|
||
}
|
||
// 确保 url 存在,且为 string
|
||
if (!mergedOptions.url) {
|
||
mergedOptions.url = '';
|
||
}
|
||
return mergedOptions;
|
||
}
|
||
/**
|
||
* 设置全局默认配置
|
||
* @param customConfig 自定义配置
|
||
*/
|
||
setConfig(customConfig: Partial<RequestConfig>): void {
|
||
this.config = deepMerge(this.config, customConfig);
|
||
}
|
||
|
||
/**
|
||
* 主要请求部分
|
||
* @param options 请求参数
|
||
*/
|
||
request<T = unknown>(options: RequestOptions): Promise<T> {
|
||
// 合并 meta 配置,优先级:单次请求 > 全局
|
||
const mergedMeta: RequestMeta = {
|
||
...this.config.meta,
|
||
...(options.meta || {})
|
||
};
|
||
// 让 options.meta 传递到拦截器
|
||
options.meta = mergedMeta;
|
||
options.url = options.url || '';
|
||
options.params = options.params || {};
|
||
// 将全局配置合并到本次请求 options 中(注意忽略一些特殊字段如 baseUrl/meta)
|
||
options = this.mergeGlobalConfigToOptions(options);
|
||
|
||
if (this.interceptor.request && typeof this.interceptor.request === 'function') {
|
||
const interceptorRequest = this.interceptor.request(options);
|
||
if (!interceptorRequest) {
|
||
// 返回一个处于pending状态中的Promise,来取消原promise,避免进入then()回调
|
||
return new Promise(() => {});
|
||
}
|
||
this.options = interceptorRequest;
|
||
}
|
||
|
||
return new Promise<T>((resolve, reject) => {
|
||
options.complete = (response: any) => {
|
||
// 读取 meta 配置
|
||
const meta = options.meta || this.config.meta || {};
|
||
const originalData = meta.originalData ?? false;
|
||
// 拦截器处理,加入request的配置参数
|
||
response.config = options;
|
||
if (originalData) {
|
||
// 判断是否存在拦截器
|
||
if (this.interceptor.response && typeof this.interceptor.response === 'function') {
|
||
const resInterceptors = this.interceptor.response(response);
|
||
// 如果拦截器不返回false,就将拦截器返回的内容给请求的then回调
|
||
if (resInterceptors !== false) {
|
||
resolve(resInterceptors);
|
||
} else {
|
||
// 如果拦截器返回false,意味着拦截器定义者认为返回有问题,直接接入catch回调
|
||
reject(response);
|
||
}
|
||
} else {
|
||
// 如果要求返回原始数据,就算没有拦截器,也返回最原始的数据
|
||
resolve(response);
|
||
}
|
||
} else {
|
||
if (response.statusCode === 200) {
|
||
if (this.interceptor.response && typeof this.interceptor.response === 'function') {
|
||
const resInterceptors = this.interceptor.response(response.data);
|
||
if (resInterceptors !== false) {
|
||
resolve(resInterceptors);
|
||
} else {
|
||
reject(response.data);
|
||
}
|
||
} else {
|
||
// 如果不是返回原始数据(originalData=false),且没有拦截器的情况下,返回纯数据给then回调
|
||
resolve(response.data);
|
||
}
|
||
} else {
|
||
reject(response);
|
||
}
|
||
}
|
||
};
|
||
uni.request(options);
|
||
});
|
||
}
|
||
|
||
get<T = unknown>(
|
||
url: string,
|
||
data: any = {},
|
||
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
|
||
): Promise<T> {
|
||
return this.request<T>({
|
||
method: 'GET',
|
||
url,
|
||
data,
|
||
header: options.header || {},
|
||
meta: options.meta
|
||
});
|
||
}
|
||
|
||
post<T = unknown>(
|
||
url: string,
|
||
data: any = {},
|
||
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
|
||
): Promise<T> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'POST',
|
||
data,
|
||
header: options.header || {},
|
||
meta: options.meta
|
||
});
|
||
}
|
||
|
||
put<T = unknown>(
|
||
url: string,
|
||
data: any = {},
|
||
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
|
||
): Promise<T> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'PUT',
|
||
data,
|
||
header: options.header || {},
|
||
meta: options.meta
|
||
});
|
||
}
|
||
|
||
delete<T = unknown>(
|
||
url: string,
|
||
data: any = {},
|
||
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
|
||
): Promise<T> {
|
||
return this.request<T>({
|
||
url,
|
||
method: 'DELETE',
|
||
data,
|
||
header: options.header || {},
|
||
meta: options.meta
|
||
});
|
||
}
|
||
}
|
||
|
||
// 插件化导出,支持 app.use(http, { interceptor })
|
||
const httpInstance = new Request();
|
||
|
||
interface HttpPluginOptions {
|
||
requestConfig?: Partial<RequestConfig>;
|
||
interceptor?: RequestInterceptor;
|
||
}
|
||
|
||
// 全局导出,支持 import { httpPlugin } from 'uview-pro'
|
||
const httpPlugin = {
|
||
install(app: any, options: HttpPluginOptions = {}) {
|
||
if (options.interceptor) {
|
||
const { request, response } = options.interceptor;
|
||
if (request) httpInstance.interceptor.request = request;
|
||
if (response) httpInstance.interceptor.response = response;
|
||
}
|
||
if (options.requestConfig) {
|
||
httpInstance.setConfig(options.requestConfig);
|
||
}
|
||
app.config.globalProperties.$http = httpInstance;
|
||
}
|
||
};
|
||
|
||
// 全局导出,支持 import { http } from 'uview-pro'
|
||
export { httpInstance as http };
|
||
|
||
// 插件化导出,支持 app.use(http, { interceptor })
|
||
export default httpPlugin;
|