遇到的问题:
从minio下载文件,文件大小为17M,原有代码显示无法下载
小文件的下载方法
function downloadFile(data: any, fileName: any, fileSuffix: any) {
if (data instanceof Blob) {
const reader = new FileReader()
reader.readAsText(data, 'utf-8')
// eslint-disable-next-line func-names
reader.onload = async function () {
try {
// blob的json数据,需要解析
const msg = JSON.parse(reader.result)
if (msg.code !== 200) {
createWarningMessage(msg.msg)
}
} catch (e) {
// 是 Blob 类型的文件数据,直接使用 FileSaver.js 保存文件
saveAs(data, `${fileName}.${fileSuffix}`)
}
}
} else {
// 不是 Blob 类型数据,抛出异常信息
const resText = JSON.stringify(data)
const rspObj = JSON.parse(resText)
const errMsg = rspObj.msg
createWarningMessage(errMsg)
}
}
大文件,下载过17M的文件,核心逻辑:分块的流式下载
/**
* 大文件下载方法(禁用no-await-in-loop规则方案)
* @param url PDF文件URL
* @param fileName 下载文件名
* @param options 配置项
*/
async function downloadLargePDF(
url: string,
fileName: string,
options: {
timeout?: number
retry?: number
onProgress?: (percent: number) => void
signal?: AbortSignal
} = {}
): Promise<{ status: 'success' | 'error'; bytes?: number; error?: string }> {
const { timeout = 30000, retry = 2, onProgress } = options
let retryCount = 0
let receivedBytes = 0
const chunks: Uint8Array[] = []
const controller = new AbortController()
const signal = options.signal || controller.signal
try {
// 超时设置
const timeoutId =
timeout > 0
? setTimeout(() => {
controller.abort()
throw new Error(`Request timeout after ${timeout}ms`)
}, timeout)
: null
// 发起请求
const response = await fetch(url, { signal })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
if (!response.body) throw new Error('No readable stream')
// 获取文件信息 获取流读取器 (getReader()) 用于按顺序读取数据块
// 每个 reader 会独占流,直到调用 releaseLock()
const reader = response.body.getReader()
const contentLength = response.headers.get('content-length')
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0
let lastProgress = -1
// 流式读取(关键部分:禁用await-in-loop警告)
try {
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { done, value } = await reader.read()
// 表示流是否结束
if (done) break
// 每个 value 会被存入 chunks 数组,后续合并成完整文件
chunks.push(value)
receivedBytes += value.length
// 进度更新(每秒最多更新10次)
if (onProgress && totalBytes > 0) {
const currentProgress = Math.floor(
(receivedBytes / totalBytes) * 100
)
if (currentProgress !== lastProgress) {
onProgress(currentProgress)
lastProgress = currentProgress
}
}
}
} finally {
// 资源释放 (releaseLock())
reader.releaseLock()
}
// 创建下载链接 最后通过 new Blob(chunks) 按需合并,减少内存峰值
const blob = new Blob(chunks, { type: 'application/pdf' })
const blobUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = fileName
link.style.display = 'none'
document.body.appendChild(link)
link.click()
// 清理资源
setTimeout(() => {
document.body.removeChild(link)
URL.revokeObjectURL(blobUrl)
chunks.length = 0
// 释放内存
}, 100)
if (timeoutId) clearTimeout(timeoutId)
return { status: 'success', bytes: receivedBytes }
} catch (error: any) {
// 重试逻辑
console.log("重试了?");
if (retryCount < retry && !signal.aborted) {
retryCount = +1
console.warn(`Retrying (${retryCount}/${retry})...`)
return downloadLargePDF(url, fileName, {
...options,
retry: retry - 1,
})
}
return {
status: 'error',
error: signal.aborted ? 'Download aborted' : error.message,
bytes: receivedBytes,
}
}
}
1、分块的流式下载
修改点 | 原方案问题 | 改进方案优势 |
---|---|---|
分块流式传输 | 一次性加载大文件导致内存压力 | 通过 ReadableStream 分块读取,避免内存暴涨 |
超时控制 | 无超时机制导致僵死请求 | 使用 AbortController 主动终止长时间无响应的请求 |
进度反馈 | 用户无感知大文件下载状态 | 通过 Content-Length 计算实时进度,提升用户体验 |
错误重试机制 | 网络波动直接导致失败 | 自动重试机制提升弱网环境下的成功率 |
资源释放 | 未及时释放 Blob URL 导致内存泄漏 | 下载完成后立即调用 revokeObjectURL 释放内存 |
响应状态检查 | 忽略服务器错误状态码(如 404/500) | 对非 200 状态码明确抛出错误,避免静默失败 |
2、方法的时序图