网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容 |
如果右边没有就找找左边 |
本文适合做过全栈开发的同学,最起码需要会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;
//分片id
private String identifier;
//文件名
private String filename;
//相对路径
private String relativePath;
//总分片数
private int totalChunks;
//spring 接收前端传输来的文件对象
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();
//使用transferTo(dest)方法将上传文件写到服务器上指定的文件;
//只能使用一次,原因是文件流只可以接收读取一次,传输完毕则关闭流;
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;//文件唯一id
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;
/**
* @author luoliang
* @date 2018/6/19
*/
@RestController
@RequestMapping(value = "/ffmpeg-video",produces = "application/json; charset=UTF-8")
@Slf4j
public class UploadController {
@Autowired
private FileUploadService fileUploadService;
/**
* 上传前调用(只调一次),判断文件是否已经被上传完成,如果是,跳过,
* 如果不是,判断是否传了一半,如果是,将缺失的分片编号返回,让前端传输缺失的分片即可
* @param file 文件参数
* @return
*/
@GetMapping("/upload")
public ResponseEntity<Map<String,Object>> uploadCheck(MultipartFileParams file){
log.info("file: "+file);//打印日志
return fileUploadService.uploadCheck(file);
}
/**
* 上传调用
* @param file
* @return
*/
@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;
/**
* 判断文件是否已经被上传完成,如果是,跳过,
* 如果不是,判断是否传了一半,如果是,将缺失的分片编号返回,让前端传输缺失的分片即可
* @param fileParams
* @return
*/
@Override
public ResponseEntity<Map<String,Object>> uploadCheck(MultipartFileParams fileParams) {
//获取文件唯一id
String fileDir = fileParams.getIdentifier();
String filename = fileParams.getFilename();
//分片目录
String chunkPath = uploadFilePath + fileDir+"/chunk/";
//分片目录对象
File file = new File(chunkPath);
//获取分片集合
List<File> chunkFileList = MergeFileUtil.chunkFileList(file);
//合并后文件路径
//合并文件路径,D:/develop/video/文件唯一id/merge/filename
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);
}
/**D:/develop/video/文件唯一id/chunk/分片编号
* 分片上传文件
* @param fileParams 文件参数
* @return
*/
@Override
public ResponseEntity<String> upload(MultipartFileParams fileParams) {
//获取文件唯一id
String fileDir = fileParams.getIdentifier();
//分片编号
int chunkNumber = fileParams.getChunkNumber();
//文件路径,文件具体路径,D:/develop/video/文件唯一id/chunk/1
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(dest)方法将上传文件file写到服务器上指定的dest文件;
//只能使用一次,原因是文件流只可以接收读取一次,传输完毕则关闭流;
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/";
//合并文件,D:/develop/video/文件唯一id/merge/filename
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 {
/**
* 判断要上传分片的目录路径存不存在,不存在就创建,
* @param filePath 分片路径
* @return File对象
*/
public static File isUploadChunkParentPath(String filePath){
File fileTemp = new File(filePath);
File parentFile = fileTemp.getParentFile();
if(!parentFile.exists()){
parentFile.mkdirs();
}
return fileTemp;
}
/**
* 合并文件,D:/develop/video/文件唯一id/merge/filename
* @param uploadPath 上传路径 D:/develop/video/
* @param chunkPath 分片文件目录路径
* @param mergePath 合并文件目录D:/develop/video/文件唯一id/merge/
* @param fileName 文件名
* @return
*/
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();//创建文件,已存在返回false,不存在创建文件,目录不存在直接抛异常
} catch (IOException e) {
e.printStackTrace();
}
if(!newFile){//如果newFile=false,表示文件存在
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;
}
/**
* 获取指定块文件目录所有文件
* @param file 块目录
* @return 文件list
*/
public static List<File> chunkFileList(File file){
//获取目录所有文件
File[] files = file.listFiles();
if(files == null){
return null;
}
//转换为list,方便排序
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', // '//jsonplaceholder.typicode.com/posts/',
testChunks: true,//开启分片测试,存在不上传,不存在才上传
//会在整个文件上传开始前,发送一次'/ffmpeg-video/upload'同名的get请求,并将响应体就是message,传入下面函数中
//只请求一次后端,但是下面函数是每次发送分片都自动调用一次
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 () {
// debugger
console.log('complete', arguments)
},
fileComplete () {
console.log('file complete', arguments)
},
fileSuccess(){
this.$axios({
method:'post',
url:'/ffmpeg-video/upload-success',
data: arguments[1]//这个是fileSuccess的回调值,可以直接拿来用
}).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. 运行结果
四、实战开发