springboot+vue 文件下载,实现大文件的分片压缩和下载,避免内存溢出

实现大文件的分片压缩和下载,避免内存溢出,核心在于采用​​流式处理​ ​和​​分块传输​​技术,避免一次性将整个文件加载到内存中。下面分别从后端(Spring Boot)和前端(Vue.js)角度详细说明实现方案。

💡 后端实现(Spring Boot)

后端的关键是支持分片下载请求,并流式地提供文件数据。

1. 支持分片下载的接口

使用 RandomAccessFileFileChannel来高效读取文件的指定字节范围,并设置正确的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>

⚠️ 关键注意事项与优化建议

  1. ​内存管理​

    • ​后端​ :始终使用固定大小的缓冲区进行读写,避免将整个文件内容读入内存。对于 ZipOutputStream,确保及时关闭每个 ZipEntry
    • ​前端​ :及时释放 Blob URL(URL.revokeObjectURL),防止内存泄漏。对于超大文件(如数GB),可以考虑将每个分片直接写入磁盘(通过 File System Access API),而不是全部暂存在内存中。
  2. ​分片大小权衡​

    • 分片大小需要根据网络环境和文件大小进行权衡。过小(如小于1MB)会增加请求开销;过大(如超过50MB)则失去分片的意义,且重试成本高。通常建议设置在 ​1MB 到 10MB​ 之间。
  3. ​错误处理与重试机制​

    • 为每个分片下载实现重试机制(如最多重试3次)。如果某个分片多次下载失败,可以向用户报告并允许从该分片继续下载(断点续传)。
  4. ​服务器性能​

    • 高并发下载时,上述流式处理能显著降低服务器内存压力。但对于海量小文件打包下载,仍需关注服务器CPU和IO负载,必要时可引入消息队列进行异步压缩打包。

通过在后端实现支持 Range请求的流式下载,在前端进行分片请求和客户端合并,可以有效地实现大文件的稳定下载,同时避免服务器和客户端的内存溢出问题。

相关推荐
用户203735549812 小时前
Vue+Node+MongoDB高级全栈开发视频教程 完整版
前端
泉城老铁2 小时前
springboot +mybatisplus的性能优化
后端
泉城老铁2 小时前
springboot开发中,如何提升代码的性能
后端
我是天龙_绍2 小时前
setup 函数 和 setup 语法糖
前端
我不是混子2 小时前
数据误删了咋办?别怕,今天来教你如何恢复数据
java·后端
zjjuejin3 小时前
Maven 最佳实践与性能优化
java·后端·maven
Mr.45673 小时前
Spring Boot 全局鉴权认证简单实现方案
spring boot·后端
泉城老铁3 小时前
Spring Boot和Vue.js项目中实现文件压缩下载功能
前端·spring boot·后端
我是天龙_绍3 小时前
vue3 中,setup 函数 和 <script setup> 的区别
前端