vue3:vue3 + elementplus + pinia实现js的XMLHttpRequest 下载功能。
问:
使用js的XMLHttpRequest 方法,在vue3+ts+elementplus+pinia中实现一个记录下载功能:包括下载进度、下载速度、下载剩余时间、下载进度条、取消按钮取消下载、下载文件名、下载状态(下载中、已取消、下载失败、已完成),实现一个公共组件使用el-table来展示下载的文件可以有多个同时下载,最好用pinia进行多个下载文件的数据存储。
回答:
下面是一个基于 Vue 3 + TypeScript + Element Plus + Pinia 的完整示例,使用
XMLHttpRequest
实现 多文件下载管理功能,包含:
- 下载进度、速度、剩余时间
- 下载进度条
- 可取消下载
- 文件名、状态(下载中、已取消、下载失败、已完成)
- 使用
el-table
展示多个下载任务- 使用 Pinia 管理下载状态
- 公共可复用的组件
✅ 一、项目结构
src/ ├── stores/ │ └── downloadStore.ts ├── components/ │ └── DownloadManager.vue ├── utils/ │ └── downloadHelper.ts
✅ 二、Pinia Store(
src/stores/downloadStore.ts
)// src/stores/downloadStore.ts import { defineStore } from 'pinia'export interface DownloadTask {id: stringurl: stringfilename: stringstatus: 'pending' | 'downloading' | 'completed' | 'canceled' | 'failed'progress: number // 0-100speed: string // e.g., "1.2 MB/s"remainingTime: string // e.g., "30s"cancelToken: (() => void) | nullxhr: XMLHttpRequest | null }export const useDownloadStore = defineStore('download', {state: () => ({tasks: [] as DownloadTask[],}),actions: {addTask(url: string, filename?: string) {const id = Date.now().toString() + Math.random().toString(36).substr(2, 9)const name = filename || url.split('/').pop() || 'unknown-file'const task: DownloadTask = {id,url,filename: name,status: 'pending',progress: 0,speed: '0 KB/s',remainingTime: '--',cancelToken: null,xhr: null,}this.tasks.push(task)return id},updateTask(id: string, data: Partial<DownloadTask>) {const task = this.tasks.find(t => t.id === id)if (task) Object.assign(task, data)},removeTask(id: string) {this.tasks = this.tasks.filter(t => t.id !== id)},cancelTask(id: string) {const task = this.tasks.find(t => t.id === id)if (task && task.cancelToken) {task.cancelToken()task.status = 'canceled'}},clearCompleted() {this.tasks = this.tasks.filter(t => !['completed', 'canceled', 'failed'].includes(t.status))},}, })
✅ 三、下载工具类(
src/utils/downloadHelper.ts
)// src/utils/downloadHelper.ts export interface ProgressData {progress: numberspeed: number // bytes per secondremainingTime: number // seconds }export type OnProgress = (data: ProgressData) => void export type OnComplete = () => void export type OnError = (error: Error) => voidexport function startDownload(url: string,filename: string,onProgress: OnProgress,onComplete: OnComplete,onError: OnError ): { xhr: XMLHttpRequest; cancel: () => void } {const xhr = new XMLHttpRequest()let startTime = Date.now()let loadedBytes = 0let totalBytes = 0xhr.open('GET', url, true)xhr.responseType = 'blob'// 监听下载进度xhr.onprogress = (e) => {if (e.lengthComputable) {loadedBytes = e.loadedtotalBytes = e.totalconst now = Date.now()const timeElapsed = (now - startTime) / 1000 // secondsconst speed = timeElapsed > 0 ? loadedBytes / timeElapsed : 0const progress = (loadedBytes / totalBytes) * 100let remainingTime = 0if (speed > 0) {remainingTime = (totalBytes - loadedBytes) / speed}onProgress({progress,speed,remainingTime,})}}xhr.onload = () => {if (xhr.status === 200) {const blob = xhr.responseconst link = document.createElement('a')link.href = window.URL.createObjectURL(blob)link.download = filenamelink.click()window.URL.revokeObjectURL(link.href)onComplete()} else {onError(new Error(`HTTP ${xhr.status}`))}}xhr.onerror = () => {onError(new Error('Network error'))}xhr.onabort = () => {onError(new Error('Download canceled'))}xhr.send()return {xhr,cancel: () => xhr.abort(),} }
✅ 四、公共组件:
DownloadManager.vue
<!-- src/components/DownloadManager.vue --> <template><div class="download-manager"><h3>下载管理器</h3><el-table :data="tasks" style="width: 100%" max-height="400"><el-table-column prop="filename" label="文件名" width="200" /><el-table-column label="进度" width="200"><template #default="{ row }"><el-progress :percentage="row.progress" :stroke-width="16" /></template></el-table-column><el-table-column prop="speed" label="速度" width="100" /><el-table-column prop="remainingTime" label="剩余时间" width="100" /><el-table-column prop="status" label="状态" width="100"><template #default="{ row }"><el-tag size="small" :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag></template></el-table-column><el-table-column label="操作" width="120"><template #default="{ row }"><el-buttonv-if="row.status === 'downloading'"size="small"@click="cancelDownload(row.id)"type="danger"plain>取消</el-button><el-buttonv-else-if="row.status === 'completed'"size="small"@click="removeTask(row.id)"type="success"plain>移除</el-button><el-buttonv-elsesize="small"@click="removeTask(row.id)"type="info"plain>移除</el-button></template></el-table-column></el-table><div style="margin-top: 10px;"><el-button size="small" @click="clearCompleted" :disabled="tasks.length === 0">清除完成项</el-button></div></div> </template><script setup lang="ts"> import { computed, onMounted, onUnmounted } from 'vue' import { useDownloadStore } from '@/stores/downloadStore' import { startDownload } from '@/utils/downloadHelper'const store = useDownloadStore()// 格式化字节 const formatBytes = (bytes: number): string => {if (bytes === 0) return '0 KB'const k = 1024const sizes = ['B', 'KB', 'MB', 'GB']const i = Math.floor(Math.log(bytes) / Math.log(k))return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] }// 格式化速度 const formatSpeed = (bytesPerSec: number): string => {if (bytesPerSec < 1) return '0 KB/s'return formatBytes(bytesPerSec) + '/s' }// 格式化剩余时间 const formatTime = (seconds: number): string => {if (seconds < 0) return '--'const mins = Math.floor(seconds / 60)const secs = Math.floor(seconds % 60)if (mins > 0) return `${mins}m ${secs}s`return `${secs}s` }// 状态映射 const getStatusText = (status: string): string => {const map: Record<string, string> = {pending: '等待中',downloading: '下载中',completed: '已完成',canceled: '已取消',failed: '下载失败',}return map[status] || status }const getStatusType = (status: string): string => {const map: Record<string, string> = {pending: 'info',downloading: 'primary',completed: 'success',canceled: 'warning',failed: 'danger',}return map[status] || 'info' }// 开始下载任务 const startTask = (id: string) => {const task = store.tasks.find(t => t.id === id)if (!task) returnstore.updateTask(id, { status: 'downloading' })const { xhr, cancel } = startDownload(task.url,task.filename,(progressData) => {const speedStr = formatSpeed(progressData.speed)const timeStr = formatTime(progressData.remainingTime)store.updateTask(id, {progress: progressData.progress,speed: speedStr,remainingTime: timeStr,})},() => {store.updateTask(id, { status: 'completed', progress: 100 })},(error) => {console.error(`Download failed: ${error.message}`)store.updateTask(id, { status: 'failed' })})// 保存引用以便取消store.updateTask(id, { xhr, cancelToken: cancel }) }// 取消下载 const cancelDownload = (id: string) => {store.cancelTask(id) }// 移除任务 const removeTask = (id: string) => {store.removeTask(id) }// 清除已完成/已取消/失败的任务 const clearCompleted = () => {store.clearCompleted() }// 自动启动所有 pending 任务(可扩展为队列) const startPendingTasks = () => {const pendingTasks = store.tasks.filter(t => t.status === 'pending')pendingTasks.forEach(task => startTask(task.id)) }// 挂载时启动所有待下载任务 onMounted(() => {startPendingTasks() })// 定期清理已完成任务(可选) // onUnmounted(() => {})// 响应式任务列表 const tasks = computed(() => store.tasks) </script><style scoped> .download-manager {padding: 10px;border: 1px solid #ebeef5;border-radius: 4px; } </style>
✅ 五、如何使用(在任意页面中)
<!-- ExamplePage.vue --> <template><div><h2>文件下载示例</h2><el-button @click="addDownload">添加下载任务</el-button><!-- 引入下载管理器 --><DownloadManager /></div> </template><script setup lang="ts"> import { useDownloadStore } from '@/stores/downloadStore' import DownloadManager from '@/components/DownloadManager.vue'const store = useDownloadStore()const addDownload = () => {const url = 'https://example.com/large-file.zip' // 替换为真实可下载链接const filename = 'demo-' + Date.now() + '.zip'const id = store.addTask(url, filename)// 任务添加后,DownloadManager 会自动开始下载 } </script>
✅ 六、功能亮点
功能 实现方式 多任务并发 Pinia 存储任务列表,每个任务独立 xhr 进度条 XMLHttpRequest.onprogress
+el-progress
速度计算 (已下载字节数) / (耗时秒数)
剩余时间 (剩余字节) / (当前速度)
取消下载 xhr.abort()
+ 状态更新文件名 从 URL 或手动指定 状态管理 Pinia 统一维护 UI 展示 el-table
+el-progress
+el-tag
✅ 注意事项
- CORS 限制:
XMLHttpRequest
下载受跨域限制,确保后端支持 CORS。- 大文件内存:Blob 下载大文件可能占用较多内存,建议结合流式处理(但浏览器限制)。
- 取消后无法恢复:
abort()
后任务不可继续。- 文件名获取:更优方式是读取
Content-Disposition
header,需后端配合。
✅ 可扩展建议
- 支持暂停/恢复(需后端支持
Range
请求)- 限制同时下载数量(任务队列)
- 持久化任务(
localStorage
)
这个方案是一个生产可用的下载管理器雏形,你可以根据项目需求进一步优化 UI 和逻辑