网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容 |
---|
如果右边没有就找找左边 |
运行效果:分片上传,已经上传过,如果丢失某些分片,会自动续传,已经上传完成,会直接跳过,完成秒传 |
---|

本文适合做过全栈开发的同学,最起码需要会vue+spring boot的前后端环境搭建,以及基本的前后端交互逻辑,否则你是听不懂的,最起码是做不了测试的 |
---|
如果你只是前端工程师,那么自己mock模拟响应就可以了 |
- 希望大家多多支持这位up,讲的真的很好

一、helloworld环境搭建
1. 前端环境搭建
- 下载vue-simple-upload源码https://github.com/simple-uploader/vue-uploader

- 将项目导入开发工具中,然后进入example文件夹中App.vue文件,指定后端上传文件接口(这个接口路径使我们自己规定,如果你不理解,直接和我写成一样的)

- 解决跨域问题

'/ffmpeg-video':{
target:'http://localhost:3000',
changeOrigin:true,
pathRewrite:{
'^/ffmpeg-video':'/ffmpeg-video'
}
}
- npm install 安装所有依赖

- npm run dev 启动项目


2. 后端环境
- 创建一个基本的spring boot项目,保证有spring-boot-starter-web依赖
- 配置文件,配置上传路径和端口号(端口号需要和你前端指定的一致)

- 实体类

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class MultipartFileParams {
private int chunkNumber;
private long chunkSize;
private long currentChunkSize;
private long totalSize;
private String identifier;
private String filename;
private String relativePath;
private int totalChunks;
private MultipartFile file;
}
- service层


import com.yzpnb.entity.MultipartFileParams;
import com.yzpnb.service.FileUploadService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Service
public class FileUploadServiceImpl implements FileUploadService {
@Value("${upload.file.path}")
private String uploadFilePath;
@Override
public ResponseEntity<String> upload(MultipartFileParams fileParams) {
String filePath = uploadFilePath + fileParams.getFilename();
File fileTemp = new File(filePath);
File parentFile = fileTemp.getParentFile();
if(!parentFile.exists()){
parentFile.mkdirs();
}
try {
MultipartFile file = fileParams.getFile();
file.transferTo(fileTemp);
} catch (IOException e) {
e.printStackTrace();
}
return ResponseEntity.ok("SUCCESS");
}
}
- controller

import com.yzpnb.entity.MultipartFileParams;
import com.yzpnb.service.FileUploadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/ffmpeg-video",produces = "application/json; charset=UTF-8")
@Slf4j
public class UploadController {
@Autowired
private FileUploadService fileUploadService;
@PostMapping("/upload")
public ResponseEntity<String> upload(MultipartFileParams file){
log.info("file: ",file);
return fileUploadService.upload(file);
}
}
3. 上传文件测试
- debug启动后端
- 去掉自带的debugger,然后启动前端

- 上传文件

二、源码分析
- directive 可以让你自己造一个指令,并封装相应逻辑
- mixins 可以让你将封装好的data,mounted等混入你需要的,就相当于复制一份,比如好几个组件都有相同的data和mounted,那么我们不用每个文件都写一遍,直接混入即可
- extends 作用不是复制,而是继承,扩展的意思
- provide 可以做到伪响应式,大范围的 data 和 menthod 等共用,当我们用provide将一些东西暴露出去后,就可以在其它组件用inject注入进来,但必须有血缘关系

- 父组件可以使用 props 把数据传给子组件。
- 子组件可以使用 $emit,让父组件监听到自定义事件 。
- vm.$emit( 事件名, 传递的参数) //触发当前实例上的事件
- vm.$on(事件名,function函数);//监听event事件后运行 fn
- 比如子组件定义vm.$emit(“show”,data),那么父组件,使用子组件时,就可以通过@show=""来引用事件,子组件每执行一次vm. $emit(“show”,data),父组件就触发一次@show
- 因为vue-simple-upload是封装的 simple-uploader.js,所以常用的属性和事件你得知道
- 常用属性

- 常用事件
> 8. simple-uploader.js更多的事件,请参考官方文档https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md
上面的东西没看到,看源码是很费劲的哟,或者看源码时有不知道的,可以回头来查 |
---|
1. mixins.js文件
- 首先 uploader.vue文件,暴露提供了一个uploader

mixins.js此文件是专门提供给组件混入的,通过minxins指令 |
---|

- 这个文件,向外暴露了support变量,并通过inject注入了uploader
2. uploader-btn


