vue axios 封装一个通用下载工具类,包含进度监听、剩余时间计算、暂停 / 恢复功能

vue axios 封装一个通用下载工具类,包含进度监听、剩余时间计算、暂停 / 恢复功能

  • DownloadManager.js
import axios from 'axios'
import { getToken } from '@/utils/auth'

class DownloadManager {
  constructor() {
    this.tasks = new Map() // 存储下载任务:key=任务ID,value=任务详情
  }

  /**
   * 创建下载任务
   * @param {Object} options 下载配置
   * @param {string} options.url 下载地址
   * @param {string} options.filename 保存的文件名
   * @param {Function} options.onProgress 进度回调 (percent, remainingTime) => {}
   * @param {Function} options.onComplete 完成回调 (fileBlob) => {}
   * @param {Function} options.onError 错误回调 (errorMsg) => {}
   * @returns {string} 任务ID
   */
  createTask(options) {
    const taskId = Date.now().toString() // 生成唯一任务ID
    const { url, filename, onProgress, onComplete, onError } = options

    // 初始化任务状态
    const task = {
      id: taskId,
      url,
      filename,
      status: 'pending', // pending / downloading / paused / completed / failed
      controller: null, // AbortController实例
      request: null, // Axios请求实例
      loaded: 0, // 已下载字节数
      total: 0, // 总字节数
      timePoints: [], // 速度计算用的时间点数组 [timestamp, loaded]
      onProgress,
      onComplete,
      onError,
    }

    this.tasks.set(taskId, task)
    return taskId
  }

  /**
   * 开始/恢复下载
   * @param {string} taskId 任务ID
   */
  start(taskId) {
    const task = this.tasks.get(taskId)
    if (!task) {
      task?.onError?.('任务不存在')
      return
    }

    if (task.status === 'downloading') {
      task?.onError?.('任务正在下载中')
      return
    }

    // 创建新的控制器(恢复下载时需要新的signal)
    const controller = new AbortController()
    const { signal } = controller
    task.controller = controller
    task.status = 'downloading'

    // 发起请求(支持断点续传:通过Range请求头续传已下载部分)
    const headers =
      task.loaded > 0
        ? { Range: `bytes=${task.loaded}-`, Authorization: 'Bearer ' + getToken() } // 断点续传:从已下载位置开始
        : { Authorization: 'Bearer ' + getToken() }

    task.request = axios({
      url: task.url,
      method: 'get',
      responseType: 'blob',
      headers,
      signal,
      onDownloadProgress: (e) => this.handleProgress(task, e),
    })
      .then((response) => this.handleComplete(task, response))
      .catch((error) => this.handleError(task, error))
  }

  /**
   * 暂停下载
   * @param {string} taskId 任务ID
   */
  pause(taskId) {
    const task = this.tasks.get(taskId)
    if (!task || task.status !== 'downloading') return

    // 终止当前请求(会触发onError,但会被标记为暂停状态)
    task.controller?.abort('paused')
    task.status = 'paused'
  }

  /**
   * 取消下载(清除任务)
   * @param {string} taskId 任务ID
   */
  cancel(taskId) {
    const task = this.tasks.get(taskId)
    if (!task) return

    task.controller?.abort('cancelled')
    this.tasks.delete(taskId)
  }

  /**
   * 处理下载进度
   * @param {Object} task 任务实例
   * @param {ProgressEvent} e 进度事件
   */
  handleProgress(task, e) {
    // 计算总大小(断点续传时,response headers的Content-Range可能返回总大小)
    if (e.total) {
      task.total = e.total
    } else if (e.target?.responseHeaders?.['content-range']) {
      // 从Content-Range提取总大小(格式:bytes 0-100/1000 → 总大小1000)
      const totalMatch = e.target.responseHeaders['content-range'].match(/\/(\d+)$/)
      if (totalMatch) task.total = Number(totalMatch[1])
    }

    // 计算已下载大小(断点续传时需要累加之前的loaded)
    const currentLoaded = task.loaded + e.loaded
    task.loaded = currentLoaded

    // 计算进度百分比
    const percent = task.total > 0 ? Math.min(100, Math.round((currentLoaded / task.total) * 100)) : 0

    // 计算剩余时间
    const remainingTime = this.calculateRemainingTime(task, currentLoaded)

    // 触发进度回调
    task.onProgress?.(percent, remainingTime)
  }

