实现大文件的分片压缩和下载,避免内存溢出,核心在于采用流式处理 和分块传输技术,避免一次性将整个文件加载到内存中。下面分别从后端(Spring Boot)和前端(Vue.js)角度详细说明实现方案。
💡 后端实现(Spring Boot)
后端的关键是支持分片下载请求,并流式地提供文件数据。
1. 支持分片下载的接口
使用 RandomAccessFile
或 FileChannel
来高效读取文件的指定字节范围,并设置正确的HTTP响应头。
arduino
@RestController
public class FileDownloadController {
@GetMapping("/download")
public void downloadFile(@RequestParam String filePath,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
response.sendError(HttpStatus.NOT_FOUND.value(), "File not found");
return;
}
long fileSize = file.length();
String rangeHeader = request.getHeader("Range");
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename="" + file.getName() + """);
response.setHeader("Accept-Ranges", "bytes");
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
OutputStream out = response.getOutputStream()) {
long start = 0;
long end = fileSize - 1;
long contentLength = fileSize;
// 处理分片请求(HTTP 206 Partial Content)
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
String[] range = rangeHeader.substring(6).split("-");
start = Long.parseLong(range[0]);
if (range.length > 1) {
end = Long.parseLong(range[1]);
}
contentLength = end - start + 1;
response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
response.setHeader("Content-Length", String.valueOf(contentLength));
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
} else {
// 非分片请求,返回整个文件
response.setHeader("Content-Length", String.valueOf(contentLength));
}
randomAccessFile.seek(start);
byte[] buffer = new byte[1024 * 1024]; // 1MB 缓冲区
long bytesToRead = contentLength;
int read;
while ((read = randomAccessFile.read(buffer)) != -1 && bytesToRead > 0) {
out.write(buffer, 0, (int) Math.min(read, bytesToRead));
bytesToRead -= read;
out.flush();
}
}
}
}
2. 多文件压缩与流式输出
当需要打包下载多个文件时,使用 ZipOutputStream
进行流式压缩,避免在服务器生成临时文件。
less
@PostMapping("/download-multiple")
public void downloadMultipleFiles(@RequestBody List<String> filePaths, HttpServletResponse response) throws IOException {
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename="files.zip"");
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[1024 * 1024]; // 1MB缓冲区
for (String filePath : filePaths) {
File file = new File(filePath);
if (file.exists()) {
ZipEntry zipEntry = new ZipEntry(file.getName());
zos.putNextEntry(zipEntry);
try (FileInputStream fis = new FileInputStream(file)) {
int len;
while ((len = fis.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}
zos.closeEntry();
}
}
zos.finish();
}
}
🎨 前端实现(Vue.js)
前端负责管理下载队列,通过 Range
请求头分片获取文件数据,并在客户端进行合并。
1. 分片下载核心逻辑
使用 fetch API
并设置 Range
头来请求文件的特定部分。
ini
// utils/downloader.js
export async function downloadLargeFile(url, fileName, onProgress) {
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB 每片
// 先获取文件总大小
const headResponse = await fetch(url, { method: 'HEAD' });
const fileSize = parseInt(headResponse.headers.get('content-length'));
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
const chunks = [];
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE - 1, fileSize - 1);
const response = await fetch(url, {
headers: { 'Range': `bytes=${start}-${end}` }
});
if (!response.ok) {
throw new Error(`Download failed for chunk ${chunkIndex}`);
}
const blob = await response.blob();
chunks.push(blob);
// 更新进度
if (onProgress) {
const downloaded = (chunkIndex + 1) * CHUNK_SIZE;
const progress = Math.min((downloaded / fileSize) * 100, 100);
onProgress(progress);
}
}
// 合并所有分片
const fullBlob = new Blob(chunks);
const blobUrl = URL.createObjectURL(fullBlob);
// 触发下载
const link = document.createElement('a');
link.href = blobUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl); // 释放内存
}
2. 在Vue组件中使用
在组件中调用下载函数,并显示下载进度。
xml
<template>
<div>
<button @click="startDownload" :disabled="isDownloading">
{{ isDownloading ? `下载中... ${progress.toFixed(1)}%` : '下载大文件' }}
</button>
</div>
</template>
<script>
import { downloadLargeFile } from '@/utils/downloader';
export default {
data() {
return {
isDownloading: false,
progress: 0
};
},
methods: {
async startDownload() {
this.isDownloading = true;
this.progress = 0;
try {
await downloadLargeFile(
'/api/download?filePath=/path/to/large-file.iso',
'large-file.iso',
(progress) => {
this.progress = progress;
}
);
this.$message.success('文件下载完成!');
} catch (error) {
console.error('下载失败:', error);
this.$message.error('文件下载失败');
} finally {
this.isDownloading = false;
}
}
}
};
</script>
⚠️ 关键注意事项与优化建议
-
内存管理
- 后端 :始终使用固定大小的缓冲区进行读写,避免将整个文件内容读入内存。对于
ZipOutputStream
,确保及时关闭每个ZipEntry
。 - 前端 :及时释放
Blob URL
(URL.revokeObjectURL
),防止内存泄漏。对于超大文件(如数GB),可以考虑将每个分片直接写入磁盘(通过File System Access API
),而不是全部暂存在内存中。
- 后端 :始终使用固定大小的缓冲区进行读写,避免将整个文件内容读入内存。对于
-
分片大小权衡
- 分片大小需要根据网络环境和文件大小进行权衡。过小(如小于1MB)会增加请求开销;过大(如超过50MB)则失去分片的意义,且重试成本高。通常建议设置在 1MB 到 10MB 之间。
-
错误处理与重试机制
- 为每个分片下载实现重试机制(如最多重试3次)。如果某个分片多次下载失败,可以向用户报告并允许从该分片继续下载(断点续传)。
-
服务器性能
- 高并发下载时,上述流式处理能显著降低服务器内存压力。但对于海量小文件打包下载,仍需关注服务器CPU和IO负载,必要时可引入消息队列进行异步压缩打包。
通过在后端实现支持 Range
请求的流式下载,在前端进行分片请求和客户端合并,可以有效地实现大文件的稳定下载,同时避免服务器和客户端的内存溢出问题。