vite-uni-dev-tool
Version:
vite-uni-dev-tool, debug, uni-app, 一处编写,到处调试
574 lines (506 loc) • 15.1 kB
text/typescript
import { onMounted, ref, shallowReactive, watch, type Ref } from 'vue';
import { debounce, throttle } from './utils';
// 添加分页响应数据类型
// type PaginationResponse<T = any> = {
// list?: T[];
// data?: T[];
// total: number;
// current: number;
// pageSize: number;
// };
type Option<R, P> = {
/** 是否需要uni.showLoading */
useUniLoading?: boolean;
/**
* 每次 ready 从 false 变为 true 时, 都会自动发起请求
* 为 false 时,请求永远都不会发出
*/
ready?: boolean | Ref<boolean>;
/** 立即执行 */
manual?: boolean;
/**
* 默认参数
* 分页参数在请放入 分页参数配置中
*/
defaultParams?: P;
/** 依赖刷新 */
refreshDeps?: any[];
/**
* 延迟结束loading
* 单位毫秒
* 防止请求过快造成渲染闪烁
*/
loadingDelay?: number;
/**
* 轮询间隔
* 通过 run/runAsync 来启动轮询
* 通过 cancel 取消轮询
*/
pollingInterval?: number;
/**
* 轮询错误重试次数
*/
pollingErrorRetryCount?: number;
/**
* 错误重试次数
*/
errorRetryCount?: number;
/**
* 错误重试间隔
*/
errorRetryDelay?: number;
/**
* 防抖等待时间
*/
debounceWait?: number;
/**
* 在防抖开始前执行调用
*/
debounceLeading?: boolean;
/**
* 在防抖结束后执行调用
*/
debounceTrailing?: boolean;
/**
* 节流等待时间
*/
throttleWait?: number;
/**
* 在节流开始前执行调用
*/
throttleLeading?: boolean;
/**
* 在节流结束后执行调用
*/
throttleTrailing?: boolean;
/**
* 使用分页
*/
usePagination?: boolean;
/**
* 分页配置
*
*/
paginationConfig?: {
/** 当前页码参数名,默认 'current' */
currentKey?: string;
/** 每页数量参数名,默认 'pageSize' */
pageSizeKey?: string;
/** 总数字段名,默认 'total' */
totalKey?: string;
/** 默认当前页,默认 1 */
defaultCurrent?: number;
/** 默认每页数量,默认 10 */
defaultPageSize?: number;
/** 列表字段名,默认 'list' */
listKey?: string;
};
/** 请求前 */
onBefore?: () => Promise<void> | void;
/** 请求成功 */
onSuccess?: (result: R | undefined, params?: P) => void;
/** 请求失败 */
onError?: (error: any) => void;
/** 请求完成 */
onFinally?: () => void;
};
export type Service<R, P extends any[]> = (...args: P) => Promise<R>;
const useRequest = <R, P extends any[]>(
service: Service<R, P>,
option?: Option<R, P>,
) => {
const loading = ref(false);
const data = ref<R>();
/**
* 启用 usePagination 之后每页数据合并之后的结果
* 为开启默认为 []
* 只适合从第一页开始获取数据
*/
const list = ref<R[]>([]);
const pagination = shallowReactive({
current: 1,
pageSize: 10,
total: 0,
totalPage: 0,
hasMore: true,
// 分页操作方法
setCurrent: (current: number) => {
pagination.current = Math.max(1, current);
refresh();
},
setPageSize: (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.current = 1; // 重置到第一页
refresh();
},
reset: () => {
pagination.current = option?.paginationConfig?.defaultCurrent ?? 1;
pagination.pageSize = option?.paginationConfig?.defaultPageSize ?? 10;
refresh();
},
prev: () => {
if (pagination.current > 1) {
pagination.current--;
refresh();
}
},
next: () => {
if (pagination.current < pagination.totalPage) {
pagination.current++;
refresh();
}
},
change: (current: number, pageSize: number) => {
pagination.current = current;
pagination.pageSize = pageSize;
refresh();
},
});
/** 最后一次请求的参数 */
let lastParams: P | undefined;
let loadingTimer: any;
let intervalTimer: any;
let allowPolling = true;
let pollingErrorCount = 0;
let errorCount = 0;
let intervalErrorRetry: ReturnType<typeof setTimeout> | undefined = undefined;
/** 记录上次 ready 的状态 */
let previousReady = false;
/** 获取 ready 值 */
const getReadyValue = (): boolean => {
if (option?.ready === undefined) return true;
return typeof option.ready === 'boolean'
? option.ready
: option.ready.value;
};
/**
*
* @param params 请求参数
*/
function run(...params: P) {
if (getReadyValue()) {
execServiceFunction(params)
?.catch((error) => {
errorRetry(params);
throw error;
})
?.finally(() => {
if (option?.pollingInterval && option.pollingInterval > 0) {
startPolling(params);
}
});
}
}
async function runAsync(...params: P) {
// 只有在 ready 为 true 时才执行请求
if (getReadyValue()) {
try {
const result = await execServiceFunction?.(params);
return result;
} catch (error) {
errorRetry(params);
throw error;
} finally {
if (option?.pollingInterval && option.pollingInterval > 0) {
startPolling(params);
}
}
}
}
/**
* 刷新
*
*/
function refresh() {
// 只有在 ready 为 true 时才执行请求
if (getReadyValue()) {
const fallbackParams: P = ((lastParams || option?.defaultParams) ??
[]) as P;
execServiceFunction(fallbackParams);
}
}
/**
* 刷新异步
*
* @return {*}
*/
function refreshAsync() {
// 只有在 ready 为 true 时才执行请求
if (getReadyValue()) {
const fallbackParams: P = ((lastParams || option?.defaultParams) ??
[]) as P;
return execServiceFunction(fallbackParams);
}
}
async function execService(params: P) {
// 只有在 ready 为 true 时才执行请求
if (getReadyValue()) {
try {
loading.value = true;
if (option?.useUniLoading) {
uni.showLoading({
title: '加载中...',
mask: true,
});
}
lastParams = params;
await option?.onBefore?.();
const result = await service(...params);
data.value = result;
// 如果启用了分页功能,处理分页数据
if (option?.usePagination) {
const config = option.paginationConfig || {};
const currentKey = config.currentKey || 'current';
const pageSizeKey = config.pageSizeKey || 'pageSize';
const totalKey = config.totalKey || 'total';
const listKey = config.listKey || 'list';
// 检查结果是否为分页格式
if (result && typeof result === 'object') {
const paginationResult = result as any;
pagination.current =
paginationResult[currentKey] || pagination.current;
pagination.pageSize =
paginationResult[pageSizeKey] || pagination.pageSize;
pagination.total = paginationResult[totalKey] || 0;
pagination.totalPage = Math.ceil(
pagination.total / pagination.pageSize,
);
pagination.hasMore = pagination.current < pagination.totalPage;
// 获取列表数据
const resultData =
paginationResult[listKey] ||
paginationResult.data ||
paginationResult.list ||
[];
// 根据当前页码决定是否合并列表数据
if (pagination.current === 1) {
// 第一页时,替换列表
list.value = [...resultData];
} else {
// 非第一页时,合并列表
list.value = [...list.value, ...resultData];
}
} else {
pagination.hasMore = false;
}
}
option?.onSuccess?.(data.value, params);
return data.value;
} catch (error) {
pollingErrorCount++;
errorCount++;
option?.onError?.(error);
throw error;
} finally {
option?.onFinally?.();
// 结束loading
if (option?.useUniLoading) {
// 延迟结束loading
if (option?.loadingDelay && option.loadingDelay > 0) {
clearTimeout(loadingTimer);
loadingTimer = setTimeout(() => {
loading.value = false;
uni.hideLoading();
}, option.loadingDelay);
} else {
loading.value = false;
uni.hideLoading();
}
}
}
}
}
const execServiceDebounce = debounce(execService, option?.debounceWait ?? 0, {
leading: option?.debounceLeading,
trailing: option?.debounceTrailing,
});
const execServiceThrottle = throttle(execService, option?.throttleWait ?? 0, {
leading: option?.throttleLeading,
trailing: option?.throttleTrailing,
});
function execServiceFunction(params: P) {
if (option?.usePagination) {
const config = option.paginationConfig || {};
const currentKey = config.currentKey || 'current';
const pageSizeKey = config.pageSizeKey || 'pageSize';
// 将分页参数注入到请求参数中
const paramsWithPagination = [...params] as any[];
// 如果参数是对象,添加分页字段
if (
paramsWithPagination.length > 0 &&
typeof paramsWithPagination[0] === 'object' &&
paramsWithPagination[0] !== null
) {
paramsWithPagination[0] = {
...paramsWithPagination[0],
[currentKey]: pagination.current,
[pageSizeKey]: pagination.pageSize,
};
} else {
// 如果没有参数或参数不是对象,创建一个包含分页信息的对象
paramsWithPagination.unshift({
[currentKey]: pagination.current,
[pageSizeKey]: pagination.pageSize,
});
}
if (option?.debounceWait) {
return execServiceDebounce(paramsWithPagination as P);
} else if (option?.throttleWait) {
return execServiceThrottle(paramsWithPagination as P);
} else {
return execService(paramsWithPagination as P);
}
} else {
if (option?.debounceWait) {
return execServiceDebounce(params);
} else if (option?.throttleWait) {
return execServiceThrottle(params);
} else {
return execService(params);
}
}
}
/**
* 开启轮询
*
* @param {P} params
*/
function startPolling(params: P) {
cancel();
allowPolling = true;
if (!getReadyValue()) return;
// 使用递归函数实现轮询,确保请求完成后才开始计时下一次请求
const poll = async () => {
if (!allowPolling) return;
// 检查 ready 状态
try {
await execService(params);
// 检查错误重试次数限制
if (
option?.pollingErrorRetryCount &&
pollingErrorCount >= option.pollingErrorRetryCount
) {
cancel();
return;
}
// 如果仍然允许轮询,则设置定时器等待下次轮询
if (allowPolling) {
intervalTimer = setTimeout(poll, option?.pollingInterval);
}
} catch (error) {
// execService 内部已经处理了错误,这里只需要确保继续轮询或停止
if (
option?.pollingErrorRetryCount &&
pollingErrorCount >= option.pollingErrorRetryCount
) {
cancel();
return;
}
// 如果仍然允许轮询,则设置定时器等待下次轮询
if (allowPolling) {
intervalTimer = setTimeout(poll, option?.pollingInterval);
}
}
};
// 先等待轮询间隔时间,然后再开始执行请求
intervalTimer = setTimeout(() => {
// 开始轮询
poll();
}, option?.pollingInterval);
}
/** 取消轮询 */
function cancel() {
allowPolling = false;
pollingErrorCount = 0;
errorCount = 0;
clearInterval(intervalTimer);
clearInterval(intervalErrorRetry);
}
/**
* 错误重试
*
* @param {P} params
* @return {*}
*/
function errorRetry(params: P) {
// 存在则不进入错误重试
if (option?.pollingErrorRetryCount || option?.pollingInterval) return;
// 不存在 或者 次数小于 0 不进入错误重试
if (!option?.errorRetryCount || option.errorRetryCount <= 0) return;
if (!getReadyValue()) return;
const poll = async () => {
try {
await execService(params);
cancel();
} catch (error) {
if (option.errorRetryCount && option.errorRetryCount < errorCount) {
cancel();
return;
}
intervalErrorRetry = setTimeout(poll, option?.errorRetryDelay ?? 0);
}
};
intervalErrorRetry = setTimeout(poll, option?.errorRetryDelay ?? 0);
}
function setData(value: R) {
data.value = value;
}
function setLoading(value: boolean) {
loading.value = value;
}
onMounted(() => {
// 记录初始 ready 状态
previousReady = getReadyValue();
// 初始化分页配置
if (option?.usePagination) {
const config = option.paginationConfig || {};
pagination.current = config.defaultCurrent ?? 1;
pagination.pageSize = config.defaultPageSize ?? 10;
}
if (!option?.manual && option?.ready !== false) {
const defaultParams = (option?.defaultParams || []) as P;
run(...defaultParams);
}
});
// 监听 ready 参数变化
watch(
() => getReadyValue(),
(currentReady) => {
// 只有当 ready 从 false 变为 true 时才执行请求
if (currentReady && !previousReady) {
const params = (lastParams || option?.defaultParams || []) as P;
// run(...params);
execService(params).catch(() => {
errorRetry(params);
});
} else {
cancel();
}
// 更新 previousReady 状态
previousReady = !!currentReady;
},
);
watch(
() => option?.refreshDeps,
() => {
refresh();
},
{
deep: true,
},
);
return {
data,
setData,
loading,
setLoading,
run,
runAsync,
refresh,
refreshAsync,
cancel,
pagination: option?.usePagination ? pagination : undefined,
list,
};
};
export default useRequest;