• Vue—大文件分片上传


    背景

    如题,最近遇到大文件上传慢的问题,用户需要经常上传一些超过一百多M的文件,系统由于历史原因上传功能并没有做分片上传的功能,是整个文件上传,并且服务器带宽限制和NGINX对文件大小的限制等问题,所以决定将文件上传功能改为分片上传。

    决定将上传功能修改为分片上传后遂百度分片上传的相关开源项目,本项目使用的技术是Vue2+antd+SpringBoot,但是找到的开源项目基本不合适。

    前端Vue代码

    1、引入依赖

    // 引入SparkMD5用于计算文件MD5值
    npm install --save SparkMD5
    
    • 1
    • 2

    2、编写UI及对应函数

    在这里插入图片描述

    3、设置分片大小

    data() {
          return {
         	 CHUNK_SIZE: 20 * 1024 * 1024, // 分片上传大小20MB
          }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4、

    async customRequest(data) {
       var that = this
       // 1、设置文件状态为上传中
       for (var ff of this.fileList) {
         if (ff.uid === data.file.uid) {
           ff.status = 'done'
           break;
         }
       }
    
       let file = data.file;
       let time = new Date().getTime();
       // 2、求出分片数量、计算文件MD5
       let chunks = Math.ceil(file.size / that.CHUNK_SIZE);
       let spark = new SparkMD5.ArrayBuffer();
       spark.append(file);
       let md5 = spark.end();
       console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
       spark.destroy(); //释放缓存
    
       // 3、循环读取分片并上传
       let currentChunk = 0;
       // 3.1、读完第一个分片
       let blob = loadNext(currentChunk, that.CHUNK_SIZE);
       for (currentChunk = 1; currentChunk <= chunks; currentChunk++) {
         // 上传分片
         var params = {
           chunkNumber: currentChunk,
           totalChunks: chunks,
           chunkSize: that.CHUNK_SIZE,
           currentChunkSize: blob.size,
           totalSize: file.size,
           identifier: md5,
           filename: file.name
         }
         // 3.2、上传文件分片,阻塞等待返回再继续执行
         await this.uploadFileChunk(data, blob, params);
    
         // 3.3、加载下一分片
         if (currentChunk < chunks) {
           blob = loadNext(currentChunk, that.CHUNK_SIZE);
         }
       }
    
       // 4、发送合并文件请求
       var mergeParams = {
         totalChunks: chunks,
         chunkSize: that.CHUNK_SIZE,
         totalSize: file.size,
         identifier: md5,
         filename: file.name
       }
       await this.mergeFileChunk(data, mergeParams);
       
       // 获取文件分片方法
       function loadNext(currentChunk, CHUNK_SIZE) {
         let start = currentChunk * CHUNK_SIZE;
         let end = start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE;
         return file.slice(start, end);
       }
     },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    后端Spring Boot代码

    后端代码这块是基于开源项目(金鳞岂是池中物灬 / simple-uploader)做了点小改动,具体代码如下:

    文件分片上传类FileChunk

    @Data
    public class FileChunk {
        /**
         * 主键id
         */
        private Long id;
        /**
         * 当前块的次序,第一个块是 1,注意不是从 0 开始的
         */
        private Integer chunkNumber;
        /**
         * 文件被分成块的总数。
         */
        private Integer totalChunks;
        /**
         * 分块大小,根据 totalSize 和这个值你就可以计算出总共的块数。注意最后一块的大小可能会比这个要大。
         */
        private Integer chunkSize;
        /**
         * 当前块的大小,实际大小。
         */
        private Integer currentChunkSize;
        /**
         * 文件总大小。
         */
        private Long totalSize;
        /**
         * 这个就是每个文件的唯一标示。
         */
        private String identifier;
        /**
         * 文件名。
         */
        private String filename;
        /**
         * 文件夹上传的时候文件的相对路径属性。
         */
        private String relativePath;
        /**
         * 创建时间
         */
        private Date createTime;
        /**
         * Spring MultipartFile
         */
        private MultipartFile file;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    文件分片上传接口

    
    	/**
    	 * Post方法:分片上传
    	 *
    	 * @param fileChunk 分片
    	 * @return AjaxResult
    	 */
    	@PostMapping("/upload")
    	public BaseResponse uploadChunk(FileChunk fileChunk) {
    		logger.info("上传分片——开始:{}", fileChunk.toString());
    		if (fileChunk.getFile().isEmpty()) {
    			logger.error("上传文件不存在!");
    			throw new RuntimeException("上传文件不存在!");
    		}
    		File chunkPath  = new File(uploadPath + File.separator + "temp" + File.separator + fileChunk.getIdentifier());
    		if (!chunkPath.exists()) {
    			final boolean flag = chunkPath.mkdirs();
    			if (!flag) {
    				logger.error("创建目录失败!");
    				return new BaseResponse().fail("上传失败");
    			}
    		}
    		RandomAccessFile raFile = null;
    		BufferedInputStream inputStream = null;
    		try {
    			File chuckFile = new File(chunkPath, String.valueOf(fileChunk.getChunkNumber()));
    			raFile = new RandomAccessFile(chuckFile, "rw");
    			raFile.seek(raFile.length());
    			inputStream = new BufferedInputStream(fileChunk.getFile().getInputStream());
    			byte[] buf = new byte[1024];
    			int length = 0;
    			while ((length = inputStream.read(buf)) != -1) {
    				raFile.write(buf, 0, length);
    			}
    		} catch (IOException e) {
    			throw new RuntimeException(e);
    		} finally {
    			if (inputStream != null) {
    				try {
    					inputStream.close();
    				} catch (IOException e) {
    					throw new RuntimeException(e);
    				}
    			}
    			if (raFile != null) {
    				try {
    					raFile.close();
    				} catch (IOException e) {
    					throw new RuntimeException(e);
    				}
    			}
    		}
    		logger.info("上传分片——结束:{}", fileChunk.toString());
    		return new BaseResponse().success();
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    合并文件分片接口

    /**
    * 合并文件
    *
    * @param fileChunk 分片信息
    * @return AjaxResult
    */
    @PostMapping("/merge")
    public BaseResponse merge(FileChunk fileChunk) {
    	logger.info("合并文件——开始:{}", fileChunk.toString());
    	//分片文件临时目录
    	File tempPath = new File(uploadPath + File.separator + "temp" + File.separator + fileChunk.getIdentifier());
    	// 上传的文件
    	File realFile = new File(uploadPath + File.separator + "temp" + File.separator + fileChunk.getFilename());
    	// 文件追加写入
    	FileOutputStream os;
    	try {
    		os = new FileOutputStream(realFile, true);
    		if (tempPath.exists()) {
    			//获取临时目录下的所有文件
    			File[] tempFiles = tempPath.listFiles();
    			//按名称排序
    			Arrays.sort(tempFiles, (o1, o2) -> {
    				if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {
    					return -1;
    				}
    				if (Integer.parseInt(o1.getName()) == Integer.parseInt(o2.getName())) {
    					return 0;
    				}
    				return 1;
    			});
    			//每次读取10MB大小,字节读取
    			byte[] bytes = new byte[10 * 1024 * 1024];
    			int len;
    	
    			for (int i = 0; i < tempFiles.length; i++) {
    				FileInputStream fis = new FileInputStream(tempFiles[i]);
    				while ((len = fis.read(bytes)) != -1) {
    					os.write(bytes, 0, len);
    				}
    				fis.close();
    				//删除分片
    				tempFiles[i].delete();
    			}
    			os.close();
    			// TODO:验证合并文件的MD5值是否与传输过来的文件MD5值一致
    			//删除临时目录
    			if (tempPath.isDirectory() && tempPath.exists()) {
    				System.gc(); // 回收资源
    				tempPath.delete();
    			}
    	
    		}
    	} catch (Exception e) {
    		logger.error("文件合并——失败 " + e.getMessage());
    		return new BaseResponse().fail("文件合并失败");
    	}
    	logger.info("合并文件——结束:{}", fileChunk.toString());
    	// 文件合并成功,下一步上传至到阿里云
    	String ossUrl = uploadFileToOos(realFile, fileChunk.getFilename());
    	if(StrUtil.isEmpty(ossUrl)){
    		return new BaseResponse().fail("文件上传失败");
    	}
    	Map<String, Object> returnMap = new HashMap<>();
    	returnMap.put("oosUrl", ossUrl);
    	returnMap.put("attachmentName", fileChunk.getFilename());
    	return new BaseResponse().success(returnMap);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    上述合并文件代码其实缺少了验证文件MD5值这一步,当时写代码时没发现。哈哈。

    总结

    至此文件分片上传的功能已经开发完毕,基于上述代码其实还可以实现文件秒传、断点续传和失败重试功能。

    参考

    CSDN博文 vue—大文件分片上传
    金鳞岂是池中物灬 / simple-uploader

  • 相关阅读:
    seo优化
    三、日志编写 —— TinyWebServer
    内存中为什么分堆和栈,能否只用一种模型呢?为什么每个线程都有单独的栈
    HTML+CSS+JavaScript仿京东购物商城网站 web前端制作服装购物商城 html电商购物网站
    C# 中的特性
    FullGC 过多 为什么会让CPU飙升100%
    stm32 LWIP开发-1-
    系统架构师笔记——嵌入式系统
    KNN(k-Nearest Neighbor)算法原理
    python 实现蚁群算法(simpy带绘图)
  • 原文地址:https://blog.csdn.net/sinat_24044957/article/details/133790038