HTML5檔案上傳進度條

在傳統網頁上傳大檔案,得等到全部傳完才會有回應,等待期間沒消沒息,搞不清楚是沒傳完還是當掉常為人詬病,也嚴重破壞使用者體驗。想在傳輸過程回報上傳進度,過去有些Flash、Java Applet或ActiveX的解決方案,但依賴外掛元件有部署及無法跨平台的疑慮。當HTML5規格漸成主流,長久以來的問題總算有了簡潔有效的解法。

要掌握上傳進度有一個關鍵: Client Script必須掌握檔案大小以及已上傳資料量,才可能計算上傳百分比回報狀態。傳統使用選取檔案配合送出鈕的做法,一來無法得知所選取檔案大小,二來在按下POST鈕後Script即失去主導權,一切交由瀏覽器主控,更別奢談得知上傳進度。在 從桌面拖拉檔案到網頁一文提到的HTML5 File API,一舉突破JavaScript無從得知檔案大小的盲點,邁進一大步。而透過XHR(XMLHttpRequest),改用jQuery.ajax()非同步上傳檔案,便能在上傳過程繼續更新網頁回報進度。這樣子只剩下一個挑戰 -- 如何得知已上傳資料量?

好消息! 隨著瀏覽器日新月異,XHR也跟著進化,HTML5世代瀏覽器(IE需為IE10+,IE9又哭哭了...)已內建 XMLHttpRequest Level 2(XHR2),增加不少新功能,包含直接處理ArrayBuffer/Blob等二進位資料的能力,也多了onprogress事件,能在傳輸過程中持續觸發回報上傳進度! 有了新武器,要實現上傳進度回報就簡單多了。

先看成品展示:

選取三個檔案,下方即出現三個包含檔案名稱、狀態文字及檔案大小的進度條,按下【Upload】鈕上傳,狀態會由Waiting轉為Uploading,右方則會顯示已上傳Byte數及百分比,為求酷炫(謎之聲: 上回是誰說要一、二、三大家一起放手的?),我還用CSS做了一個依百分比呈現不同長度的的綠色條。後端接收程式用ASP.NET MVC寫,上傳檔案會被寫入App_Data,展示過程能看到圖案上傳完後出現在App_Data的時機 ,代表的確是用大骨及珍貴藥材下去熬湯絕非添加湯塊

Client端程式碼如下(ASP.NET MVC cshtml):

@{
    Layout = null;
}

 


    
    Ajax Upload Lab
    


    
         
   
               data-bind="event: { change: selectorChange }" />
        
    
    
        
            
            
                
                
                
            
        
    
    
    
    


簡要說明程式重點:

  1. 進度條呈現使用Knockout.js MVVM
    直接擴充File API傳回的File資料,加上uploadedBytes(已上傳大小)、percentage(已上傳百分比)、progress(顯示1024/2048(50.0%)等進度數據)、status(依進度百分比傳回Waiting、Uploading或Done)、message(保留額外狀態訊息用)、widthStyle(控制綠色進度條長度的CSS Style參數)等observable。KO的相依性追蹤機制很好用,只需更新uploadedBytes性,其餘屬性就會自動更新。
  2. 長度會改變的綠色進度條
    用了一點CSS技巧。將
    設成position: absolute、top: 0px、bottom: 0px、left: 0px向上左下三方填滿,向右的邊界則隨百分比進度增加不斷遞減: right: 100%, right: 99%, … , rigth: 0%,就可做出愈來愈長的色條。
  3. $.ajax()上傳二進位資料
    前面提到XHR2支援ArrayBuffer,想將檔案內容原汁原味傳送到伺服器端,故使用FileReader.readAsArrayBuffer()讀成ArrayBuffer,$.ajax()上傳時processData要設false,就能以Byte Array方式將二進位資料完整上傳,在ASP.NET MVC端則以Request.InputStream讀出,確保取得沒被編碼或轉換過的原始內容。

接下來看ASP.NET MVC端:

        [HttpPost]
        public ActionResult Upload(string file)
        {
            //由InputStream取得XHR上傳的內容
            var stream = Request.InputStream;
            long totalLen = stream.Length, uploadedBytes = 0;
 
            //為了展示傳輸進度,故意一次1K慢慢讀
            byte[] buffer = new byte[1024];
            string outPath = Path.Combine(Server.MapPath("~/App_Data"), file);
            using (FileStream fs = new FileStream(outPath, FileMode.Create)) 
            {
                while (uploadedBytes < totalLen)
                {
                    var len = stream.Read(buffer, 0, buffer.Length);
                    fs.Write(buffer, 0, len);
                    uploadedBytes += len;
                    //故意延遲1ms
                    Thread.Sleep(1);
                }
            } 
            return Content("OK");
        }

理論上用InputStream.CopyTo(FileStream)就可以一次將資料寫成檔案。但為展示Server端慢慢消化資料的效果,我採取較曲折的讀取方法,只開1K的byte[],分批慢慢讀取,每讀1K再穿插Thread.Sleep(1)的延遲。

即使在Server端加入機制慢慢消化資料,$.ajax()呼叫後需等一段時間才執行完成,但進度條卻比保時捷911還凶猛,0到100花不到0.1秒。(註: 文首的操作展示屬節目效果,除非網路極慢或檔案超大,瞬間從0到100是正常的) 仔細一想才驚覺 -- MVC端取得InputStream時,XHR已傳完所有資料,後續處理再慢,從XHR的角度資料已100%傳完。換句話說,XHR onprogress回報的進度是指資料上傳進度,而非Server端處理進度。但是,若以"上傳資料"的角度,XHR2進度條在Internet傳輸大檔時能提供使用者即時的狀態回饋,確實是有效的解決方案。

做到這裡,上傳資料進度條完成。但興起一個念頭,手邊專案不乏上傳CSV、Excel或文字檔寫入DB的作業,這類操作中資料傳輸時間很短,大部分時間花在逐筆寫入資料庫,上傳進度條概念可否進一步改成反應伺服器端的處理進度呢? 很有趣的題目,若做得出來手邊有一票模組可以套用,但該怎麼玩呢? 下回待續。

你可能感兴趣的:(jQuery,ASP.NET,MVC,HTML5,knockoutjs)