效果:
大文件分片下载支持的功能:
- 展示目标文件信息
- 提高下载速度:通过并发请求多个块,可以更有效地利用网络带宽
- 断点续传:支持暂停后从已下载部分继续,无需重新开始
- 错误恢复:单个块下载失败只需重试该块,而不是整个文件
- 更好的用户体验:实时显示下载进度、速度和预计剩余时间
- 内存效率:通过分块下载和处理,减少了一次性内存占用
大文件分片下载
前端处理流程:
后端处理流程:
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,文件定位:将文件指针移动到请求的起始位置
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">
<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();