马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
效果:
大文件分片下载支持的功能:
- 展示目标文件信息
- 进步下载速率:通过并发哀求多个块,可以更有效地利用网络带宽
- 断点续传:支持停息后从已下载部分继承,无需重新开始
- 错误规复:单个块下载失败只需重试该块,而不是整个文件
- 更好的用户体验:及时表现下载进度、速率和预计剩余时间
- 内存服从:通太过块下载和处理处罚,淘汰了一次性内存占用
大文件分片下载
前端处理处罚流程:
后端处理处罚流程:
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_methods
- def get_file_info(file_path):
- """
- 获取文件信息:
- - name: 文件名
- - size: 文件大小,单位字节
- - type: 文件类型
- """
- if not os.path.exists(file_path):
- return None
- file_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-100
- 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
- # 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_length
- response['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 response
- def 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 None
- while True:
- if remaining is not None:
- # 如果指定了结束位置,则读取剩余字节或块大小,取小的那个
- bytes_to_read = min(chunk_size, remaining)
- if bytes_to_read <= 0:
- break
- else:
- # 否则读取指定块大小
- bytes_to_read = chunk_size
- data = f.read(bytes_to_read)
- if not data:
- break
- yield data
- if remaining is not None:
- remaining -= len(data)
- # proj urls.py
- from django.urls import path, include
- urlpatterns = [
- # 下载文件
- path('download/', include(('download.urls', 'download'), namespace='download')),
- ]
- # download.urls.py
- from django.urls import path
- from download import views
- urlpatterns = [
- 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,文件定位:将文件指针移动到哀求的起始位置
2,分块读取:按指定的块巨细读取文件,克制一次性读取大量数据
- data = f.read(bytes_to_read)
复制代码 3,界限控制:确保只读取哀求范围内的数据
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">
- <div
- v-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, error
- this.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();
复制代码 |