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);
2740

被折叠的 条评论
为什么被折叠?



