JS大文件的分流下载

遇到的问题:

从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、方法的时序图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值