.vue文件_Spring Boot 2.x(十六):玩转vue文件上传

6c727d8d2a2cdd1c326219171e082c1f.png

为什么使用Vue-Simple-Uploader

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

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

  • 可暂停、继续上传

  • 上传队列管理,支持最大并发上传

  • 分块上传

  • 支持进度、预估剩余时间、出错自动重试、重传等操作

  • 支持“快传”,通过文件判断服务端是否已存在从而实现“快传”

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

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

Vue代码:

         :options="uploadOptions1"
        :autoStart="true"
        class="uploader-app"
      >
        
        选择文件选择文件夹
        

该组件默认支持多文件上传,这里我们从官方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请求,将刚刚上传的分块合为一个文件,至此文件的断点分块上传就完成了。

7fe478e55f9bfd92f483c9e2ba414054.png
未命名文件

下面是对应的代码~

Vue代码:

        :options="uploadOptions2"
        :autoStart="true"
        :files="files"
        @file-added="onFileAdded2"
        @file-success="onFileSuccess2"
        @file-progress="onFileProgress2"
        @file-error="onFileError2"
      >
        
        分块上传
        

校验是否上传过的代码

 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 checkChunks(Chunk chunk) {
        return uploadService.checkChunkExits(chunk);
    }

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

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

    @PostMapping("chunk")    
    public Map 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 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方便后面进行排序进行mergereturn sb.append(File.separator)
                .append(chunk.getFilename())
                .append("-")
                .append(chunk.getChunkNumber()).toString();
    }/**
     * 保存信息到Redis
     */public Integer saveChunk(Integer chunkNumber, String identifier) {// 获取目前的chunkList
        Set oldChunkNumber = (Set) redisDao.hmGet(identifier, "chunkNumberList");// 如果获取为空,则新建Set集合,并将当前分块的chunkNumber加入后存到Redisif (Objects.isNull(oldChunkNumber)) {
            Set 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中遇到的问题和坑写一写,谢谢一直支持我的粉丝们~爱你们~

0a550494c9685c2e68e65535750c7522.png

我从没见过一个不孤独的人会发出耀眼的光芒

194806099b77d0bfd820ba1d08fa3e2b.png

纯原创技术公号

作者:Vi.Young

扫码获取更多干货

 博客园:Vi的技术博客

7a4813de822cb88bc5dfd2c55a3ef439.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/340732.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

成为Java流专家–第2部分:中级操作

就像魔术棒一样&#xff0c;中间操作将一个Stream转换为另一个Stream。 这些操作可以无穷无尽的方式组合在一起&#xff0c;以可读有效的方式执行从简单到高度复杂的任务。 本文是五分之二&#xff0c;其中还有一个GitHub存储库&#xff0c;其中包含每个单元的说明和练习。 第…

HTML怎么在li中加select标签,自定义UL LI选择框似乎在其他HTML元素后面

我似乎无法弄清楚为什么我的自定义UL LI选择框出现在其他HTML元素后面。你如何解决这个问题&#xff0c;当用户点击选择框时&#xff0c;它会出现在其他页面元素的顶部&#xff1f;这里是手头的问题的一个画面&#xff1a; 下面是一个描绘所期望的结果&#xff1a; 这里是有问题…

java初学者面试_Java面试的前50个问题,面向初学者和经验丰富的程序员

java初学者面试您可以参加任何Java面试&#xff0c;无论是大四还是中级&#xff0c;经验或新来的人&#xff0c;一定会看到线​​程&#xff0c;并发和多线程中的几个问题。 实际上&#xff0c;这种内置的并发支持是Java编程语言的最强优势之一&#xff0c;并帮助它在企业界和程…

convert.todatetime指定日期格式_SQL基础知识V2——常用日期函数

点击上方SQL数据库开发&#xff0c;关注获取SQL视频教程SQL专栏SQL数据库基础知识汇总SQL数据库高级知识汇总日期函数是数据库中经常需要使用到的&#xff0c;本期将常用的几个日期函数给小伙伴做详细的介绍。GETDATE()GETDATE()函数用于返回当前数据库系统的日期和时间&#x…

恢复html的初始选定状态,Adobe Photoshop

了解如何在 Adobe Photoshop 中使用“还原/重做”命令和“历史记录”面板来控制图像的状态。通过使用“还原/重做”命令和“历史记录”面板&#xff0c;您可以轻松地控制图像的状态。使用还原或重做命令Photoshop CC 20.0(2018 年 10 月版)中的更新功能从 Photoshop CC 2018 年…

classcastexception异常_优雅的异常处理

代码正常运行不会出问题&#xff0c;但是遇到意外&#xff0c;参数变量&#xff0c;不符合要求&#xff0c;发生意外&#xff0c;代码终止运行代码中的异常处理好了&#xff0c;代码就可以顺利的继续运行&#xff01;3.3 最近我们遇到过的异常System.out.println(1/0);//java.l…

Spring Boot微服务,Docker和Kubernetes研讨会–第3部分

在之前的文章中&#xff0c;我们为使用Docker和Spring Boot的订单管理系统构建了一些微服务&#xff08;订单服务&#xff0c;产品服务&#xff0c;客户服务&#xff09;。 我们使用Netflix库来管理&#xff0c;发现和平衡微服务。 管理这些微服务及其多个容器可能会有些棘手&…

2021年茂名市高考成绩查询,2021年茂名高考最高分多少分,历年茂名高考状元

2021年茂名高考成绩公布时&#xff0c;茂名高考成绩最高分受到很多网友关注&#xff0c;具体2021年茂名高考最高分多少分&#xff0c;考生可以在聚志愿网站查询具体位次排名&#xff0c;报考茂名高考一分一段表位次排名、同分数人数、位次查询等相关数据。本期文章小编主要为大…

客制化键盘键位修改_可以用很漂亮形容的一把键盘,差一步就完美

内置钢板是轴体固定需要也是机械键盘的标志&#xff0c;钢板除了可以带来极其稳定的固定效果也成就了机械键盘扎实的手感&#xff0c;同时是机械键盘分量感的主要来源&#xff0c;给人一种结实感&#xff0c;这是薄膜键帽所不具备的。▲外置钢板在悬浮式结构的键盘上比较常见&a…

adb 切换默认桌面_公告 | 武林外传手游官方服务器全面开放桌面版体验

为了更好的优化玩家游戏体验&#xff0c;应广大客官要求&#xff0c;《武林外传手游》全新桌面版今日上线&#xff0c;iOS及安卓服务器均可在电脑上畅玩《武林外传手游》啦&#xff01;大屏画面更加细腻绚丽&#xff0c;预设键位助你畅游江湖&#xff0c;欢迎广大玩家下载体验&…

java获取word书签表格数据_Python读取word文档里面的表格数据

更多精彩&#xff0c;请点击上方蓝字关注我们&#xff01;我们常见的办公数据通常可以分为结构化数据与非结构化数据&#xff0c;比如常见的word, ppt, excel。前两者存储的是非结构化数据&#xff0c;excel存储的是结构化数据。从事数据统计或分析的工作或多或少都会从excel获…

abb智能控制系统_ABB助力国网冀北电力打造虚拟电厂

近日&#xff0c;ABB为国网冀北电力有限公司定制了智能配电计量与协调控制解决方案&#xff0c;对其虚拟电厂进行远程电能管理&#xff0c;实现高峰调节和负载转移&#xff0c;提高电力系统效率&#xff0c;保持供电稳定性。虚拟电厂是通过分布式电力管理系统将电网中发电端(尤…

通过通用数据访问扩展AWS生态系统

Amazon Web Services&#xff08;AWS&#xff09;可帮助组织托管和管理其数据流程&#xff0c;例如构建数据可视化和执行ETL任务。 在CData&#xff0c;我们可以轻松地将AWS Services与异构业务应用程序和分布式数据存储连接起来&#xff0c;以最终帮助企业对其数据进行更全面的…

android官方文档中文版_Now in Android:01 - 如何掌握最新的 Android 技术?

每隔几天我都会通过 Android Developers 的油管官方频道&#xff0c;来了解一下最近 Android 发布了哪些有趣的新技术&#xff0c;最近发现官方推出了一个新的系列视频叫做&#xff1a;Now in Android&#xff0c;目前更新了 4 期&#xff0c;我觉得这个系列蛮有趣的&#xff0…

c++ 反射_固体火箭发动机黏接壳体超声C扫描检测系统研制与应用

某固体火箭发动机燃烧室采用壳体/绝热层/包覆层/推进剂的多界面结构形式&#xff0c;其中绝热层采用玻璃纤维缠绕成型后与钢质旋压壳体胶接而成。在胶接过程中&#xff0c;若存在壳体内部多余物清理不干净、绝热层与壳体配合不严、胶层内部气体未排净等情况&#xff0c;黏接层易…

java scala_经过几天的Scala回归Java的10个最烦人的事情

java scala因此&#xff0c;我正在尝试使用Scala&#xff0c;因为我想编写一个解析器&#xff0c;而Scala Parsers API似乎非常合适。 毕竟&#xff0c;我可以在Scala中实现解析器并将其包装在Java接口后面&#xff0c;因此除了附加的运行时依赖关系之外&#xff0c;应该不存在…

python区域找图命令_python读取图片任意范围区域

使用python进行图片处理&#xff0c;现在需要读出图片的任意一块区域&#xff0c;并将其转化为一维数组&#xff0c;方便后续卷积操作的使用。 下面使用两种方法进行处理&#xff1a; convert 函数 from PIL import Image import numpy as np import matplotlib.pyplot as plt …

[MEGA DEAL] Ultimate SQL Bootcamp认证捆绑包(98%)

像Pro一样管理任务和数据库&#xff0c;提供有关SQL Lite&#xff0c;Microsoft SQL&#xff0c;MySQL&#xff0c;PostgreSQL&#xff0c;Rest API和Oracle SQL的6门课程 嘿&#xff0c;怪胎&#xff0c; 本周&#xff0c;在我们的JCG Deals商店 &#xff0c;我们提供了另一…

jersey spring_教程–带有Jersey和Spring的Java REST API设计和实现

jersey spring想要在Java中使用REST&#xff1f; 然后您来对地方了&#xff0c;因为在博客文章中&#xff0c;我将向您介绍如何“美丽”地设计REST API&#xff0c;以及如何使用Jersey框架在Java中实现它。 本教程中开发的RESTful API将演示针对存储在MySql数据库中的播客资源的…

html5 css svg,6款基于SVG的HTML5CSS3应用和动画

1、CSS3/SVG质感背景小图标 镂空效果图标按钮今天我们来分享一款用CSS3和SVG实现的质感背景小图标&#xff0c;鼠标滑过图标时出现镂空的效果&#xff0c;并且有质感背景的描边&#xff0c;效果非常不错。2、HTML5 SVG Tab滑块菜单 非常酷的Tab菜单之前我们分享过很多HTML5/CSS…