djangovue3实现前后端大文件分片下载
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_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组件):
- 提供配置选项:并发块数、块大小
- 显示下载进度:进度条、已下载量、下载速度、剩余时间提供操作按钮:开始、暂停、继续、取消
- 可视化显示每个分块的下载状态
大文件分块下载
文件名: {{ fileInfo.name }}
文件大小: {{ formatFileSize(fileInfo.size) }}
类型: {{ fileInfo.type }}
并发块数:
1 2 3 5 8
块大小:
512 KB 1 MB 2 MB 5 MB
进度: {{ progress.toFixed(2) }}%
已下载: {{ formatFileSize(downloadedBytes) }} / {{ formatFileSize(totalBytes) }}
速度: {{ downloadSpeed }}
已完成块: {{ downloadedChunks }} / {{ totalChunks }}
剩余时间: {{ remainingTime }}
开始下载
暂停
继续
取消
{{ statusMessage }}
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();