- 上面的assignBrowse方法是simple-uploader.js的,具体为什么,在uploader.vue中讲解
- 这个方法除了传输3个props变量,还将自己传输了进去,通过$refs的方式(this. $refs.名字这种方式获取dom结点)
- 这个方法主要就是,点击选择文件时,弹出选择文件窗口,
3. uploader-unsupport
当你的浏览器不支持Uploader.js时会提示,这个库需要支持HTML5 File API以及文件切片。

4. uploader-drop
这个文件是拖动文件到指定位置,也就是选择文件的一个东西,可以拖动文件到这里,但没有上传逻辑,只是将文件选择好 |
---|


5. uploader-list
这个文件主要负责上传文件后的列表显示(由 Uploader.File 文件、文件夹对象组成的数组,文件和文件夹共存) |
---|

- 上图中,我们可以看到fileList变量是定义在computed中而不是data域中,作用是,当uploader.fileList中的值发生变化,会立即展示结果,比如我们上传一个文件,这时fileList就会加入一个文件对象,那么就会立即双向绑定渲染出来
- computed用来监控自己定义的变量,该变量不在data里面声明,直接在computed里面定义,然后就可以在页面上进行双向数据绑定展示出结果或者用作其他处理
- computed比较适合对多个变量或者对象进行处理后返回一个结果值,也就是数多个变量中的某一个值发生了变化则我们监控的这个值也就会发生变化,举例:购物车里面的商品列表和总金额之间的关系,只要商品列表里面的商品数量发生变化,或减少或增多或删除商品,总金额都应该发生变化。这里的这个总金额使用computed属性来进行计算是最好的选择
6. uploader-files
和上面uploader-list基本相同,唯一不同点就是引用的对象不一样(文件对象组成的数组,纯文件列表,没有文件夹) |
---|

7. uploader-file
文件、文件夹单个组件,就是在列表中显示的单个文件,这个组件相对代码较多,我将文档的内容直接搬过来,一个个介绍太浪费时间了 |
---|













8. uploader
源码位置:vue-upload-master文件夹->src文件夹->components文件夹->uploader.vue文件 |
---|





三、分片上传
1. 后端
- 实体类

import lombok.Data;
@Data
public class FileInfo {
private String uniqueIdentifier;
private String name;
}
- controller

import com.yzpnb.entity.FileInfo;
import com.yzpnb.entity.MultipartFileParams;
import com.yzpnb.service.FileUploadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping(value = "/ffmpeg-video",produces = "application/json; charset=UTF-8")
@Slf4j
public class UploadController {
@Autowired
private FileUploadService fileUploadService;
@GetMapping("/upload")
public ResponseEntity<Map<String,Object>> uploadCheck(MultipartFileParams file){
log.info("file: "+file);
return fileUploadService.uploadCheck(file);
}
@PostMapping("/upload")
public ResponseEntity<String> upload(MultipartFileParams file){
log.info("file: "+file);
return fileUploadService.upload(file);
}
@PostMapping("/upload-success")
public ResponseEntity<String> uploadSuccess(@RequestBody FileInfo file){
return fileUploadService.uploadSuccess(file);
}
}
- service


import com.yzpnb.entity.FileInfo;
import com.yzpnb.entity.MultipartFileParams;
import com.yzpnb.service.FileUploadService;
import com.yzpnb.utils.MergeFileUtil;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Log4j2
public class FileUploadServiceImpl implements FileUploadService {
@Value("${upload.file.path}")
private String uploadFilePath;
@Override
public ResponseEntity<Map<String,Object>> uploadCheck(MultipartFileParams fileParams) {
String fileDir = fileParams.getIdentifier();
String filename = fileParams.getFilename();
String chunkPath = uploadFilePath + fileDir+"/chunk/";
File file = new File(chunkPath);
List<File> chunkFileList = MergeFileUtil.chunkFileList(file);
String filePath = uploadFilePath + fileDir+"/merge/"+filename;
File fileMergeExist = new File(filePath);
String [] temp;
boolean isExists = fileMergeExist.exists();
if(chunkFileList == null){
temp= new String[0];
}else{
temp = new String[chunkFileList.size()];
if(!isExists && chunkFileList.size()>0){
for(int i = 0;i<chunkFileList.size();i++){
temp[i] = chunkFileList.get(i).getName();
}
}
}
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("code",1);
hashMap.put("message","Success");
hashMap.put("needSkiped",isExists);
hashMap.put("uploadList",temp);
return ResponseEntity.ok(hashMap);
}
@Override
public ResponseEntity<String> upload(MultipartFileParams fileParams) {
String fileDir = fileParams.getIdentifier();
int chunkNumber = fileParams.getChunkNumber();
String filePath = uploadFilePath + fileDir+"/chunk/"+chunkNumber;
File fileTemp = new File(filePath);
File parentFile = fileTemp.getParentFile();
if(!parentFile.exists()){
parentFile.mkdirs();
}
try {
MultipartFile file = fileParams.getFile();
file.transferTo(fileTemp);
} catch (IOException e) {
e.printStackTrace();
}
return ResponseEntity.ok("SUCCESS");
}
@Override
public ResponseEntity<String> uploadSuccess(FileInfo fileInfo) {
log.info("filename: "+fileInfo.getName());
log.info("UniqueIdentifier: "+fileInfo.getUniqueIdentifier());
String chunkPath = uploadFilePath + fileInfo.getUniqueIdentifier()+"/chunk/";
String mergePath = uploadFilePath + fileInfo.getUniqueIdentifier()+"/merge/";
File file = MergeFileUtil.mergeFile(uploadFilePath,chunkPath, mergePath,fileInfo.getName());
if(file == null){
return ResponseEntity.ok("ERROR:文件合并失败");
}
return ResponseEntity.ok("SUCCESS");
}
}
- 工具类

