摘要:為什么使用最近用到了來完成文件上傳的操作,踩了一些坑,對比了一些的組件,發(fā)現(xiàn)了一個很好用的組件再說說為什么選用這個組件,對比和的上傳組件,它能做到更多的事情,比如可暫停繼續(xù)上傳上傳隊列管理,支持最大并發(fā)上傳分塊上傳支持進度預(yù)估
為什么使用Vue-Simple-Uploader
最近用到了Vue + Spring Boot來完成文件上傳的操作,踩了一些坑,對比了一些Vue的組件,發(fā)現(xiàn)了一個很好用的組件——Vue-Simple-Uploader
再說說為什么選用這個組件,對比vue-ant-design和element-ui的上傳組件,它能做到更多的事情,比如:
可暫停、繼續(xù)上傳
上傳隊列管理,支持最大并發(fā)上傳
分塊上傳
支持進度、預(yù)估剩余時間、出錯自動重試、重傳等操作
支持“快傳”,通過文件判斷服務(wù)端是否已存在從而實現(xiàn)“快傳”
由于需求中需要用到斷點續(xù)傳,所以選用了這個組件,下面我會從最基礎(chǔ)的上傳開始說起:
單文件上傳、多文件上傳、文件夾上傳Vue代碼:
選擇文件 選擇文件夾
該組件默認支持多文件上傳,這里我們從官方demo中粘貼過來這段代碼,然后在uploadOption1中配置上傳的路徑即可,其中uploader-btn 中設(shè)置directory屬性即可選擇文件夾進行上傳。
uploadOption1:
uploadOptions1: { target: "http://localhost:18080/api/upload/single",//上傳的接口 testChunks: false, //是否開啟服務(wù)器分片校驗 fileParameterName: "file",//默認的文件參數(shù)名 headers: {}, query() {}, categaryMap: { //用于限制上傳的類型 image: ["gif", "jpg", "jpeg", "png", "bmp"] } }
在后臺的接口的編寫,我們?yōu)榱朔奖悖x了一個chunk類用于接收組件默認傳輸?shù)囊恍┖竺娣奖惴謮K斷點續(xù)傳的參數(shù):
Chunk類
@Data public class Chunk implements Serializable { private static final long serialVersionUID = 7073871700302406420L; private Long id; /** * 當(dāng)前文件塊,從1開始 */ private Integer chunkNumber; /** * 分塊大小 */ private Long chunkSize; /** * 當(dāng)前分塊大小 */ private Long currentChunkSize; /** * 總大小 */ private Long totalSize; /** * 文件標識 */ private String identifier; /** * 文件名 */ private String filename; /** * 相對路徑 */ private String relativePath; /** * 總塊數(shù) */ private Integer totalChunks; /** * 文件類型 */ private String type; /** * 要上傳的文件 */ private MultipartFile file; }
在編寫接口的時候,我們直接使用這個類作為參數(shù)去接收vue-simple-uploader傳來的參數(shù)即可,注意這里要使用POST來接收喲~
接口方法:
@PostMapping("single") public void singleUpload(Chunk chunk) { // 獲取傳來的文件 MultipartFile file = chunk.getFile(); // 獲取文件名 String filename = chunk.getFilename(); try { // 獲取文件的內(nèi)容 byte[] bytes = file.getBytes(); // SINGLE_UPLOADER是我定義的一個路徑常量,這里的意思是,如果不存在該目錄,則去創(chuàng)建 if (!Files.isWritable(Paths.get(SINGLE_FOLDER))) { Files.createDirectories(Paths.get(SINGLE_FOLDER)); } // 獲取上傳文件的路徑 Path path = Paths.get(SINGLE_FOLDER,filename); // 將字節(jié)寫入該文件 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
下面我們啟動項目,選擇需要上傳的文件就可以看到效果了~ 是不是很方便~ 但是同樣的事情其余的組件基本上也可以做到,之所以選擇這個,更多的是因為它可以支持斷點分塊上傳,實現(xiàn)上傳過程中斷網(wǎng),再次聯(lián)網(wǎng)的話可以從斷點位置開始繼續(xù)秒傳~下面我們來看看斷點續(xù)傳是怎么玩的。
斷點分塊續(xù)傳先說一下分塊斷點續(xù)傳的大概原理,我們在組件可以配置分塊的大小,大于該值的文件會被分割成若干塊兒去上傳,同時將該分塊的chunkNumber保存到數(shù)據(jù)庫(Mysql or Redis,這里我選擇的是Redis)
組件上傳的時候會攜帶一個identifier的參數(shù)(這里我采用的是默認的值,你也可以通過生成md5的方式來重新賦值參數(shù)),將identifier作為Redis的key,設(shè)置hashKey為”chunkNumber“,value是由每次上傳的chunkNumber組成的一個Set集合。
在將uploadOption中的testChunk的值設(shè)置為true之后,該組件會先發(fā)一個get請求,獲取到已經(jīng)上傳的chunkNumber集合,然后在checkChunkUploadedByResponse方法中判斷是否存在該片段來進行跳過,發(fā)送post請求上傳分塊的文件。
每次上傳片段的時候,service層返回當(dāng)前的集合大小,并與參數(shù)中的totalChunks進行對比,如果發(fā)現(xiàn)相等,就返回一個狀態(tài)值,來控制前端發(fā)出merge請求,將剛剛上傳的分塊合為一個文件,至此文件的斷點分塊上傳就完成了。
下面是對應(yīng)的代碼~
Vue代碼:
分塊上傳
校驗是否上傳過的代碼
uploadOptions2: { target: "http://localhost:18080/api/upload/chunk", chunkSize: 1 * 1024 * 1024, testChunks: true, checkChunkUploadedByResponse: function(chunk, message) { let objMessage = JSON.parse(message); // 獲取當(dāng)前的上傳塊的集合 let chunkNumbers = objMessage.chunkNumbers; // 判斷當(dāng)前的塊是否被該集合包含,從而判定是否需要跳過 return (chunkNumbers || []).indexOf(chunk.offset + 1) >= 0; }, headers: {}, query() {}, categaryMap: { image: ["gif", "jpg", "jpeg", "png", "bmp"], zip: ["zip"], document: ["csv"] } }
上傳后成功的處理,判斷狀態(tài)來進行merge操作
onFileSuccess2(rootFile, file, response, chunk) { let res = JSON.parse(response); // 后臺報錯 if (res.code == 1) { return; } // 需要合并 if (res.code == 205) { // 發(fā)送merge請求,參數(shù)為identifier和filename,這個要注意需要和后臺的Chunk類中的參數(shù)名對應(yīng),否則會接收不到~ const formData = new FormData(); formData.append("identifier", file.uniqueIdentifier); formData.append("filename", file.name); merge(formData).then(response => {}); } },
判定是否存在的代碼,注意這里的是GET請求?。?!
@GetMapping("chunk") public MapcheckChunks(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; }
保存分塊,并保存數(shù)據(jù)到Redis的代碼。這里的是POST請求?。?!
@PostMapping("chunk") public MapsaveChunk(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相等,判定分塊已經(jīng)上傳完畢,進行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 路徑是否存在,不存在則創(chuàng)建 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 oldChunkNumber = (Set ) redisDao.hmGet(identifier, "chunkNumberList"); // 如果獲取為空,則新建Set集合,并將當(dāng)前分塊的chunkNumber加入后存到Redis if (Objects.isNull(oldChunkNumber)) { Set newChunkNumber = new HashSet<>(); newChunkNumber.add(chunkNumber); redisDao.hmSet(identifier, "chunkNumberList", newChunkNumber); // 返回集合的大小 return newChunkNumber.size(); } else { // 如果不為空,將當(dāng)前分塊的chunkNumber加到當(dāng)前的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; // 創(chuàng)建文件 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(); } }
至此,我們的斷點續(xù)傳就完美結(jié)束了,完整的代碼我已經(jīng)上傳到gayhub~,歡迎star fork pr~(后面還會把博文也上傳到gayhub喲~)
前端:https://github.com/viyog/vibo...公眾號后臺:https://github.com/viyog/viboot
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/76090.html
摘要:哪吒社區(qū)技能樹打卡打卡貼函數(shù)式接口簡介領(lǐng)域優(yōu)質(zhì)創(chuàng)作者哪吒公眾號作者架構(gòu)師奮斗者掃描主頁左側(cè)二維碼,加入群聊,一起學(xué)習(xí)一起進步歡迎點贊收藏留言前情提要無意間聽到領(lǐng)導(dǎo)們的談話,現(xiàn)在公司的現(xiàn)狀是碼農(nóng)太多,但能獨立帶隊的人太少,簡而言之,不缺干 ? 哪吒社區(qū)Java技能樹打卡?【打卡貼 day2...
摘要:在打開的輸入框內(nèi),可以輸入任何命令。輸入進行項目創(chuàng)建,最后一步選擇依賴的庫。自動補全的能力較弱文件中的類名沒法與代碼聯(lián)動。挪動文件,其他文件中的類名全稱不會自動變化。如果要收費,第二好用還是。輕量,擴展性好,易用性等細節(jié)需要增強。 上期玩轉(zhuǎn)了maven,這期我們來用VSCode一起來玩 spring-boot 安裝spring-boot擴展插件 最主要的插件是 Spring Boot ...
摘要:開公眾號差不多兩年了,有不少原創(chuàng)教程,當(dāng)原創(chuàng)越來越多時,大家搜索起來就很不方便,因此做了一個索引幫助大家快速找到需要的文章系列處理登錄請求前后端分離一使用完美處理權(quán)限問題前后端分離二使用完美處理權(quán)限問題前后端分離三中密碼加鹽與中異常統(tǒng)一處理 開公眾號差不多兩年了,有不少原創(chuàng)教程,當(dāng)原創(chuàng)越來越多時,大家搜索起來就很不方便,因此做了一個索引幫助大家快速找到需要的文章! Spring Boo...
摘要:網(wǎng)上已經(jīng)一些運行不錯的圖床了,比如圖殼路過圖床那為什么我們還要自己搭建圖床呢一來是因為碼農(nóng)總是喜歡折騰,二來是有了自己的圖床數(shù)據(jù)自己存儲更安全。下面是演示地址,可以先看一下搭建完成的效果。我們是以作為原型進行搭建。網(wǎng)上已經(jīng)一些運行不錯的圖床了,比如SM.MS https://sm.ms/圖殼 https://imgkr.com路過圖床 https://imgchr.com/那為什么我們還要自...
閱讀 3106·2021-11-24 10:34
閱讀 3351·2021-11-22 13:53
閱讀 2657·2021-11-22 12:03
閱讀 3624·2021-09-26 09:47
閱讀 3034·2021-09-23 11:21
閱讀 4872·2021-09-22 15:08
閱讀 3340·2021-07-23 10:59
閱讀 1285·2019-08-29 18:31