2023-06-02
后端
00
请注意,本文编写于 389 天前,最后修改于 35 天前,其中某些信息可能已经过时。

目录

准备工作
引入依赖
实现接口
实现分片上传
实现分片下载
前端实现
引入依赖
实现上传
实现下载
实现分片上传
实现分片下载完整代码
总结

在前端下载大文件时,我们通常会遇到两个问题:

大文件下载时间过长,容易被中断,需要支持断点续传。 如果文件较大,一次网络传输可能会卡顿或失败。 其中,第一个问题可以通过前端进行分片,上传到后端,后端再进行分片返回等方式解决;而第二个问题则通过浏览器的下载解决。

因此本文将介绍如何使用SpringBoot和Vue来实现大文件分片下载,并通过浏览器进行下载。

准备工作

首先,我们需要准备好以下环境:

  • JDK 1.8 或以上版本
  • Maven 3.0 或以上版本
  • Node.js 和 npm 包管理器
  • Vue CLI 脚手架工具
  • IntelliJ IDEA 或 Eclipse 等 Java 开发工具

引入依赖

在 pom.xml 文件中添加以下依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.5.5</version> </dependency>

spring-boot-starter-web 是 SpringBoot 的 web 模块。

实现接口

在 SpringBoot 中,我们可以通过 ResponseEntity 类来实现文件下载。下面是一个简单的下载接口实现示例:

@GetMapping("/download") public ResponseEntity<byte[]> downloadFile(HttpServletRequest request, HttpServletResponse response) throws IOException { String fileName = "test.zip"; File file = new File(fileName); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); headers.setContentDispositionFormData("attachment", URLEncoder.encode(file.getName(), "UTF-8")); headers.setContentLength(file.length()); return new ResponseEntity<>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK); }

这个接口实现了将文件 test.zip 下载到本地。其中,headers 中设置了 Content-Type 和 Content-Disposition 信息,用于告诉浏览器下载的文件类型和名称。

实现分片上传

为了实现大文件断点续传,我们需要将文件切成若干个小块进行上传,同时记录每个小块的位置和大小。在 SpringBoot 中,可以通过 MultipartFile 类来实现文件上传。下面是一个简单的上传接口实现示例:

@PostMapping("/upload") public void uploadFile(@RequestParam("file") MultipartFile file) throws IOException { String fileName = file.getOriginalFilename(); String filePath = "/path/to/upload/dir/" + fileName; File dest = new File(filePath); file.transferTo(dest); }

这个接口实现了将文件上传到服务器指定目录。其中,@RequestParam 注解用于获取前端上传的文件对象,transferTo 方法用于将文件保存到指定目录中。

实现分片下载

下面是分片下载文件的接口。其在处理请求时会解析前端传来的 Range 头部信息,判断是否有分片下载需求,如果有则进行分片下载,并返回响应信息告诉前端支持分片下载,并返回文件大小、文件名等信息供前端页面展示。同时该接口还设置了跨域相关的 header,以及对文件名进行编码,避免中文文件名下载时出错。最后,它将可读取范围内的文件内容写入输出流进行响应,完成文件下载。

