框架基礎:ajax設計方案(三)--- 集成ajax上傳技術 大文件/超大文件前端切割上傳,後端進行重組
同步更新博客:框架基礎:ajax設計方案(三)--- 集成ajax上傳技術 大文件/超大文件前端切割上傳,後端進行重組
馬上要過年了,哎,回家的心情也特別的激烈。有錢沒錢,回家過年,家永遠是舔舐傷口最好的地方。新的一年繼續加油努力。
上次做了前端的ajax的上傳文件技術,支持單文件,多文件上傳,並對文件的格式和大小進行檢查和限制。但是上次還有個心結一直沒解開,就是本來前端瀏覽器的文件切割已經好了,但是後台文件重組一直沒搞明白和處理好,所以就擱置了。主要也是對自己的代碼負責,因為自己本身都沒把這個技術搞透徹,外加各種測試都沒通過,不想這樣打臉。所以這個心結一直憋了好久,做夢都在想。終於功夫不負有心人,這周終於將這個問題幹掉了,一個字:爽!!!下面咱們直接干!!
一些概念:
前端ajax的level2的方案中可以發送很多數據類型,具體可查看ajax2的設計規範(PS:w3c的原版設計規範,打開有點慢)
js新的技術中增加了File對象(實際上就是blob的具體化的一個東西),設計規範參考這裡2個 File API 規範 和 FileSystem API 規範
後台二進位文件重組
1. 創建空的文件流
2. 讀取臨時存儲切割文件的文件夾,獲得所有文件路徑 切記:一定要按順序進行排序,否則組合文件將會錯誤
3. 按順序將切割小文件讀取成二進位流,寫進空的文件流中
4. 寫入完成,關閉文件流,刪除臨時存儲切割文件的地方
工具準備:
1. 前端代碼,包含ajax庫和測試代碼
2. nginx伺服器,做分離,反向代理後台代碼
3. 文件MD5計算工具,檢查文件切割上傳完成之後是否改變
4. IIS伺服器,部署後台切割文件組合代碼
5. postMan介面測試工具,測試介面狀態
具體思路:
1. 前端瀏覽器選擇文件(如果默認沒有超過切割大小將使用默認上傳,否則將進行文件切割上傳)
2. 前端使用新的特性將文件進行切割成一片一片的子文件,並每次分配一個請求
3. 後端接受文件,將子文件進行存儲
4. 後端根據請求參數判斷,如果為最後一次切割文件上傳,則執行切割文件重組
5. 重組文件,並清除臨時存儲的子文件
前端切割文件代碼:
//切割大文件 cutFile:function(file,cutSize){ var count = file.size / cutSize | 0 ,fileArr = []; for (var i= 0; i< count ; i++){ fileArr.push({ name:file.name+".part"+(i+1), file:file.slice( cutSize * i , cutSize * ( i + 1 )) }); }; fileArr.push({ name:file.name+".part"+(count+1), file:file.slice(cutSize*count,file.size) }); return fileArr; }
前端切割文件上傳代碼:
/* * ajax大文件切割上傳(支持單個文件) -- level2的新特性,請保證你的項目支持新的特性再使用 * url 文件上傳地址 * fileSelector input=file 選擇器 * cutSize 切割文件大小 * fileType 文件限制類型 mime類型 * successEvent 上傳成功處理 * progressEvent 上傳進度事件 * errorEvent 上傳失敗處理 * timeoutEvent 超時處理事件 * * return: status: 0 請選擇文件 * 1 非允許文件格式 * */ upload_big:function(url,fileSelector,cutSize,fileType,successEvent,progressEvent,errorEvent,timeoutEvent){ var file = document.querySelector(fileSelector).files,result ={}; //以下為上傳文件限制檢查 if (file.length === 1){ if (fileType != "*"){ if (fileType.indexOf(file.type)=== -1 ){ result["status"] = 1; result["errMsg"] = "非允許文件格式"; } } }else{ result["status"] = 0; result["errMsg"] = "請選擇文件/只能上傳一個文件"; }; if (result.status !== undefined) return result; //如果有錯誤信息直接拋出去,結束運行 //判斷上傳文件是否超過需要切割的大小 if (file[0].size > cutSize){ var fileArr = tool.cutFile(file[0],cutSize); //切割文件 cutFile_upload(fileArr); }else{ return tempObj.upload(url,fileSelector,file[0].size,fileType,successEvent,errorEvent,timeoutEvent); }; /* * 切割文件上傳,配合後台介面進行對接 * 傳輸參數: * count -- 當前傳輸part的次數 * name -- 做過處理的文件名稱 * file -- 上傳的.part的切割文件 * isLast -- 是否為最後一次切割文件上傳(默認值:"true" 字元串,只有最後一次才附加) * */ function cutFile_upload(fileArr,count){ var formData = new FormData(); if (count == undefined){ count = 0; formData.append("count",count); formData.append("name",fileArr[0].name); formData.append("file".name,fileArr[0].file); }else{ if (count === fileArr.length-1){ formData.append("isLast","true") }; formData.append("count",count); formData.append("name",fileArr[count].name); formData.append("file".name,fileArr[count].file); }; var ajaxParam ={ type:"post", url:url, data:formData, isFormData:true, success:function(data){ /* * data 參數設置 需要後台介面配合 * 建議:如果後台成功保存.part文件,建議返回下次所需要的部分,比如當前發送count為0,則data返回下次為1。 * 如果保存不成功,則可false,或者返回錯誤信息,可在successEvent中處理 * * */ progressEvent(count+1,fileArr.length); //上傳進度事件,第一個參數:當前上傳次數;第二個參數:總共文件數 var currCount = Number(data); if (currCount){ if (currCount != fileArr.length){ cutFile_upload(fileArr,currCount); }; }; successEvent(data); //成功處理事件 }, error:errorEvent, timeout:timeoutEvent }; ajax.common(ajaxParam); } }
後端文件重組代碼(.NET webAPI)-- 其他任何後端語言思想是通用的:
[Route("upload5")] public int Post_bigFile1() { //前端傳輸是否為切割文件最後一個小文件 var isLast = HttpContext.Current.Request["isLast"]; //前端傳輸當前為第幾次切割小文件 var count = HttpContext.Current.Request["count"]; //獲取前端處理過的傳輸文件名 string fileName = HttpContext.Current.Request["name"]; //存儲接受到的切割文件 HttpPostedFile file = HttpContext.Current.Request.Files[0]; //處理文件名稱(去除.part*,還原真實文件名稱) string newFileName = fileName.Substring(0, fileName.LastIndexOf(.)); //判斷指定目錄是否存在臨時存儲文件夾,沒有就創建 if (!System.IO.Directory.Exists(@"D:" + newFileName)) { //不存在就創建目錄 System.IO.Directory.CreateDirectory(@"D:" + newFileName); } //存儲文件 file.SaveAs("D:" + newFileName + "" + HttpContext.Current.Request["name"]); //判斷是否為最後一次切割文件傳輸 if (isLast == "true") { //判斷組合的文件是否存在 if (File.Exists(@"L:" + newFileName))//如果文件存在 { File.Delete(@"L:" + newFileName);//先刪除,否則新文件就不能創建 } //創建空的文件流 FileStream FileOut = new FileStream(@"L:" + newFileName, FileMode.CreateNew,FileAccess.ReadWrite); BinaryWriter bw = new BinaryWriter(FileOut); //獲取臨時存儲目錄下的所有切割文件 string[] allFile = Directory.GetFiles("D:" + newFileName); //將文件進行排序拼接 allFile = allFile.OrderBy(s => int.Parse(Regex.Match(s, @"d+$").Value)).ToArray(); //allFile.OrderBy(); for (int i = 0; i < allFile.Length; i++) { FileStream FileIn = new FileStream(allFile[i], FileMode.Open); BinaryReader br = new BinaryReader(FileIn); byte[] data = new byte[1048576]; //流讀取,緩存空間 int readLen = 0; //每次實際讀取的位元組大小 readLen = br.Read(data,0, data.Length); bw.Write(data,0, readLen); //關閉輸入流 FileIn.Close(); }; //關閉二進位寫入 bw.Close(); FileOut.Close(); } return int.Parse(count) + 1; }
以下為測試代碼:
html頁面代碼:
選擇文件:<input type="file" id="file1" multiple accept="*"/><br /><input type="button" id="upload" value="上傳" />
js代碼:
$("#upload").click(function () { var temp = ajax.upload_big("/api/ajaxUpload/upload5/","#file1",1024*1024,"*",function(x){},function(count,all){console.log("當前傳輸進度:"+count+"/"+all);})});
瀏覽器測試結果:
IE10-11:
chrome
opera:
火狐
edge
360瀏覽器
代碼已集成github:GerryIsWarrior/ajax點顆星星是我最大的鼓勵,有什麼問題可以博客、郵箱、github上留言
這一次上傳版本,代碼做過變動,變動如下:
- 增加大文件傳輸方法upload_big,工具類增加文件切割tool.cutFile
- 解決火狐瀏覽器默認要求後台返回xml類型的問題
遺留問題待確認:這次safair瀏覽器沒有測試,因為File對象的slice方法不支持,加各種前綴測試都不支持,主要我是window版的safair,這個問題先記著,有測試過的兄弟可以幫忙看一下,我的window版safair瀏覽器是不支持的。
還有最重要的一點,如果有問題歡迎指出來,我在github上維護這個庫,這段時間專註於前端的通信技術的研究,第一個階段是ajax的通信技術,後期包括伺服器的SSE推送技術,還有webScoket技術。
其實研究這個技術最大的感慨就是,本來前端切割文件相對來說簡單,但是後台文件重組有點問題,查閱各種資料,翻牆去谷歌等等,搞了好長一段時間才把後台介面的設計和實現完善。以後技術的發展不僅僅局限於一端的,所以能全棧發展就全棧發展,新的前端技術,都是需要後台進行配合才可以溜起來。對了,其實這些東西國外11年左右就開始搞了,國內還是相對不是很跟進,這次去外面看看發現了好多東西,還是需要多看看的,增長技術視野的。哦了,吃飯去了,再不吃要餓死了。
我的前端分散式,容器化,組件化的框架正在從無到有,到時候歡迎大家給建議和指正,3q。
再啰嗦幾句,馬上要過年了,大家吃好喝好玩好,來年繼續奮戰。代碼改變世界。
推薦閱讀: