django+vue3实现前后端大文件分片下载

效果:
在这里插入图片描述

大文件分片下载支持的功能:

  • 展示目标文件信息
  • 提高下载速度:通过并发请求多个块,可以更有效地利用网络带宽
  • 断点续传:支持暂停后从已下载部分继续,无需重新开始
  • 错误恢复:单个块下载失败只需重试该块,而不是整个文件
  • 更好的用户体验:实时显示下载进度、速度和预计剩余时间
  • 内存效率:通过分块下载和处理,减少了一次性内存占用

大文件分片下载

前端处理流程:

用户操作
重试次数<最大值
达到最大重试
取消当前请求
保存下载状态
暂停下载
重新开始未完成分块
恢复下载
取消所有请求
重置所有状态
取消下载
开始
获取文件信息
计算总分块数
初始化分块状态
并发下载多个分块
分块下载完成?
下载下一个分块
重试逻辑
错误处理
所有分块下载完成?
合并所有分块
创建完整文件
结束

后端处理流程:

后端
前端
API调用
API调用
返回文件元数据
file_info API
解析Range头
download_large_file API
定位文件指针
流式读取响应字节范围
返回206状态码和数据
获取文件信息
初始化下载器
计算分块策略
创建并发下载队列
发送Range请求
所有分块下载完成?
合并分块并下载

django代码

1,代码

# settings.py
# 指定文件访问的 URL 前缀
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media/'# views.py
import os
import mimetypes
from django.conf import settings
from django.http import StreamingHttpResponse, JsonResponse, HttpResponse
from django.utils.http import http_date
from django.views.decorators.http import require_http_methodsdef get_file_info(file_path):"""获取文件信息:- name: 文件名- size: 文件大小,单位字节- type: 文件类型"""if not os.path.exists(file_path):return Nonefile_size = os.path.getsize(file_path)file_name = os.path.basename(file_path)content_type, encoding = mimetypes.guess_type(file_path)return {'name': file_name,'size': file_size,'type': content_type or 'application/octet-stream'}@require_http_methods(["GET"])
def file_info(request):"""获取文件信息API"""file_path = os.path.join(settings.MEDIA_ROOT, "user_info_big.csv")info = get_file_info(file_path)if info is None:return JsonResponse({"error": "File not found"}, status=404)return JsonResponse(info)@require_http_methods(["GET"])
def download_large_file(request):"""分片下载文件的API:param request: 请求对象:return: 文件流"""file_path = os.path.join(settings.MEDIA_ROOT, "user_info_big.csv")# 1,检查文件是否存在if not os.path.exists(file_path):return HttpResponse("File not found", status=404)# 2,获取文件信息file_size = os.path.getsize(file_path)file_name = os.path.basename(file_path)content_type, encoding = mimetypes.guess_type(file_path)content_type = content_type or 'application/octet-stream'# 3,获取请求中的Range头range_header = request.META.get('HTTP_RANGE', '').strip()# 格式:bytes=0-100range_match = range_header.replace('bytes=', '').split('-')# 起始位置range_start = int(range_match[0]) if range_match[0] else 0# 结束位置range_end = int(range_match[1]) if range_match[1] else file_size - 1# 4,确保范围合法range_start = max(0, range_start)range_end = min(file_size - 1, range_end)# 5,计算实际要发送的数据大小content_length = range_end - range_start + 1# 6,创建响应:使用StreamingHttpResponse,将文件流式传输。206表示部分内容,200表示全部内容response = StreamingHttpResponse(file_iterator(file_path, range_start, range_end, chunk_size=8192),status=206 if range_header else 200,content_type=content_type)# 7,设置响应头response['Content-Length'] = content_lengthresponse['Accept-Ranges'] = 'bytes'response['Content-Disposition'] = f'attachment; filename="{file_name}"'if range_header:response['Content-Range'] = f'bytes {range_start}-{range_end}/{file_size}'response['Last-Modified'] = http_date(os.path.getmtime(file_path))# 模拟处理延迟,方便测试暂停/继续功能# time.sleep(0.1)  # 取消注释以添加人为延迟# 8,返回响应return responsedef file_iterator(file_path, start_byte=0, end_byte=None, chunk_size=8192):"""文件读取迭代器:param file_path: 文件路径:param start_byte: 起始字节:param end_byte: 结束字节:param chunk_size: 块大小"""with open(file_path, 'rb') as f:# 移动到起始位置f.seek(start_byte)# 计算剩余字节数remaining = end_byte - start_byte + 1 if end_byte else Nonewhile True:if remaining is not None:# 如果指定了结束位置,则读取剩余字节或块大小,取小的那个bytes_to_read = min(chunk_size, remaining)if bytes_to_read <= 0:breakelse:# 否则读取指定块大小bytes_to_read = chunk_sizedata = f.read(bytes_to_read)if not data:breakyield dataif remaining is not None:remaining -= len(data)# proj urls.py
from django.urls import path, includeurlpatterns = [# 下载文件path('download/', include(('download.urls', 'download'), namespace='download')),
]# download.urls.py
from django.urls import pathfrom download import viewsurlpatterns = [path('large_file/file_info/', views.file_info, name='file_info'),path('large_file/download_large_file/', views.download_large_file, name='download_large_file'),
]

