效果:
大文件分片下載支持的功能:
- 展示目標文件信息
- 提高下載速度:通過并發請求多個塊,可以更有效地利用網絡帶寬
- 斷點續傳:支持暫停后從已下載部分繼續,無需重新開始
- 錯誤恢復:單個塊下載失敗只需重試該塊,而不是整個文件
- 更好的用戶體驗:實時顯示下載進度、速度和預計剩余時間
- 內存效率:通過分塊下載和處理,減少了一次性內存占用
大文件分片下載
前端處理流程:
后端處理流程:
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();