package com.yzpnb.utils;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
public class MergeFileUtil {
public static File isUploadChunkParentPath(String filePath){
File fileTemp = new File(filePath);
File parentFile = fileTemp.getParentFile();
if(!parentFile.exists()){
parentFile.mkdirs();
}
return fileTemp;
}
public static File mergeFile(String uploadPath,String chunkPath,String mergePath,String fileName){
File file = new File(chunkPath);
List<File> chunkFileList = chunkFileList(file);
File fileTemp = new File(mergePath);
if(!fileTemp.exists()){
fileTemp.mkdirs();
}
File mergeFile = new File(mergePath + fileName);
if(mergeFile.exists()){
mergeFile.delete();
}
boolean newFile = false;
try {
newFile = mergeFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if(!newFile){
return null;
}
try {
RandomAccessFile raf_write = new RandomAccessFile(mergeFile,"rw");
byte[] b = new byte[1024];
for(File chunkFile:chunkFileList){
RandomAccessFile raf_read = new RandomAccessFile(chunkFile,"r");
int len =-1;
while((len = raf_read.read(b))!=-1){
raf_write.write(b,0,len);
}
raf_read.close();
}
raf_write.close();
} catch (Exception e) {
e.printStackTrace();
return null;
}
return mergeFile;
}
public static List<File> chunkFileList(File file){
File[] files = file.listFiles();
if(files == null){
return null;
}
List<File> chunkFileList = new ArrayList<>();
chunkFileList.addAll(Arrays.asList(files));
Collections.sort(chunkFileList, new Comparator<File>() {
@Override public int compare(File o1, File o2) {
if(Integer.parseInt(o1.getName())>Integer.parseInt(o2.getName())){
return 1;
}
return -1;
}
});
return chunkFileList;
}
}
2. 前端
- 引入axios


- 上传前的判断,实现上传文件前,先判断是否已经存在文件等操作(代码统一放在最后)

- 上传成功后的回调,请求后端上传成功接口,然后合并分片


<template>
<uploader
:options="options"
:file-status-text="statusText"
class="uploader-example"
ref="uploader"
@file-complete="fileComplete"
@complete="complete"
@file-success="fileSuccess"
></uploader>
</template>
<script>
export default {
data () {
return {
options: {
target: '/ffmpeg-video/upload',
testChunks: true,
checkChunkUploadedByResponse:function(chunk,message){
let messageObj = JSON.parse(message)
if(messageObj.needSkiped){
return true
}else{
return (messageObj.uploadList || []).indexOf(chunk.offset+1+"")>=0
}
return true
}
},
attrs: {
accept: 'image/*'
},
statusText: {
success: '成功了',
error: '出错了',
uploading: '上传中',
paused: '暂停中',
waiting: '等待中'
}
}
},
methods: {
complete () {
console.log('complete', arguments)
},
fileComplete () {
console.log('file complete', arguments)
},
fileSuccess(){
this.$axios({
method:'post',
url:'/ffmpeg-video/upload-success',
data: arguments[1]
}).then(response =>{
console.log("fileSuccess")
},error =>{
})
}
},
mounted () {
console.log( localStorage.getItem("Access-Token"))
this.$nextTick(() => {
window.uploader = this.$refs.uploader.uploader
})
}
}
</script>
<style>
.uploader-example {
width: 880px;
padding: 15px;
margin: 40px auto 0;
font-size: 12px;
box-shadow: 0 0 10px rgba(0, 0, 0, .4);
}
.uploader-example .uploader-btn {
margin-right: 4px;
}
.uploader-example .uploader-list {
max-height: 440px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
</style>
3. 运行结果








四、实战开发