2,核心功能解析

(1)file_info 端点 - 获取文件元数据

这个端点提供文件的基本信息,让前端能够规划下载策略:

  • 功能:返回文件名称、大小和MIME类型
  • 用途:前端根据文件大小和设置的块大小计算出需要下载的分块数量

(2)download_large_file 端点 - 实现分片下载

这是实现分片下载的核心API,通过HTTP Range请求实现:
1,解析Range头:从HTTP_RANGE头部解析客户端请求的字节范围

range_header = request.META.get('HTTP_RANGE', '').strip()
range_match = range_header.replace('bytes=', '').split('-')
range_start = int(range_match[0]) if range_match[0] else 0
range_end = int(range_match[1]) if range_match[1] else file_size - 1

2,流式传输:使用StreamingHttpResponse和迭代器按块读取和传输文件,避免一次加载整个文件到内存

response = StreamingHttpResponse(file_iterator(file_path, range_start, range_end, chunk_size=8192),status=206 if range_header else 200,content_type=content_type
)

3,返回响应头:设置必要的响应头,包括Content-Range指示返回内容的范围

response['Content-Range'] = f'bytes {range_start}-{range_end}/{file_size}'

(3)file_iterator 函数 - 高效的文件读取

这个函数创建一个迭代器,高效地读取文件的指定部分:

1,文件定位:将文件指针移动到请求的起始位置

f.seek(start_byte)

2,分块读取:按指定的块大小读取文件,避免一次性读取大量数据

data = f.read(bytes_to_read)

3,边界控制:确保只读取请求范围内的数据

remaining -= len(data)

HTTP状态码和响应头的作用

1,206 Partial Content:

  • 表示服务器成功处理了部分GET请求
  • 分片下载的标准HTTP状态码

2,Content-Range: bytes start-end/total:

  • 指示响应中包含的字节范围和文件总大小
  • 帮助客户端确认接收的是哪部分数据

3,Accept-Ranges: bytes:

  • 表明服务器支持范围请求
  • 让客户端知道可以使用Range头请求部分内容

4,Content-Length:

  • 表示当前响应内容的长度
  • 不是文件总长度,而是本次返回的片段长度

vue3代码

1,代码

1,前端界面 (Vue组件):

  • 提供配置选项:并发块数、块大小
  • 显示下载进度:进度条、已下载量、下载速度、剩余时间提供操作按钮:开始、暂停、继续、取消
  • 可视化显示每个分块的下载状态