import cn.hutool.core.io.FileUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.URLEncoder; /** * @author: Chen Shaohua * @Date: 2023/6/1 19:03:18 */ @RestController @RequestMapping("/downFloadFile") @CrossOrigin @Slf4j public class DownFloadFileController { @RequestMapping("/spiltDownLoad") public void spiltDownLoad(String filePath, HttpServletRequest request, HttpServletResponse response) throws Exception { //解决前端接受自定义heards response.setHeader("Access-Control-Expose-Headers", "Content-Disposition,Accept-Range,fSize,fName,Content-Range,Content-Lenght,responseType"); File file = new File(filePath); //设置编码 response.setCharacterEncoding("utf-8"); InputStream is = null; OutputStream os = null; try { //分片下载 long fSize = file.length(); response.setHeader("responseType", "blob"); //前段识别下载 response.setContentType("application/x-download"); //response.setContentType("application/octet-stream"); //文件名 String fileName = URLEncoder.encode(FileUtil.getName(filePath), "UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=" + fileName); //http Range 告诉前段支持分片下载 response.setHeader("Accept-Range", "bytes"); //告诉前段文件大小 文件名字 response.setHeader("fSize", String.valueOf(fSize)); response.setHeader("fName", fileName); //起始位置 结束位置 读取了多少 long pos = 0, last = fSize - 1, sum = 0; //需不需要分片下载 if (null != request.getHeader("Range")) { //支持分片下载 206 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); //bytes=10-100 String numRange = request.getHeader("Range").replaceAll("bytes=", ""); String[] strRange = numRange.split("-"); //取起始位置 if (strRange.length >= 2) { pos = Long.parseLong(strRange[0].trim()); last = Long.parseLong(strRange[1].trim()); if (last > fSize - 1) { last = fSize - 1; } } else { pos = Long.parseLong(numRange.replaceAll("-", "").trim()); } } //读多少 long rangeLenght = last - pos + 1; //告诉前端有多少分片 String contentRange = new StringBuffer("bytes ").append(pos).append("-").append(last).append("/").append(fSize).toString(); //规范告诉文件大小 response.setHeader("Content-Range", contentRange); response.setHeader("Content-Lenght", String.valueOf(rangeLenght)); os = new BufferedOutputStream(response.getOutputStream()); is = new BufferedInputStream(new FileInputStream(file)); //跳过已读 is.skip(pos); byte[] buffer = new byte[1024 * 1024 * 100]; int lenght = 0; while (sum < rangeLenght) { lenght = is.read(buffer, 0, (rangeLenght - sum) <= buffer.length ? ((int) (rangeLenght - sum)) : buffer.length); sum = sum + lenght; os.write(buffer, 0, lenght); } } catch (Exception e) { log.info("", e); } finally { if (is != null) { is.close(); } if (os != null) { os.close(); } } } }

前端实现

引入依赖

在 package.json 文件中添加以下依赖:

"dependencies": { "axios": "^0.21.4", "vue": "^2.6.14", "vue-router": "^3.5.2", "vuex": "^3.6.2", "element-ui": "^2.14.1", }

其中,axios 是一个基于 Promise 的 HTTP 库,用于发送 HTTP 请求。

实现上传

在 Vue 中,我们可以通过 FormData 类来实现文件上传。下面是一个简单的上传方法实现示例:

async uploadFile(file) { let formData = new FormData(); formData.append('file', file); await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); }

这个方法实现了将文件上传到服务器。其中,formData 对象用于封装文件对象,headers 中设置了 Content-Type 信息,用于告诉服务器请求体的格式为 multipart/form-data。

实现下载

在 Vue 中,我们可以通过 Blob 类来实现文件下载。下面是一个简单的下载方法实现示例:

async downloadFile(fileName) { let url = '/api/download/' + fileName; let response = await axios.get(url, { responseType: 'blob', headers: { 'Range': 'bytes=0-' } }); let blob = new Blob([response.data], { type: response.headers['content-type'] }); let link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = fileName; link.click(); }

这个方法实现了将文件下载到本地。其中,responseType 设置为 blob 表示响应体的格式为二进制数据流,headers 中设置了 Range 信息,用于告诉服务器需要下载的文件范围。

实现分片上传

为了实现大文件断点续传,我们需要将文件切成若干个小块进行上传和下载,同时记录每个小块的位置和大小。在 Vue 中,我们可以通过 FileReader 类来实现文件读取,Blob 类来实现文件切割,Blob.slice 方法来实现文件分片,FormData 类来实现文件上传。下面是一个简单的分片上传和下载方法实现示例:

async uploadFileChunk(file, chunkIndex, chunkSize, chunkCount) { let formData = new FormData(); let start = chunkIndex * chunkSize; let end = Math.min(start + chunkSize, file.size); formData.append('file', file.slice(start, end)); formData.append('fileName', file.name); formData.append('chunkIndex', chunkIndex); formData.append('chunkSize', chunkSize); formData.append('chunkCount', chunkCount); await axios.post('/api/upload-chunk', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); } async uploadFileWithChunks(file) { let chunkSize = 1024 * 1024; // 每个分片的大小为 1MB let chunkCount = Math.ceil(file.size / chunkSize); for (let i = 0; i < chunkCount; i++) { await this.uploadFileChunk(file, i, chunkSize, chunkCount); } }

这个方法实现了将文件分片上传和下载到服务器。其中,uploadFileChunk 方法用于将文件分片上传到服务器,uploadFileWithChunks 方法用于将整个文件分片上传到服务器。我们可以通过调用这些方法来实现大文件断点续传。

实现分片下载完整代码

完整可以直接使用实现分片下载后端结合下面前端代码实现简单的示例,实际参数根据项目实际来写。

image-20230601163123187

<template> <div> <el-button @click="download" id="download">下载</el-button> <el-button @click="stop">暂停</el-button> <el-button @click="start">继续</el-button> <el-button @click="reStart">重新下载</el-button> <el-progress type="circle" :percentage="percentage"></el-progress> </div> </template> <style></style> <script> export default { name: 'SpiltDownLoad', data() { return { percentage: 0, // 下载进度 filesCurrentPage: 0,//文件开始偏移量 fileFinalOffset: 0, //文件最后偏移量 stopRecursiveTags: true, //停止递归标签,默认是true 继续进行递归 contentList: [], // 文件流数组 fileName: '', } }, //初始化 mounted() { var _this = this; console.log('init'); }, // 监听 watch: { }, methods: { //停止下载 stop() { //改变递归标签为false this.stopRecursiveTags = false; }, //开始下载 start() { //重置递归标签为true 最后进行合并 this.stopRecursiveTags = true; //重新调用下载方法 this.download(); }, //重新开始下载 reStart() { let _this = this; //构造一个blob对象来处理数据 const blob = new Blob(_this.contentList); //对于<a>标签,只有 Firefox 和 Chrome(内核) 支持 download 属性 //IE10以上支持blob但是依然不支持download if ("download" in document.createElement("a")) { //支持a标签download的浏览器 const link = document.createElement("a"); //创建a标签 link.download = _this.fileName; //a标签添加属性 link.style.display = "none"; link.href = URL.createObjectURL(blob); document.body.appendChild(link); link.click(); //执行下载 URL.revokeObjectURL(link.href); //释放url document.body.removeChild(link); //释放标签 } else { //其他浏览器 navigator.msSaveBlob(blob, _this.fileName); } }, // 分段下载需要后端配合 download() { var _this = this; // 下载地址 const url = "http://localhost:8080/downFloadFile/spiltDownLoad"; const chunkSize = 1024 * 1024 * 100; // 单个分段大小,这里测试用100M let filesTotalSize = chunkSize; // 安装包总大小,默认100M let filesPages = 1; // 总共分几段下载 //计算百分比之前先清空上次的 if (_this.percentage == 100) { _this.percentage = 0; _this.filesCurrentPage = 0; _this.contentList = []; } let sentAxios = (num) => { let rande = chunkSize; if (num) { rande = `${(num - 1) * chunkSize + 2}-${num * chunkSize + 1}`; } else { // 第一次0-1方便获取总数,计算下载进度,每段下载字节范围区间 rande = "0-1"; } let headers = { range: rande, }; //测试用,上线根据项目实际修改 let params = { filePath: 'D:\\ceshi.rar' } _this.axios({ method: "get", url: url.trim(), async: true, data: {}, params: params, headers: headers, responseType: "blob" }) .then((response) => { if (response.status == 200 || response.status == 206) { //检查了下才发现,后端对文件流做了一层封装,所以将content指向response.data即可 const content = response.data; //截取文件总长度和最后偏移量 let result = response.headers["content-range"].split("/"); // 获取文件总大小,方便计算下载百分比 减去第一次获取总数 filesTotalSize = result[1] - 2; //获取最后一片文件位置,用于断点续传 _this.fileFinalOffset = result[0].split("-")[1] // 计算总共页数,向上取整 filesPages = Math.ceil(filesTotalSize / chunkSize); // 文件流数组 //_this.contentList.push(content); _this.contentList[num] = content; //计算下载百分比 当前下载的片数/总片数 if(_this.stopRecursiveTags){ _this.percentage = Number((((_this.contentList.length - 1) / filesPages) * 100).toFixed(2)); } // 递归获取文件数据(判断是否要继续递归) if (_this.filesCurrentPage < filesPages && _this.stopRecursiveTags) { _this.filesCurrentPage++; sentAxios(_this.filesCurrentPage); //结束递归 return; } //递归标签为true 才进行下载 if (_this.stopRecursiveTags) { // 文件名称 _this.fileName = decodeURIComponent(response.headers["fname"]); //构造一个blob对象来处理数据 const blob = new Blob(_this.contentList); //对于<a>标签,只有 Firefox 和 Chrome(内核) 支持 download 属性 //IE10以上支持blob但是依然不支持download if ("download" in document.createElement("a")) { //支持a标签download的浏览器 const link = document.createElement("a"); //创建a标签 link.download = _this.fileName; //a标签添加属性 link.style.display = "none"; link.href = URL.createObjectURL(blob); document.body.appendChild(link); link.click(); //执行下载 URL.revokeObjectURL(link.href); //释放url document.body.removeChild(link); //释放标签 } else { //其他浏览器 navigator.msSaveBlob(blob, _this.fileName); } } } else { //调用暂停方法,记录当前下载位置 _this.stop(); console.log("下载失败") } }) .catch(function (error) { console.log(error); }); }; // 第一次获取数据方便获取总数 sentAxios(_this.filesCurrentPage); _this.$message({ message: '文件开始下载!', type: 'success' }); } } } </script>

总结

通过使用 SpringBoot 和 Vue,我们可以很方便地实现大文件分片下载功能。在后端,通过指定读取分下大小实现文件分片下载,同时使用了 MultipartFile 类来实现文件分片上传。在前端,我们使用了 Blob 类来实现文件切割和合并,同时使用了 FormData 类来实现文件上传。通过将这些技术组合起来,我们可以很好地解决大文件下载的问题,提高用户体验。

本文作者:酷少少

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!