之前仿造uploadify寫了一個HTML5版的文件上傳插件,沒看過的朋友可以點此先看一下~得到了不少朋友的好評,我自己也用在了項目中,不論是用戶頭像上傳,還是各種媒體文件的上傳,以及各種個性的業務需求,都能得到滿足。小小開心了一把。
但無論插件再怎么靈活,也難以應付所有的需求,比如,你要上傳一個2G的文件。以現在我們的網速,恐怕再快也得傳半小時。要命的是,如果你在上傳到90%的時候不小心關掉了瀏覽器,或者是手一抖摁了F5,完了,一切還得從頭再來。這種用戶體驗簡直太糟糕了。所以,斷點續傳就十分有必要了。什么是續傳我就不解釋了,用QQ傳文件這么多年,大家都見過了。
這里要說的是斷點續傳都有哪些技術要點。使用傳統的表單提交文件或是HTML5的FormData都是將文件“整塊”提交,服務端取到該文件后再進行轉移、重命名等操作,因此,無法實時保存文件的已上傳部分。而且在http協議下,我們無法保持瀏覽器與服務端的長連接,不能以文件流的形式來提交。所以要解決的問題具體來講有以下幾點:
對上傳的文件進行分割,每次只上傳一小片。服務端接收到文件后追加到原來部分,最后合并成完整的文件。
每次上傳文件片前先獲取已上傳的文件大小,確定本次應切割的位置
每次上傳完成后更新已上傳文件大小的記錄
標識客戶端和服務端的文件,保證不會把A文件的內容追加到B文件上
在參考了張鑫旭大哥的這篇文章后,我將學到的技術應用在了我的插件Huploadify中,成功的添加了斷點續傳功能。在此將技術和插件都分享給大家。
工作原理/技術要點
首先的首先,要明確,如果我們有一個10M的文件,每次切割上傳1M,那么是需要發10次請求來完成的。在http協議下,只能這么搞。斷點上傳分三步來完成:
選擇一個文件后,獲取該文件在服務器上的大小,通過本地存儲或自定義的函數來獲取。
根據已上傳大小切割文件,發出n次請求不斷向服務器提交文件片,服務端不斷追加文件內容
當已上傳文件大小達到文件總大小時,上傳結束
首先是文件的分割,HTML5新增了Blob數據類型,并且提供了一個可以分割數據的方法:slice(),其用法和字符串、數組的slice()方法一樣,可以截取一個二進制文件的一部分。
其次是文件片的保存與追加,我后臺用PHP寫的,先用file_get_contents獲取文件的二進制格式,再用file_put_contents每次將文件追加,具體的寫法可以參照后面,或者是下載我打包好的文件。
接下來我們還需要實時保存已上傳文件的大小,以便于下次上傳前進行正確切割。使用HTML5的localStorage是一種方法,將已上傳的大小保存在本地,下次上傳前先從本地讀取。不過這種方式是很局限的,拋開用戶可能通過各種管家清除掉本地數據不講,假如用戶在A頁面上傳了一個文件的50%,然后在B頁面想把該文件上傳到另外一個地方,結果從本地一讀文件已上傳50%了,直接從51%的位置開始上傳了,顯然是個錯誤。問題就在于本地不能存太多的信息,通過File API只能獲取到文件的原始名稱,無法正確的與服務器上的文件正確匹配。所以真正在項目中用,還得依靠服務端來保存這些數據。
關于如何將數據存在服務端,已經前端如何取數據,我在下面會講到。
技術要點就上面的那么多了,其實也沒有多少技術含量哈~來看看我的插件如何使用吧。
續傳功能的使用方法
文件的引入就不講了,可參考上一篇關于插件的介紹。關鍵點是新增的幾個配置,先來看一下:
?
在服務端保存數據
用戶在使用上傳的時候可能有各種你意想不到的操作,這里我發揮想象描述一下用戶可能的行為:
同一臺機器使用不同帳號登錄,上傳同一個文件
文件上傳了一部分,然后修改了文件內容,再次上傳
文件上傳完成100%,再次上傳該文件
同一個頁面有多個上傳按鈕,上傳同一個文件,或在不同頁面上傳同一個文件
僅僅上面四條,是不是情況就夠復雜了?再加上你系統還有自己的業務邏輯,所以在服務端保存已上傳文件數據是非常有必要的。而且保存數據和獲取數據的函數都交給你來定義,抱著插件有足夠的靈活性。
因為涉及到了服務端的技術,無法演示,我將我項目中的真實使用場景在此講解一下,來展示一下如何自已定義方法來實現服務端保存數據的可靠上傳。我定義的getUploadedSize函數如下:
文件初始化
?
文件上傳完畢的代碼
?
文件塊的處理代碼,up6對文件塊的處理,以及文件續傳的邏輯進行了大幅度的優化,開發者不需要關心續傳的細節,因為up6默認就是自動續傳
?
我向后臺的某個地址發送一個請求,傳遞文件名和文件的最后修改時間為參數,后臺根據這兩個參數來找到與前臺所選擇的文件對應的服務器上的文件,將服務器返回的文件大小return出去,來被插件使用。為什么要傳遞這兩個參數呢?我們在前臺無法知道服務器上的這個文件的名稱,所以使用原始文件名作為一個輔助標識。為了防止用戶在兩次上傳間隔修改了文件,我們把文件的最后修改時間也傳給服務端,讓服務端進行比較,若時間不對應則返回已上傳大小為0,重新上傳此文件。
再來看后臺都要做哪些工作。數據庫中需要有一張表來記錄每個已文件的情況,包含的字段大致有:字段描述
uid用戶ID
id文件ID標識(唯一)
lenSvr服務器文件大小
lenLoc本地文件大小
blockOffset文件塊偏移(在整個文件中的位置)
blockSize文件塊大小
blockIndex文件塊索引(基于1)
blockMd5文件塊MD5
complete當前文件是否已經傳完
根據client_filename和last_modified_date,再加上系統中的其他關聯信息,可以定位到本次上傳的文件在服務端的大小,然后返回給客戶端。當然這是我自己的用法,你也可以根據自己的需求靈活設計。總之最終的目的就是要找到前臺選擇的文件在服務器上真正對應的文件,并將已上傳大小正確返回。
另外需注意的一點,就是在續傳的第二步,不斷提交文件片的過程中,也需要服務端準確定位到相應的文件,不能把A的數據追加到B上。采用的方式也是提交fileName和lastModifyDate兩個參數(已寫在插件內部,可服務端直接獲取),服務端找到對應的文件進行追加。
另外再啰嗦一句,后臺獲取文件的時候需要取成二進制的,而我們提交是使用FormData來提交的,所以PHP代碼需要這么寫:
file_put_contents('uploads/'.$filename,file_get_contents($_FILES["file"]["tmp_name"]),FILE_APPEND);
如果上面的說明還是不夠清楚,就需要你自己來探索一下了,畢竟考慮到插件可能應用在復雜的系統中,很多工作還是需要你來做的。或者你也可以給我留言,我很樂意為你解答疑惑。
該版本的其他改動
從1.0到2.0,Huploadify又新加了很多東西,不過只是新加,使用方式跟之前的沒有變化。例如上面的斷點續傳功能,你如果不想使用,只需設置breakPoints為false即可,插件仍按照以前的方式工作。除了斷點續傳這個大頭,插件還做了如下改動:
增加了onSelect回調函數,在選擇了文件之后觸發,用法與uploadify官網的一致
up6提供了3個事件,選擇文件,選擇文件夾,粘貼
用戶選擇文件時會觸發open_files,選擇文件夾觸發open_folders,粘貼會觸發以上兩個事件,因為用戶可能粘貼的文件和文件夾
?
刪除掉正在上傳的文件,中斷發送請求
完善了input file組件的accept屬性支持,瀏覽時只顯示運行的文件格式,就是這個東東:
?
4.?對外開放了方法調用接口,upload、stop、cancel、disable、ennable。我在demo中有演示。使用方法如下:var up = $('#upload').Huploadify({
auto:false,
fileTypeExts:'*.jpg;*.png;*.exe;*.mp3;*.mp4;*.zip;*.doc;*.docx;*.ppt;*.pptx;*.xls;*.xlsx;*.pdf',
multi:true
});
up.upload(1);//開始上傳文件,接收一個參數,表示上傳第幾個文件,可傳入*上傳隊列中的所有文件
up.stop();//暫停上傳隊列中的所有文件,不接收參數。用于開啟了斷點需傳
up.cancel(1);//刪除隊列中的某個文件,接收一個參數,表示刪除第幾個文件,可傳入*刪除隊列中的所有文件
up.disable();//使選擇文件按鈕失效,不接收參數
up.ennable();//使選擇文件按鈕生效,不接收參數??5. 修改其他已知bug
結束
插件剛剛完成,與我們的后端程序員調試完成了斷點續傳功能暫未發現問題,歡迎大家在使用的時候給我提任何問題。老實來講這個功能使用起來還是挺費解的,為了最大程度的保證靈活做成這樣,大家可以與我多多交流~
我在demo中使用了本地存儲來做已上傳文件大小的保存,下載壓縮包后可看一下效果。上傳一個比較大的視頻文件,上傳到中間關閉瀏覽器,再次打開瀏覽器上傳同一個文件,會看到從上次斷掉的地方繼續上傳。