<template><div class="enhanced-downloader"><div class="card"><h2>大文件分块下载</h2><div class="file-info" v-if="fileInfo"><p><strong>文件名:</strong> {{ fileInfo.name }}</p><p><strong>文件大小:</strong> {{ formatFileSize(fileInfo.size) }}</p><p><strong>类型:</strong> {{ fileInfo.type }}</p></div><div class="config-panel" v-if="!isDownloading && !isPaused"><div class="config-item"><label>并发块数:</label><select v-model="concurrency"><option :value="1">1</option><option :value="2">2</option><option :value="3">3</option><option :value="5">5</option><option :value="8">8</option></select></div><div class="config-item"><label>块大小:</label><select v-model="chunkSize"><option :value="512 * 1024">512 KB</option><option :value="1024 * 1024">1 MB</option><option :value="2 * 1024 * 1024">2 MB</option><option :value="5 * 1024 * 1024">5 MB</option></select></div></div><div class="progress-container" v-if="isDownloading || isPaused"><div class="progress-bar"><div class="progress" :style="{ width: `${progress}%` }"></div></div><div class="progress-stats"><div class="stat-item"><span class="label">进度:</span><span class="value">{{ progress.toFixed(2) }}%</span></div><div class="stat-item"><span class="label">已下载:</span><span class="value">{{ formatFileSize(downloadedBytes) }} / {{ formatFileSize(totalBytes) }}</span></div><div class="stat-item"><span class="label">速度:</span><span class="value">{{ downloadSpeed }}</span></div><div class="stat-item"><span class="label">已完成块:</span><span class="value">{{ downloadedChunks }} / {{ totalChunks }}</span></div><div class="stat-item"><span class="label">剩余时间:</span><span class="value">{{ remainingTime }}</span></div></div></div><div class="chunk-visualization" v-if="isDownloading || isPaused"><div class="chunk-grid"><divv-for="(chunk, index) in chunkStatus":key="index"class="chunk-block":class="{'downloaded': chunk === 'completed','downloading': chunk === 'downloading','pending': chunk === 'pending','error': chunk === 'error'}":title="`块 ${index + 1}: ${chunk}`"></div></div></div><div class="actions"><button@click="startDownload":disabled="isDownloading"v-if="!isDownloading && !isPaused"class="btn btn-primary">开始下载</button><button@click="pauseDownload":disabled="!isDownloading"v-if="isDownloading && !isPaused"class="btn btn-warning">暂停</button><button@click="resumeDownload":disabled="!isPaused"v-if="isPaused"class="btn btn-success">继续</button><button@click="cancelDownload":disabled="!isDownloading && !isPaused"class="btn btn-danger">取消</button></div><div class="status-message" v-if="statusMessage">{{ statusMessage }}</div></div></div>
</template><script setup>
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
import {ChunkDownloader} from './downloadService';// API URL
const API_BASE_URL = 'http://localhost:8000/download/';// 下载配置
const concurrency = ref(3);
const chunkSize = ref(1024 * 1024); // 1MB
const downloader = ref(null);// 状态变量
const fileInfo = ref(null);
const isDownloading = ref(false);
const isPaused = ref(false);
const downloadedBytes = ref(0);
const totalBytes = ref(0);
const downloadedChunks = ref(0);
const totalChunks = ref(0);
const statusMessage = ref('准备就绪');
const downloadStartTime = ref(0);
const lastUpdateTime = ref(0);
const lastBytes = ref(0);
const downloadSpeed = ref('0 KB/s');
const remainingTime = ref('计算中...');
const speedInterval = ref(null);// 块状态
const chunkStatus = ref([]);// 计算下载进度百分比
const progress = computed(() => {if (totalBytes.value === 0) return 0;return (downloadedBytes.value / totalBytes.value) * 100;
});// 初始化
onMounted(async () => {try {await fetchFileInfo();} catch (error) {console.error('获取文件信息失败:', error);statusMessage.value = `获取文件信息失败: ${error.message}`;}
});// 清理资源
onUnmounted(() => {if (downloader.value) {downloader.value.cancel();}clearInterval(speedInterval.value);
});// 获取文件信息
async function fetchFileInfo() {const response = await fetch(`${API_BASE_URL}large_file/file_info/`);if (!response.ok) {throw new Error(`HTTP 错误! 状态码: ${response.status}`);}fileInfo.value = await response.json();totalBytes.value = fileInfo.value.size;// 根据文件大小初始化分块状态const initialTotalChunks = Math.ceil(fileInfo.value.size / chunkSize.value);chunkStatus.value = Array(initialTotalChunks).fill('pending');totalChunks.value = initialTotalChunks;
}// 开始下载
async function startDownload() {if (isDownloading.value) return;try {// 初始化下载器downloader.value = new ChunkDownloader(`${API_BASE_URL}`, {chunkSize: chunkSize.value,concurrency: concurrency.value,maxRetries: 3,onProgress: handleProgress,onComplete: handleComplete,onError: handleError,onStatusChange: handleStatusChange});// 初始化状态downloadedBytes.value = 0;downloadedChunks.value = 0;isDownloading.value = true;isPaused.value = false;statusMessage.value = '准备下载...';// 获取文件信息await downloader.value.fetchFileInfo();// 更新总块数totalChunks.value = Math.ceil(downloader.value.fileSize / chunkSize.value);chunkStatus.value = Array(totalChunks.value).fill('pending');// 开始下载downloadStartTime.value = Date.now();lastUpdateTime.value = Date.now();lastBytes.value = 0;startSpeedCalculator();await downloader.value.start();} catch (error) {console.error('下载启动失败:', error);statusMessage.value = `下载启动失败: ${error.message}`;isDownloading.value = false;}
}// 暂停下载
function pauseDownload() {if (!isDownloading.value || !downloader.value) return;downloader.value.pause();isDownloading.value = false;isPaused.value = true;statusMessage.value = '下载已暂停';clearInterval(speedInterval.value);
}// 继续下载
function resumeDownload() {if (!isPaused.value || !downloader.value) return;downloader.value.resume();isDownloading.value = true;isPaused.value = false;statusMessage.value = '继续下载...';// 重新开始速度计算lastUpdateTime.value = Date.now();lastBytes.value = downloadedBytes.value;startSpeedCalculator();
}// 取消下载
function cancelDownload() {if (!downloader.value) return;downloader.value.cancel();isDownloading.value = false;isPaused.value = false;downloadedBytes.value = 0;downloadedChunks.value = 0;statusMessage.value = '下载已取消';clearInterval(speedInterval.value);// 重置块状态chunkStatus.value = Array(totalChunks.value).fill('pending');
}// 处理进度更新
function handleProgress(data) {downloadedBytes.value = data.downloadedBytes;downloadedChunks.value = data.downloadedChunks;// 更新块状态(这里仅是简化的更新方式,实际上应该由downloader提供精确的块状态)const newChunkStatus = [...chunkStatus.value];const completedChunksCount = Math.floor(downloadedChunks.value);for (let i = 0; i < newChunkStatus.length; i++) {if (i < completedChunksCount) {newChunkStatus[i] = 'completed';} else if (i < completedChunksCount + concurrency.value && newChunkStatus[i] !== 'completed') {newChunkStatus[i] = 'downloading';}}chunkStatus.value = newChunkStatus;
}// 处理下载完成
function handleComplete(data) {isDownloading.value = false;isPaused.value = false;statusMessage.value = '下载完成';clearInterval(speedInterval.value);// 标记所有块为已完成chunkStatus.value = Array(totalChunks.value).fill('completed');
}// 处理错误
function handleError(error) {console.error('下载错误:', error);statusMessage.value = `下载错误: ${error.message}`;
}// 处理状态变化
function handleStatusChange(status, error) {switch (status) {case 'downloading':isDownloading.value = true;isPaused.value = false;statusMessage.value = '下载中...';break;case 'paused':isDownloading.value = false;isPaused.value = true;statusMessage.value = '下载已暂停';break;case 'completed':isDownloading.value = false;isPaused.value = false;statusMessage.value = '下载完成';break;case 'error':isDownloading.value = false;statusMessage.value = `下载错误: ${error?.message || '未知错误'}`;break;}
}// 启动下载速度计算器
function startSpeedCalculator() {clearInterval(speedInterval.value);speedInterval.value = setInterval(() => {const now = Date.now();const timeElapsed = (now - lastUpdateTime.value) / 1000; // 转换为秒const bytesDownloaded = downloadedBytes.value - lastBytes.value;if (timeElapsed > 0) {const speed = bytesDownloaded / timeElapsed; // 字节/秒downloadSpeed.value = formatFileSize(speed) + '/s';// 计算剩余时间if (speed > 0) {const bytesRemaining = totalBytes.value - downloadedBytes.value;const secondsRemaining = bytesRemaining / speed;remainingTime.value = formatTime(secondsRemaining);} else {remainingTime.value = '计算中...';}lastUpdateTime.value = now;lastBytes.value = downloadedBytes.value;}}, 1000);
}// 格式化文件大小
function formatFileSize(bytes) {if (bytes === 0) return '0 B';const k = 1024;const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];const i = Math.floor(Math.log(bytes) / Math.log(k));return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}// 格式化时间
function formatTime(seconds) {if (!isFinite(seconds) || seconds < 0) {return '计算中...';}if (seconds < 60) {return `${Math.ceil(seconds)}`;} else if (seconds < 3600) {const minutes = Math.floor(seconds / 60);const secs = Math.ceil(seconds % 60);return `${minutes}${secs}`;} else {const hours = Math.floor(seconds / 3600);const minutes = Math.floor((seconds % 3600) / 60);return `${hours}小时${minutes}分钟`;}
}// 监听配置改变,更新块状态
watch([chunkSize], () => {if (fileInfo.value && fileInfo.value.size) {totalChunks.value = Math.ceil(fileInfo.value.size / chunkSize.value);chunkStatus.value = Array(totalChunks.value).fill('pending');}
});
</script><style scoped>
.enhanced-downloader {max-width: 800px;margin: 0 auto;padding: 20px;
}.card {background: #fff;border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);padding: 20px;
}h2 {margin-top: 0;color: #333;
}.file-info {margin-bottom: 20px;
}.progress-container {margin-bottom: 20px;
}.progress-bar {height: 20px;background-color: #f0f0f0;border-radius: 10px;overflow: hidden;margin-bottom: 10px;
}.progress {height: 100%;background-color: #4CAF50;transition: width 0.3s ease;
}.progress-stats {display: flex;justify-content: space-between;font-size: 14px;color: #666;
}.actions {display: flex;gap: 10px;margin-bottom: 20px;
}.btn {padding: 8px 16px;border: none;border-radius: 4px;cursor: pointer;font-weight: bold;transition: background-color 0.3s;
}.btn:disabled {opacity: 0.5;cursor: not-allowed;
}.btn-primary {background-color: #4CAF50;color: white;
}.btn-warning {background-color: #FF9800;color: white;
}.btn-success {background-color: #2196F3;color: white;
}.btn-danger {background-color: #F44336;color: white;
}.status {font-style: italic;color: #666;
}
</style>

2,下载服务 (ChunkDownloader类):

  • 负责管理整个下载过程
  • 处理文件信息获取、分块下载、进度追踪
  • 实现并发控制、重试机制、暂停/继续功能
// downloadService.js - 分块下载实现/*文件分块下载器*/
export class ChunkDownloader {constructor(url, options = {}) {this.url = url;this.chunkSize = options.chunkSize || 1024 * 1024; // 默认1MB每块this.maxRetries = options.maxRetries || 3;this.concurrency = options.concurrency || 3; // 并发下载块数this.timeout = options.timeout || 30000; // 超时时间this.fileSize = 0;this.fileName = '';this.contentType = '';this.chunks = [];this.downloadedChunks = 0;this.activeDownloads = 0;this.totalChunks = 0;this.downloadedBytes = 0;this.status = 'idle'; // idle, downloading, paused, completed, errorthis.error = null;this.onProgress = options.onProgress || (() => {});this.onComplete = options.onComplete || (() => {});this.onError = options.onError || (() => {});this.onStatusChange = options.onStatusChange || (() => {});this.abortControllers = new Map();this.pendingChunks = [];this.processedChunks = new Set();}// 获取文件信息async fetchFileInfo() {try {const response = await fetch(this.url + 'large_file/file_info/');if (!response.ok) {throw new Error(`无法获取文件信息: ${response.status}`);}const info = await response.json();this.fileSize = info.size;this.fileName = info.name;this.contentType = info.type;// 计算分块数量this.totalChunks = Math.ceil(this.fileSize / this.chunkSize);return info;} catch (error) {this.error = error;this.status = 'error';this.onStatusChange(this.status, error);this.onError(error);throw error;}}// 开始下载async start() {if (this.status === 'downloading') {return;}try {// 如果还没获取文件信息,先获取if (this.fileSize === 0) {await this.fetchFileInfo();}// 初始化状态this.status = 'downloading';this.onStatusChange(this.status);// 如果是全新下载,初始化块数组if (this.chunks.length === 0) {this.chunks = new Array(this.totalChunks).fill(null);this.pendingChunks = Array.from({length: this.totalChunks}, (_, i) => i);}// 开始并发下载this.startConcurrentDownloads();} catch (error) {this.error = error;this.status = 'error';this.onStatusChange(this.status, error);this.onError(error);}}// 开始并发下载startConcurrentDownloads() {// 确保同时只有指定数量的并发下载while (this.activeDownloads < this.concurrency && this.pendingChunks.length > 0) {const chunkIndex = this.pendingChunks.shift();this.downloadChunk(chunkIndex);}}// 下载指定的块async downloadChunk(chunkIndex, retryCount = 0) {if (this.status !== 'downloading' || this.processedChunks.has(chunkIndex)) {return;}this.activeDownloads++;const startByte = chunkIndex * this.chunkSize;const endByte = Math.min(startByte + this.chunkSize - 1, this.fileSize - 1);// 创建用于取消请求的控制器const controller = new AbortController();this.abortControllers.set(chunkIndex, controller);try {const response = await fetch(this.url + 'large_file/download_large_file/',{method: 'GET',headers: {'Range': `bytes=${startByte}-${endByte}`},signal: controller.signal,timeout: this.timeout});if (!response.ok && response.status !== 206) {throw new Error(`服务器错误: ${response.status}`);}// 获取块数据const blob = await response.blob();this.chunks[chunkIndex] = blob;this.downloadedChunks++;this.downloadedBytes += blob.size;this.processedChunks.add(chunkIndex);// 更新进度this.onProgress({downloadedChunks: this.downloadedChunks,totalChunks: this.totalChunks,downloadedBytes: this.downloadedBytes,totalBytes: this.fileSize,progress: (this.downloadedBytes / this.fileSize) * 100});// 清理控制器this.abortControllers.delete(chunkIndex);// 检查是否下载完成if (this.downloadedChunks === this.totalChunks) {this.completeDownload();} else if (this.status === 'downloading') {// 继续下载下一个块this.activeDownloads--;this.startConcurrentDownloads();}} catch (error) {this.abortControllers.delete(chunkIndex);if (error.name === 'AbortError') {// 用户取消,不进行重试this.activeDownloads--;return;}// 重试逻辑if (retryCount < this.maxRetries) {console.warn(`${chunkIndex} 下载失败,重试 ${retryCount + 1}/${this.maxRetries}`);this.activeDownloads--;this.downloadChunk(chunkIndex, retryCount + 1);} else {console.error(`${chunkIndex} 下载失败,已达到最大重试次数`);this.error = error;this.status = 'error';this.onStatusChange(this.status, error);this.onError(error);this.activeDownloads--;}}}// 完成下载completeDownload() {if (this.status === 'completed') {return;}try {// 合并所有块const blob = new Blob(this.chunks, {type: this.contentType});// 创建下载链接const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = this.fileName;document.body.appendChild(a);a.click();document.body.removeChild(a);// 清理资源setTimeout(() => URL.revokeObjectURL(url), 100);// 更新状态this.status = 'completed';this.onStatusChange(this.status);this.onComplete({fileName: this.fileName,fileSize: this.fileSize,contentType: this.contentType,blob: blob});} catch (error) {this.error = error;this.status = 'error';this.onStatusChange(this.status, error);this.onError(error);}}// 暂停下载pause() {if (this.status !== 'downloading') {return;}// 取消所有正在进行的请求this.abortControllers.forEach(controller => {controller.abort();});// 清空控制器集合this.abortControllers.clear();// 更新状态this.status = 'paused';this.activeDownloads = 0;this.onStatusChange(this.status);// 将当前处理中的块重新加入待处理队列this.pendingChunks = Array.from({length: this.totalChunks}, (_, i) => i).filter(i => !this.processedChunks.has(i));}// 继续下载resume() {if (this.status !== 'paused') {return;}this.status = 'downloading';this.onStatusChange(this.status);this.startConcurrentDownloads();}// 取消下载cancel() {// 取消所有正在进行的请求this.abortControllers.forEach(controller => {controller.abort();});// 重置所有状态this.chunks = [];this.downloadedChunks = 0;this.activeDownloads = 0;this.downloadedBytes = 0;this.status = 'idle';this.error = null;this.abortControllers.clear();this.pendingChunks = [];this.processedChunks.clear();this.onStatusChange(this.status);}// 获取当前状态getStatus() {return {status: this.status,downloadedChunks: this.downloadedChunks,totalChunks: this.totalChunks,downloadedBytes: this.downloadedBytes,totalBytes: this.fileSize,progress: this.fileSize ? (this.downloadedBytes / this.fileSize) * 100 : 0,fileName: this.fileName,error: this.error};}
}

2,核心技术原理

(1)HTTP Range请求

该实现通过HTTP的Range头部实现分块下载:

const response = await fetch(this.url + 'large_file/download_large_file/',{method: 'GET',headers: {'Range': `bytes=${startByte}-${endByte}`},signal: controller.signal,timeout: this.timeout});
  • 服务器会返回状态码206(Partial Content)和请求的文件片段。

(2)并发控制

代码通过控制同时活跃的下载请求数量来实现并发:

while (this.activeDownloads < this.concurrency && this.pendingChunks.length > 0) {const chunkIndex = this.pendingChunks.shift();this.downloadChunk(chunkIndex);
}

(3)状态管理和进度追踪

  • 跟踪每个块的下载状态(待下载、下载中、已完成、错误)
  • 计算并报告总体进度、下载速度和剩余时间

(4)错误处理和重试机制

对下载失败的块进行自动重试:

if (retryCount < this.maxRetries) {console.warn(`${chunkIndex} 下载失败,重试 ${retryCount + 1}/${this.maxRetries}`);this.activeDownloads--;this.downloadChunk(chunkIndex, retryCount + 1);
}

(5)暂停/恢复功能

通过AbortController取消活跃的请求,并保存未完成的块索引:

pause() {// 取消所有正在进行的请求this.abortControllers.forEach(controller => {controller.abort();});// 将当前处理中的块重新加入待处理队列this.pendingChunks = Array.from({length: this.totalChunks}, (_, i) => i).filter(i => !this.processedChunks.has(i));
}

(6)文件合并和下载

所有块下载完成后,使用Blob API合并所有分块并创建下载链接:

const blob = new Blob(this.chunks, {type: this.contentType});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = this.fileName;
a.click();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/72539.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

matlab中如何集成使用python

在 MATLAB 中集成使用 Python 可以通过调用 Python 脚本或函数来实现。MATLAB 提供了 py 模块来直接调用 Python 代码。以下是一个简单的示例&#xff0c;展示如何在 MATLAB 中调用 Python 函数。 示例&#xff1a;在 MATLAB 中调用 Python 函数 1. 编写 Python 函数 首先&a…

ICMP、UDP以及IP、ARP报文包的仲裁处理

在之前的章节中&#xff0c;笔者就UDP、ICMP、IP、ARP、MAC层以及巨型帧等做了详细介绍以及代码实现及仿真&#xff0c;从本章节开始&#xff0c;笔者将就各个模块组合在一起&#xff0c;实现UDP协议栈的整体收发&#xff0c;在实现模块的整体组合之前&#xff0c;还需要考虑一…

【大模型学习】第十九章 什么是迁移学习

目录 1. 迁移学习的起源背景 1.1 传统机器学习的问题 1.2 迁移学习的提出背景 2. 什么是迁移学习 2.1 迁移学习的定义 2.2 生活实例解释 3. 技术要点与原理 3.1 迁移学习方法分类 3.1.1 基于特征的迁移学习(Feature-based Transfer) 案例说明 代码示例 3.1.2 基于…

基于大模型的分泌性中耳炎全流程预测与治疗管理研究报告

目录 一、引言 1.1 研究背景与意义 1.2 研究目的与目标 1.3 研究方法与创新点 二、分泌性中耳炎概述 2.1 疾病定义与特征 2.2 发病原因与机制 2.3 疾病危害与影响 三、大模型技术原理与应用现状 3.1 大模型基本原理 3.2 在医疗领域的应用案例 3.3 选择大模型预测分…

【NLP 38、实践 ⑩ NER 命名实体识别任务 Bert 实现】

去做具体的事&#xff0c;然后稳稳托举自己 —— 25.3.17 数据文件&#xff1a; 通过网盘分享的文件&#xff1a;Ner命名实体识别任务 链接: https://pan.baidu.com/s/1fUiin2um4PCS5i91V9dJFA?pwdyc6u 提取码: yc6u --来自百度网盘超级会员v3的分享 一、配置文件 config.py …

蓝桥杯学习-11栈

11栈 先进后出 例题–蓝桥19877 用数组来设置栈 1.向栈顶插入元素--top位置标记元素 2.删除栈顶元素--top指针减减 3.输出栈顶元素--输出top位置元素使用arraylist import java.util.ArrayList; import java.util.Scanner;public class Main {public static void main(Str…

Linux 蓝牙音频软件栈实现分析

Linux 蓝牙音频软件栈实现分析 蓝牙协议栈简介蓝牙控制器探测BlueZ 插件系统及音频插件蓝牙协议栈简介 蓝牙协议栈是实现蓝牙通信功能的软件架构,它由多个层次组成,每一层负责特定的功能。蓝牙协议栈的设计遵循蓝牙标准 (由蓝牙技术联盟,Bluetooth SIG 定义),支持多种蓝牙…

JetBrains(全家桶: IDEA、WebStorm、GoLand、PyCharm) 2024.3+ 2025 版免费体验方案

JetBrains&#xff08;全家桶: IDEA、WebStorm、GoLand、PyCharm&#xff09; 2024.3 2025 版免费体验方案 前言 JetBrains IDE 是许多开发者的主力工具&#xff0c;但从 2024.02 版本起&#xff0c;JetBrains 调整了试用政策&#xff0c;新用户不再享有默认的 30 天免费试用…

1.8PageTable

页表的作用 虚拟地址空间映射&#xff1a;页表记录了进程的虚拟页号到物理页号的映射关系。每个进程都有自己的页表&#xff0c;操作系统为每个进程维护一个独立的页表。内存管理&#xff1a;页表用于实现虚拟内存管理&#xff0c;支持进程的虚拟地址空间和物理地址空间之间的…

Prosys OPC UA Gateway:实现 OPC Classic 与 OPC UA 无缝连接

在工业自动化的数字化转型中&#xff0c;设备与系统之间的高效通信至关重要。然而&#xff0c;许多企业仍依赖于基于 COM/DCOM 技术的 OPC 产品&#xff0c;这给与现代化的 OPC UA 架构的集成带来了挑战。 Prosys OPC UA Gateway 正是为解决这一问题而生&#xff0c;它作为一款…

数据结构------线性表

一、线性表顺序存储详解 &#xff08;一&#xff09;线性表核心概念 1. 结构定义 // 数据元素类型 typedef struct person {char name[32];char sex;int age;int score; } DATATYPE;// 顺序表结构 typedef struct list {DATATYPE *head; // 存储空间基地址int tlen; …

【WPF】在System.Drawing.Rectangle中限制鼠标保持在Rectangle中移动?

方案一&#xff0c;在OnMouseMove方法限制 在WPF应用程序中&#xff0c;鼠标在移动过程中保持在这个矩形区域内&#xff0c;可以通过监听鼠标的移动事件并根据鼠标的当前位置调整其坐标来实现。不过需要注意的是&#xff0c;WPF原生使用的是System.Windows.Rect而不是System.D…

基于银河麒麟系统ARM架构安装达梦数据库并配置主从模式

达梦数据库简要概述 达梦数据库&#xff08;DM Database&#xff09;是一款由武汉达梦公司开发的关系型数据库管理系统&#xff0c;支持多种高可用性和数据同步方案。在主从模式&#xff08;也称为 Master-Slave 或 Primary-Secondary 模式&#xff09;中&#xff0c;主要通过…

系统思考全球化落地

感谢加密货币公司Bybit的再次邀请&#xff0c;为全球团队分享系统思考课程&#xff01;虽然大家来自不同国家&#xff0c;线上学习的形式依然让大家充满热情与互动&#xff0c;思维的碰撞不断激发新的灵感。 尽管时间存在挑战&#xff0c;但我看到大家的讨论异常积极&#xff…

Figma的汉化

Figma的汉化插件有客户端版本与Chrome版本&#xff0c;大家可根据自己的需要进行选择。 下载插件 进入Figma软件汉化-Figma中文版下载-Figma中文社区使用客户端&#xff1a;直接下载客户端使用网页版&#xff1a;安装chrome浏览器汉化插件国外推荐前往chrome商店安装国内推荐下…

【Go语言圣经2.5】

目标 了解类型定义不仅告诉编译器如何在内存中存储和处理数据&#xff0c;还对程序设计产生深远影响&#xff1a; 内存结构&#xff1a;类型决定了变量的底层存储&#xff08;比如占用多少字节、内存布局等&#xff09;。操作符与方法集&#xff1a;类型决定了哪些内置运算符…

IDEA 一键完成:打包 + 推送 + 部署docker镜像

1、本方案要解决场景&#xff1f; 想直接通过本地 IDEA 将最新的代码部署到远程服务器上。 2、本方案适用于什么样的项目&#xff1f; 项目是一个 Spring Boot 的 Java 项目。项目用 maven 进行管理。项目的运行基于 docker 容器&#xff08;即项目将被打成 docker image&am…

SpringBoot 第一课(Ⅲ) 配置类注解

目录 一、PropertySource 二、ImportResource ①SpringConfig &#xff08;Spring框架全注解&#xff09; ②ImportResource注解实现 三、Bean 四、多配置文件 多Profile文件的使用 文件命名约定&#xff1a; 激活Profile&#xff1a; YAML文件支持多文档块&#xff…

深度解析React Native底层核心架构

React Native 工作原理深度解析 一、核心架构&#xff1a;三层异构协作体系 React Native 的跨平台能力源于其独特的 JS层-Shadow层-Native层 架构设计&#xff0c;三者在不同线程中协同工作&#xff1a; JS层 运行于JavaScriptCore&#xff08;iOS&#xff09;或Hermes&…

对话智能体的正确打开方式:解析主流AI聊天工具的核心能力与使用方式

一、人机对话的黄金法则 在与人工智能对话系统交互时&#xff0c;掌握以下七项核心原则可显著提升沟通效率&#xff1a;文末有教程分享地址 意图精准表达术 采用"背景需求限定条件"的结构化表达 示例优化&#xff1a;"请用Python编写一个网络爬虫&#xff08…