最近遇見一個需要上傳百兆大檔案的需求,調研了七牛和騰訊雲的切片分段上傳功能,是以在此整理前端大檔案上傳相關功能的實作。
在某些業務中,大檔案上傳是一個比較重要的互動場景,如上傳入庫比較大的Excel表格資料、上傳影音檔案等。如果檔案體積比較大,或者網絡條件不好時,上傳的時間會比較長(要傳輸更多的封包,丢包重傳的機率也更大),使用者不能重新整理頁面,隻能耐心等待請求完成。
下面從檔案上傳方式入手,整理大檔案上傳的思路,并給出了相關執行個體代碼,由于PHP内置了比較友善的檔案拆分和拼接方法,是以服務端代碼使用PHP進行示例編寫。
本文相關示例代碼位于github上,主要參考
聊聊大檔案上傳
大檔案切割上傳
檔案上傳的幾種方式
首先我們來看看檔案上傳的幾種方式。
普通表單上傳
使用PHP來展示正常的表單上傳是一個不錯的選擇。首先建構檔案上傳的表單,并指定表單的送出内容類型為enctype="multipart/form-data",表明表單需要上傳二進制資料。

然後編寫index.php上傳檔案接收代碼,使用move_uploaded_file方法即可(php大法好…)
form表單上傳大檔案時,很容易遇見伺服器逾時的問題。通過xhr,前端也可以進行異步上傳檔案的操作,一般由兩個思路。
檔案編碼上傳
第一個思路是将檔案進行編碼,然後在服務端進行解碼,之前寫過一篇在前端實作圖檔壓縮上傳的部落格,其主要實作原理就是将圖檔轉換成base64進行傳遞
varimgURL = URL.createObjectURL(file);
ctx.drawImage(imgURL, 0, 0);
// 擷取圖檔的編碼,然後将圖檔當做是一個很長的字元串進行傳遞
vardata= canvas.toDataURL( "image/jpeg", 0.5);
在服務端需要做的事情也比較簡單,首先解碼base64,然後儲存圖檔即可
$imgData = $_REQUEST[ 'imgData'];
$base64 = explode( ',', $imgData)[ 1];
$img = base64_decode($base64);
$url = './test.jpg';
if(file_put_contents($url, $img)) {
exit(json_encode( array(
url => $url
)));
}
base64編碼的缺點在于其體積比原圖檔更大(因為Base64将三個位元組轉化成四個位元組,是以編碼後的文本,會比原文本大出三分之一左右),對于體積很大的檔案來說,上傳和解析的時間會明顯增加。
更多關于base64的知識,可以參考Base64筆記。
除了進行base64編碼,還可以在前端直接讀取檔案内容後以二進制格式上傳
// 讀取二進制檔案
functionreadBinary(text){
vardata = newArrayBuffer(text.length);
varui8a = newUint8Array(data, 0);
for( vari = 0; i < text.length; i++){
ui8a[i] = (text.charCodeAt(i) & 0xff);
console.log(ui8a)
varreader = newFileReader;
reader. = function{
readBinary( this.result) // 讀取result或直接上傳
// 把從input裡讀取的檔案内容,放到fileReader的result字段裡
reader.readAsBinaryString(file);
formData異步上傳
FormData對象主要用來組裝一組用 發送請求的鍵/值對,可以更加靈活地發送Ajax請求。可以使用FormData來模拟表單送出。
letfiles = e.target.files // 擷取input的file對象
letformData = newFormData;
formData.append( 'file', file);
axios.post(url, formData);
服務端處理方式與直接form表單請求基本相同。
iframe無重新整理頁面
在低版本的浏覽器(如IE)上,xhr是不支援直接上傳formdata的,是以隻能用form來上傳檔案,而form送出本身會進行頁面跳轉,這是因為form表單的target屬性導緻的,其取值有
_self,預設值,在相同的視窗中打開響應頁面
_blank,在新視窗打開
_parent,在父視窗打開
_top,在最頂層的視窗打開
framename,在指定名字的iframe中打開
如果需要讓使用者體驗異步上傳檔案的感覺,可以通過framename指定iframe來實作。把form的target屬性設定為一個看不見的iframe,那麼傳回的資料就會被這個iframe接受,是以隻有該iframe會被重新整理,至于傳回結果,也可以通過解析這個iframe内的文本來擷取。
functionupload{
varnow = + newDate
varid = 'frame'+ now
$( "body").append( `<iframe style="display:none;" name="${id}" id="${id}" />`);
var$form = $( "#myForm")
$form.attr({
"action": '/index.php',
"method": "post",
"enctype": "multipart/form-data",
"encoding": "multipart/form-data",
"target": id
}).submit
$( "#"+id).on( "load", function{
varcontent = $( this).contents.find( "body").text
try{
vardata = JSON.parse(content)
} catch(e){
console.log(e)
})
大檔案上傳
現在來看看在上面提到的幾種上傳方式中實作大檔案上傳會遇見的逾時問題,
表單上傳和iframe無重新整理頁面上傳,實際上都是通過form标簽進行上傳檔案,這種方式将整個請求完全交給浏覽器處理,當上傳大檔案時,可能會遇見請求逾時的情形
通過fromData,其實際也是在xhr中封裝一組請求參數,用來模拟表單請求,無法避免大檔案上傳逾時的問題
編碼上傳,我們可以比較靈活地控制上傳的内容
大檔案上傳最主要的問題就在于:在同一個請求中,要上傳大量的資料,導緻整個過程會比較漫長,且失敗後需要重頭開始上傳。試想,如果我們将這個請求拆分成多個請求,每個請求的時間就會縮短,且如果某個請求失敗,隻需要重新發送這一次請求即可,無需從頭開始,這樣是否可以解決大檔案上傳的問題呢?
綜合上面的問題,看來大檔案上傳需要實作下面幾個需求
支援拆分上傳請求(即切片)
支援斷點續傳
支援顯示上傳進度和暫停上傳
接下來讓我們依次實作這些功能,看起來最主要的功能應該就是切片了。
檔案切片
參考: 大檔案切割上傳
編碼方式上傳中,在前端我們隻要先擷取檔案的二進制内容,然後對其内容進行拆分,最後将每個切片上傳到服務端即可。
在Java中,檔案FIle對象是Blob對象的子類,Blob對象包含一個重要的方法slice,通過這個方法,我們就可以對二進制檔案進行拆分。
下面是一個拆分檔案的示例,對于up6來說開發者不需要關心拆分的細節,由控件幫助實作,開發者隻需要關心業務邏輯即可。
控件上傳的時候會為每一個檔案塊資料添加相關的資訊,開發者在服務端接收到資料後可以自已進行處理。
伺服器接收到這些切片後,再将他們拼接起來就可以了,下面是PHP拼接切片的示例代碼
對于up6來說,開發人員不需要進行拼接,up6已經提供了示例代碼,已經實作了這個邏輯。
保證唯一性,控件會為每一個檔案塊添加資訊,如塊索引,塊MD5,檔案MD5
斷點續傳
up6自帶續傳功能,up6在服務端已經儲存了檔案的資訊,在用戶端也儲存了檔案的進度資訊。在上傳時控件會自動加載檔案進度資訊,開發者不需要關心這些細節。在檔案塊的處理邏輯中隻需要根據檔案塊索引來識别即可。
此時上傳時重新整理頁面或者關閉浏覽器,再次上傳相同檔案時,之前已經上傳成功的切片就不會再重新上傳了。
服務端實作斷點續傳的邏輯基本相似,隻要在getUploadSliceRecord内部調用服務端的查詢接口擷取已上傳切片的記錄即可,是以這裡不再展開。
此外斷點續傳還需要考慮切片過期的情況:如果調用了mkfile接口,則磁盤上的切片内容就可以清除掉了,如果用戶端一直不調用mkfile的接口,放任這些切片一直儲存在磁盤顯然是不可靠的,一般情況下,切片上傳都有一段時間的有效期,超過該有效期,就會被清除掉。基于上述原因,斷點續傳也必須同步切片過期的實作邏輯。
續傳效果
上傳進度和暫停
通過xhr.upload中的progress方法可以實作監控每一個切片上傳進度。
上傳暫停的實作也比較簡單,通過xhr.abort可以取消目前未完成上傳切片的上傳,實作上傳暫停的效果,恢複上傳就跟斷點續傳類似,先擷取已上傳的切片清單,然後重新發送未上傳的切片。
由于篇幅關系,上傳進度和暫停的功能這裡就先不實作了。
實作效果:
小結
目前社群已經存在一些成熟的大檔案上傳解決方案,如七牛SDK,騰訊雲SDK等,也許并不需要我們手動去實作一個簡陋的大檔案上傳庫,但是了解其原理還是十分有必要的。
本文首先整理了前端檔案上傳的幾種方式,然後讨論了大檔案上傳的幾種場景,以及大檔案上傳需要實作的幾個功能
通過Blob對象的slice方法将檔案拆分成切片
整理了服務端還原檔案所需條件和參數,示範了PHP将切片還原成檔案
通過儲存已上傳切片的記錄來實作斷點續傳
還留下了一些問題,如:合并檔案時避免記憶體溢出、切片失效政策、上傳進度暫停等功能,并沒有去深入或一一實作,繼續學習吧
後端代碼邏輯大部分是相同的,目前能夠支援MySQL,Oracle,SQL。在使用前需要配置一下資料庫,可以參考我寫的這篇文章:http://blog.ncmem.com/wordpress/2019/08/12/java-http%E5%A4%A7%E6%96%87%E4%BB%B6%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0%E4%B8%8A%E4%BC%A0/