为什么使用Vue-Simple-Uploader

最近用到了Vue + Spring Boot来完成文件上传的操作,踩了一些坑,对比了一些Vue的组件,发现了一个很好用的组件——Vue-Simple-Uploader

再说说为什么选用这个组件,对比vue-ant-design和element-ui的上传组件,它能做到更多的事情,比如:

  • 可暂停、继续上传
  • 上传队列管理,支持最大并发上传
  • 分块上传
  • 支持进度、预估剩余时间、出错自动重试、重传等操作
  • 支持“快传”,通过文件判断服务端是否已存在从而实现“快传”

由于需求中需要用到断点续传,所以选用了这个组件,下面我会从最基础的上传开始说起:

单文件上传、多文件上传、文件夹上传

Vue代码:

 <uploader
:options="uploadOptions1"
:autoStart="true"
class="uploader-app"
>
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<uploader-btn style="margin-right:20px;" :attrs="attrs">选择文件</uploader-btn>
<uploader-btn :attrs="attrs" directory>选择文件夹</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>

该组件默认支持多文件上传,这里我们从官方demo中粘贴过来这段代码,然后在uploadOption1中配置上传的路径即可,其中uploader-btn 中设置directory属性即可选择文件夹进行上传。

uploadOption1:

 uploadOptions1: {
target: "//localhost:18080/api/upload/single",//上传的接口
testChunks: false, //是否开启服务器分片校验
fileParameterName: "file",//默认的文件参数名
headers: {},
query() {},
categaryMap: { //用于限制上传的类型
image: ["gif", "jpg", "jpeg", "png", "bmp"]
}
}

在后台的接口的编写,我们为了方便,定义了一个chunk类用于接收组件默认传输的一些后面方便分块断点续传的参数:

Chunk类

@Data
public class Chunk implements Serializable { private static final long serialVersionUID = 7073871700302406420L; private Long id;
/**
* 当前文件块,从1开始
*/
private Integer chunkNumber;
/**
* 分块大小
*/
private Long chunkSize;
/**
* 当前分块大小
*/
private Long currentChunkSize;
/**
* 总大小
*/
private Long totalSize;
/**
* 文件标识
*/
private String identifier;
/**
* 文件名
*/
private String filename;
/**
* 相对路径
*/
private String relativePath;
/**
* 总块数
*/
private Integer totalChunks;
/**
* 文件类型
*/
private String type; /**
* 要上传的文件
*/
private MultipartFile file;
}

在编写接口的时候,我们直接使用这个类作为参数去接收vue-simple-uploader传来的参数即可,注意这里要使用POST来接收哟~

接口方法:

    @PostMapping("single")
public void singleUpload(Chunk chunk) {
// 获取传来的文件
MultipartFile file = chunk.getFile();
// 获取文件名
String filename = chunk.getFilename();
try {
// 获取文件的内容
byte[] bytes = file.getBytes();
// SINGLE_UPLOADER是我定义的一个路径常量,这里的意思是,如果不存在该目录,则去创建
if (!Files.isWritable(Paths.get(SINGLE_FOLDER))) {
Files.createDirectories(Paths.get(SINGLE_FOLDER));
}
// 获取上传文件的路径
Path path = Paths.get(SINGLE_FOLDER,filename);
// 将字节写入该文件
Files.write(path, bytes);
} catch (IOException e) {
e.printStackTrace();
}
}

这里需要注意一点,如果文件过大的话,Spring Boot后台会报错

org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.

这时需要在application.yml中配置servlet的最大接收文件大小(默认大小是1MB和10MB)

spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB

下面我们启动项目,选择需要上传的文件就可以看到效果了~ 是不是很方便~ 但是同样的事情其余的组件基本上也可以做到,之所以选择这个,更多的是因为它可以支持断点分块上传,实现上传过程中断网,再次联网的话可以从断点位置开始继续秒传~下面我们来看看断点续传是怎么玩的。

断点分块续传

先说一下分块断点续传的大概原理,我们在组件可以配置分块的大小,大于该值的文件会被分割成若干块儿去上传,同时将该分块的chunkNumber保存到数据库(Mysql or Redis,这里我选择的是Redis

组件上传的时候会携带一个identifier的参数(这里我采用的是默认的值,你也可以通过生成md5的方式来重新赋值参数),将identifier作为Redis的key,设置hashKey为”chunkNumber“,value是由每次上传的chunkNumber组成的一个Set集合。

在将uploadOption中的testChunk的值设置为true之后,该组件会先发一个get请求,获取到已经上传的chunkNumber集合,然后在checkChunkUploadedByResponse方法中判断是否存在该片段来进行跳过,发送post请求上传分块的文件。

每次上传片段的时候,service层返回当前的集合大小,并与参数中的totalChunks进行对比,如果发现相等,就返回一个状态值,来控制前端发出merge请求,将刚刚上传的分块合为一个文件,至此文件的断点分块上传就完成了。

下面是对应的代码~

Vue代码:

<uploader
:options="uploadOptions2"
:autoStart="true"
:files="files"
@file-added="onFileAdded2"
@file-success="onFileSuccess2"
@file-progress="onFileProgress2"
@file-error="onFileError2"
>
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<uploader-btn :attrs="attrs">分块上传</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>

校验是否上传过的代码

 uploadOptions2: {
target: "//localhost:18080/api/upload/chunk",
chunkSize: 1 * 1024 * 1024,
testChunks: true,
checkChunkUploadedByResponse: function(chunk, message) {
let objMessage = JSON.parse(message);
// 获取当前的上传块的集合
let chunkNumbers = objMessage.chunkNumbers;
// 判断当前的块是否被该集合包含,从而判定是否需要跳过
return (chunkNumbers || []).indexOf(chunk.offset + 1) >= 0;
},
headers: {},
query() {},
categaryMap: {
image: ["gif", "jpg", "jpeg", "png", "bmp"],
zip: ["zip"],
document: ["csv"]
}
}

上传后成功的处理,判断状态来进行merge操作

onFileSuccess2(rootFile, file, response, chunk) {
let res = JSON.parse(response);
// 后台报错
if (res.code == 1) {
return;
}
// 需要合并
if (res.code == 205) {
// 发送merge请求,参数为identifier和filename,这个要注意需要和后台的Chunk类中的参数名对应,否则会接收不到~
const formData = new FormData();
formData.append("identifier", file.uniqueIdentifier);
formData.append("filename", file.name);
merge(formData).then(response => {});
}
},

判定是否存在的代码,注意这里的是GET请求!!!

 				   @GetMapping("chunk")
public Map<String, Object> checkChunks(Chunk chunk) {
return uploadService.checkChunkExits(chunk);
} @Override
public Map<String, Object> checkChunkExits(Chunk chunk) {
Map<String, Object> res = new HashMap<>();
String identifier = chunk.getIdentifier();
if (redisDao.existsKey(identifier)) {
Set<Integer> chunkNumbers = (Set<Integer>) redisDao.hmGet(identifier, "chunkNumberList");
res.put("chunkNumbers",chunkNumbers);
}
return res;
}

保存分块,并保存数据到Redis的代码。这里的是POST请求!!!


@PostMapping("chunk")
public Map<String, Object> saveChunk(Chunk chunk) {
// 这里的操作和保存单段落的基本是一致的~
MultipartFile file = chunk.getFile();
Integer chunkNumber = chunk.getChunkNumber();
String identifier = chunk.getIdentifier();
byte[] bytes;
try {
bytes = file.getBytes();
// 这里的不同之处在于这里进行了一个保存分块时将文件名的按照-chunkNumber的进行保存
Path path = Paths.get(generatePath(CHUNK_FOLDER, chunk));
Files.write(path, bytes);
} catch (IOException e) {
e.printStackTrace();
}
// 这里进行的是保存到redis,并返回集合的大小的操作
Integer chunks = uploadService.saveChunk(chunkNumber, identifier);
Map<String, Object> result = new HashMap<>();
// 如果集合的大小和totalChunks相等,判定分块已经上传完毕,进行merge操作
if (chunks.equals(chunk.getTotalChunks())) {
result.put("message","上传成功!");
result.put("code", 205);
}
return result;
} /**
* 生成分块的文件路径
*/
private static String generatePath(String uploadFolder, Chunk chunk) {
StringBuilder sb = new StringBuilder();
// 拼接上传的路径
sb.append(uploadFolder).append(File.separator).append(chunk.getIdentifier());
//判断uploadFolder/identifier 路径是否存在,不存在则创建
if (!Files.isWritable(Paths.get(sb.toString()))) {
try {
Files.createDirectories(Paths.get(sb.toString()));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
// 返回以 - 隔离的分块文件,后面跟的chunkNumber方便后面进行排序进行merge
return sb.append(File.separator)
.append(chunk.getFilename())
.append("-")
.append(chunk.getChunkNumber()).toString(); } /**
* 保存信息到Redis
*/
public Integer saveChunk(Integer chunkNumber, String identifier) {
// 获取目前的chunkList
Set<Integer> oldChunkNumber = (Set<Integer>) redisDao.hmGet(identifier, "chunkNumberList");
// 如果获取为空,则新建Set集合,并将当前分块的chunkNumber加入后存到Redis
if (Objects.isNull(oldChunkNumber)) {
Set<Integer> newChunkNumber = new HashSet<>();
newChunkNumber.add(chunkNumber);
redisDao.hmSet(identifier, "chunkNumberList", newChunkNumber);
// 返回集合的大小
return newChunkNumber.size();
} else {
// 如果不为空,将当前分块的chunkNumber加到当前的chunkList中,并存入Redis
oldChunkNumber.add(chunkNumber);
redisDao.hmSet(identifier, "chunkNumberList", oldChunkNumber);
// 返回集合的大小
return oldChunkNumber.size();
} }

合并的后台代码:

    @PostMapping("merge")
public void mergeChunks(Chunk chunk) {
String fileName = chunk.getFilename();
uploadService.mergeFile(fileName,CHUNK_FOLDER + File.separator + chunk.getIdentifier());
} @Override
public void mergeFile(String fileName, String chunkFolder) {
try {
// 如果合并后的路径不存在,则新建
if (!Files.isWritable(Paths.get(mergeFolder))) {
Files.createDirectories(Paths.get(mergeFolder));
}
// 合并的文件名
String target = mergeFolder + File.separator + fileName;
// 创建文件
Files.createFile(Paths.get(target));
// 遍历分块的文件夹,并进行过滤和排序后以追加的方式写入到合并后的文件
Files.list(Paths.get(chunkFolder))
//过滤带有"-"的文件
.filter(path -> path.getFileName().toString().contains("-"))
//按照从小到大进行排序
.sorted((o1, o2) -> {
String p1 = o1.getFileName().toString();
String p2 = o2.getFileName().toString();
int i1 = p1.lastIndexOf("-");
int i2 = p2.lastIndexOf("-");
return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
})
.forEach(path -> {
try {
//以追加的形式写入文件
Files.write(Paths.get(target), Files.readAllBytes(path), StandardOpenOption.APPEND);
//合并后删除该块
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}

至此,我们的断点续传就完美结束了,完整的代码我已经上传到gayhub~,欢迎star fork pr(后面还会把博文也上传到gayhub哟)

前端:https://github.com/viyog/viboot-front

后台:https://github.com/viyog/viboot

写在后面

最近由于家庭+工作忙昏了头,鸽了这么久很是抱歉,从这周开始恢复更新,同时本人在准备往大数据转型,后续会出一系列的Java转型大数据的学习笔记,包括Java基础系列的深入解读和重写,同时Spring Boot系列还会一直保持连载,不过可能不会每周都更,我会把目前使用Spring Boot中遇到的问题和坑写一写,谢谢一直支持我的粉丝们

Spring Boot 2.x(十六):玩转vue文件上传的更多相关文章

  1. Spring Boot 2.X(十六):应用监控之 Spring Boot Actuator 使用及配置

    Actuator 简介 Actuator 是 Spring Boot 提供的对应用系统的自省和监控功能.通过 Actuator,可以使用数据化的指标去度量应用的运行情况,比如查看服务器的磁盘.内存.C ...

  2. 风炫安全web安全学习第三十六节课-15种上传漏洞讲解(一)

    风炫安全web安全学习第三十六节课 15种上传漏洞讲解(一) 文件上传漏洞 0x01 漏洞描述和原理 文件上传漏洞可以说是日常渗透测试用得最多的一个漏洞,因为用它获得服务器权限最快最直接.但是想真正把 ...

  3. Spring MVC 学习总结(五)——校验与文件上传

    Spring MVC不仅是在架构上改变了项目,使代码变得可复用.可维护与可扩展,其实在功能上也加强了不少. 验证与文件上传是许多项目中不可缺少的一部分.在项目中验证非常重要,首先是安全性考虑,如防止注 ...

  4. Spring学习笔记2——表单数据验证、文件上传

    在上一章节Spring学习笔记1——IOC: 尽量使用注解以及java代码中,已经搭建了项目的整体框架,介绍了IOC以及mybatis.第二节主要介绍SpringMVC中的表单数据验证以及文件上传. ...

  5. Spring Boot教程(十六)属性配置文件详解(1)

    相信很多人选择Spring Boot主要是考虑到它既能兼顾Spring的强大功能,还能实现快速开发的便捷.我们在Spring Boot使用过程中,最直观的感受就是没有了原来自己整合Spring应用时繁 ...

  6. 学习Spring Boot:(十六)使用Shiro与JWT 实现认证服务

    前言 需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时access_token进行资源访问.这里我们将使用 JWT 1,基于散列的消息认证码,使用一 ...

  7. 第二百七十三节,Tornado框架-文件上传

    Tornado框架-文件上传 第一.普通表单上传文件 self.request.files["fafafa"] 获取上传文件信息,参数["上传文件框的name名称&quo ...

  8. ios开发网络学习十二:NSURLSession实现文件上传

    #import "ViewController.h" // ----WebKitFormBoundaryvMI3CAV0sGUtL8tr #define Kboundary @&q ...

  9. (十五)struts2的文件上传和下载

    文件上传的原理 我们以前学习上传的时候知道需要将表单的enctype属性设置为multipart/form-data. 表单的enctype属性指定的是表单数据的编码方式,有三个值: -applica ...

随机推荐

  1. python 关于celery的异步任务队列的基本使用(celery+redis)【采用配置文件设置】

    工程结构说明:源文件下载请访问https://i.cnblogs.com/Files.aspx __init__.py:实例化celery,并加载配置模块 celeryconfig.py:配置模块 t ...

  2. C# 使用CefSharp嵌入网站

    最近有一个项目,需要在winform中嵌套web网站,从网上了解到几个相关的组件有winform自带的IE内核的WebBrowser,有第三方组件谷歌内核的webkit.cefsharp.chromi ...

  3. 『正睿OI 2019SC Day2』

    分治 普通分治 普通分治是指针对序列或平面问题的分治算法. 思想 普通分治的思想是指将一个序列问题或平面问题通过某种划分方式划分为若干个子问题,直到子问题规模足够小,可以直接回答,再通过合并得到原问题 ...

  4. vim中常用折叠命令

    最常用3个折叠命令 .反复打开关闭折叠:za (意思就是,当光标处折叠处于打开状态,za关闭之,当光标处折叠关闭状态,打开之) .打开全部折叠:zR .关闭全部折叠:zM 小试折叠: :set fdm ...

  5. (原创)C#操作MYSQL数据库

    应用程序对数据库的操作都是只有4个:增,删,改,查. 只有”查”的操作需要使用适配器来存储查询得到的数据.其它3个操作不需要用到适配器. 不同的数据库有共同操作方法:都要建立连接对象,连接对象要有连接 ...

  6. 【JVM学习笔记二】垃圾收集器与内存分配策略

    1. 概述 1) GC的历史比Java久远 2) GC需要完成的三件事: | 哪些内存需要回收 | 什么时候回收 | 如何回收 3) Java内存运行时区域各个部分: | Java虚拟机栈.计数器.本 ...

  7. Node.js 连接 MongoDB数据库

    安装指令:npm install mongodb var mongodb = require("mongodb");// console.log(mongodb); var Mon ...

  8. 微信小程序必知相关知识

    微信小程序必知相关知识 1 请谈谈微信小程序主要目录和文件的作用? project.config.json 项目配置文件,用得最多的就是配置是否开启https校验: App.js 设置一些全局的基础数 ...

  9. HTML5深入学习之鼠标跟随,拖拽事件

    知识点(鼠标跟随): mousedown: 当用户用鼠标点击在某一元素上就会触发该事件 mouseover:  当鼠标指针在某一元素上移动就会触发改事件 下面这个例子的效果就是鼠标点击元素后,元素跟着 ...

  10. JavaScript 流程控制(二)循环结构

    一.while 语句 语法结构: 声明循环变量:while (循环条件) { //循环体 // 迭代条件 } 当循环条件为 true 时,执行循环体:当循环条件为false时,结束循环. 二.do.. ...