  /**
   * 计算剩余时间
   * @param {Object} task 任务实例
   * @param {number} currentLoaded 当前已下载字节数
   * @returns {string} 格式化的剩余时间(如 01:23)
   */
  calculateRemainingTime(task, currentLoaded) {
    if (task.total === 0) return '计算中...'

    // 记录时间点(每300ms记录一次,避免数据过多)
    const now = Date.now()
    if (task.timePoints.length === 0 || now - task.timePoints.at(-1)[0] > 300) {
      task.timePoints.push([now, currentLoaded])
      task.timePoints = task.timePoints.slice(-5) // 保留最近5个点,平衡实时性和稳定性
    }

    if (task.timePoints.length < 2) return '计算中...'

    // 计算平均速度(字节/秒)
    const first = task.timePoints[0]
    const last = task.timePoints.at(-1)
    const timeDiffMs = last[0] - first[0]
    const loadedDiff = last[1] - first[1]

    if (timeDiffMs === 0 || loadedDiff <= 0) return '计算中...'

    const avgSpeed = loadedDiff / (timeDiffMs / 1000) // 字节/秒
    const remainingBytes = task.total - currentLoaded
    const remainingSeconds = Math.ceil(remainingBytes / avgSpeed)

    // 格式化时间
    return this.formatTime(remainingSeconds)
  }

  /**
   * 格式化时间(秒 → 时:分:秒 或 分:秒)
   * @param {number} seconds 秒数
   * @returns {string} 格式化后的时间
   */
  formatTime(seconds) {
    if (seconds < 0) return '00:00'
    const h = Math.floor(seconds / 3600)
    const m = Math.floor((seconds % 3600) / 60)
    const s = seconds % 60
    return h > 0
      ? `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
      : `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
  }

  /**
   * 处理下载完成
   * @param {Object} task 任务实例
   * @param {AxiosResponse} response 响应对象
   */
  handleComplete(task, response) {
    task.status = 'completed'
    // 合并断点续传的blob(如果有多次暂停)
    const fileBlob = new Blob([response.data])
    task.onComplete?.(fileBlob)
    // 自动触发下载(可选)
    this.triggerDownload(fileBlob, task.filename)
  }

  /**
   * 处理下载错误
   * @param {Object} task 任务实例
   * @param {Error} error 错误对象
   */
  handleError(task, error) {
    if (error.message === 'paused') {
      // 暂停不算错误,不触发onError
      return
    } else if (error.message === 'cancelled') {
      task.onError?.('下载已取消')
    } else if (axios.isCancel(error)) {
      task.onError?.('下载已终止')
    } else {
      task.onError?.(`下载失败:${error.message || '未知错误'}`)
    }
    task.status = 'failed'
  }

  /**
   * 触发浏览器下载
   * @param {Blob} blob 文件blob
   * @param {string} filename 文件名
   */
  triggerDownload(blob, filename) {
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = filename
    a.click()
    URL.revokeObjectURL(url)
  }

  /**
   * 获取任务状态
   * @param {string} taskId 任务ID
   * @returns {Object} 任务状态
   */
  getTaskStatus(taskId) {
    const task = this.tasks.get(taskId)
    if (!task) return null
    return {
      id: task.id,
      status: task.status,
      percent: task.total > 0 ? Math.round((task.loaded / task.total) * 100) : 0,
      loaded: task.loaded,
      total: task.total,
    }
  }
}

// 导出单例实例(全局唯一下载管理器)
export default new DownloadManager()

  • 使用
import downloadManager from './DownloadManager';

// 1. 创建下载任务
const taskId = downloadManager.createTask({
  url: '/api/download/large-file.zip',
  filename: '大型文件.zip',
  onProgress: (percent, remainingTime) => {
    console.log(`进度:${percent}% | 剩余时间:${remainingTime}`);
    // 更新UI进度条和时间显示
  },
  onComplete: (blob) => {
    console.log('下载完成', blob);
   
  },
  onError: (msg) => {
    console.error('下载错误:', msg);
  }
});

// 2. 开始下载(按钮点击事件中调用)
document.getElementById('startBtn').addEventListener('click', () => {
  downloadManager.start(taskId);
});

// 3. 暂停下载(按钮点击事件中调用)
document.getElementById('pauseBtn').addEventListener('click', () => {
  downloadManager.pause(taskId);
});

// 4. 取消下载(按钮点击事件中调用)
document.getElementById('cancelBtn').addEventListener('click', () => {
  downloadManager.cancel(taskId);
});

// 5. 获取任务状态(可选)
setInterval(() => {
  const status = downloadManager.getTaskStatus(taskId);
  if (status) console.log('当前状态:', status);
}, 1000);
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码工人笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值