.vue文件_Spring Boot 2.x(十六):玩转vue文件上传
為什么使用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類
@Datapublic?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代碼:
????????: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中遇到的問題和坑寫一寫,謝謝一直支持我的粉絲們~愛你們~
我從沒見過一個不孤獨的人會發出耀眼的光芒
純原創技術公號
作者:Vi.Young
掃碼獲取更多干貨
?博客園:Vi的技術博客
總結
以上是生活随笔為你收集整理的.vue文件_Spring Boot 2.x(十六):玩转vue文件上传的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nas硬盘装在电脑上(笔记本硬盘做nas
- 下一篇: html5倒计时秒杀怎么做,vue 设