业务描述
用户点击“获取文件”按钮:
- 如果文件已生成、并成功上传到云盘,则直接下载文件;
- 如果文件正在生成中,则按钮回显为“生成中...”;
- 如果文件生成失败,则按钮回显为“重新获取”。
轮询逻辑
- 当用户点击“获取文件”按钮时,立即执行请求文件状态接口;
- 如果接口返回“文件生成中”,则开始轮询请求文件生成状态(每10s一次);
- 如果接口返回成功/失败、或者超时/页面销毁,则停止轮询。
实现思路
- getSubDownloadFile:定义文件状态请求接口;
- downlowndSubTask:递归轮询获取文件状态&下载链接
<a-button size="small" onClick={() => downlowndSubTask(row)}>
{waiting ? '生成中...' : fail ? '重新获取' : '获取文件'}
</a-button>
/** 发起http请求:获取子任务下载文件 */
export const getSubDownloadFile = async (subTaskId: string) => {
const { data } = await get<SubDownloadResult>('/download/task/sub_file', { subTaskId })
return data || null
}
export interface SubDownloadResult {
/** 子任务下载状态 */
status: 'error' | 'waiting' | 'done'
/** 信息 */
message: string
/** 下载链接 */
url?: string
}
/** 轮询:递归循环获取下载链接 */
const downlowndSubTask = async (task: DownloadSubTask, retry = 0) => {
// 1,立即执行一次
const subTaskId = task._id || ''
const res = await getSubDownloadFile(task._id || '')
// 2,遇到错误退出
if (res?.status === 'error') {
return $error(res.message) // 提示报错
}
// 3,获取到下载链接,退出
const url = res?.url ?? ''
if (url) {
window.open(url)
return
}
// 4,进入轮询
const { timer: _timer } = execDelay(downlowndSubTask, [task, retry + 1], 10000)
timer.value = _timer
}
/**
* 延迟执行某个函数
*
* @param func 要延迟执行的函数,可以是同步或异步
* @param args 要执行函数的参数
* @param delay 延迟毫秒数,默认0和直接调用一致
*/
export const execDelay = <T>(func: (...args: any[]) => Promise<T>, args: any[], delay = 0) => {
let timer = 0
const task: Promise<T> & { timer: number } = new Promise((resolve, reject) => {
const execFn = () => {
func
.apply(null, [...args])
.then((res) => {
resolve(res)
})
.catch((e) => {
reject(e)
})
}
if (delay) {
timer = setTimeout(() => execFn(), delay) as any
} else {
execFn()
}
}) as any
task.timer = timer
return task
}
延展思考
此处的轮询本质上属于一种异步循环。异步循环有两种情况:①异步串行 ②异步并行
-
实现异步循环时,先不要考虑循环控制流的问题,直接写一个执行一次的函数:doOne()
-
异步并行:可以用promise.all实现并行执行doOne()函数:
async doAll(list: any[]) => {
Promise.all(list.map(l => doOne(l)))
}
- 异步串行:可以用递归的方式串行执行doOne()函数:
async doAll(i: number) => {
// 立即执行一次
doOne()
// 设置退出条件
if (i >= N) return
// 递归执行
doAll(i++) // 如果执行时需要delay,可以改为execDelay(doAll(i++))
}
doAll(0)