HTML5檔案上傳進度條

标签: jQuery ASP.NET MVC HTML5 knockoutjs | 发表时间:2014-03-09 22:51 | 作者:Jeffrey
出处:http://blog.darkthread.net/blogs/darkthreadtw/default.aspx

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

要掌握上傳進度有一個關鍵: Client Script必須掌握檔案大小以及已上傳資料量,才可能計算上傳百分比回報狀態。傳統使用<input type="file">選取檔案配合<input type="submit">送出鈕的做法,一來無法得知所選取檔案大小,二來在按下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;
}
<!DOCTYPE html>
 
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Ajax Upload Lab</title>
    <style>
        .item {
            background-color: #6699CC; border: 1px solid gray;
            font-family: 'Courier New'; font-size: 8.5pt;
            margin-bottom: 6px; padding: 3px; color: yellow;
            box-shadow: 3px 3px 3px 1px rgba(128, 128, 128, 0.7);
        }
            .item .name { text-shadow: 1px 1px gray;  }
 
        .prg-zone { margin-top: 10px; }
 
        .bar {
            background-color: #666666;
            height: 15px; position: relative;
            margin: 3px; margin-top: 6px;
            margin-bottom: 6px; border: 1px solid #ccc;
            border-top-color: #444; border-left-color: #444;
        }
            .bar > div {
                position: absolute; color: white; font-size: 8pt;
            }
            .bar .color-bar {
                background-color: #99bb33; top: 0px; bottom: 0px;
                left: 0px;
            }
            .bar .status { top: 0px; left: 6px; }
            .bar .progress { top: 0px; right: 4px; }
    </style>
</head>
<body>
    <div>
        <input type="file" id="fileSelector" multiple
               data-bind="event: { change: selectorChange }" />
        <input type="button" value="Upload" data-bind="click: upload" />
    </div>
    <div data-bind="foreach: files" class="prg-zone">
        <div class="item">
            <div data-bind="text: name" class="name"></div>
            <div class="bar">
                <div class="color-bar" data-bind="attr: { 'style': widthStyle }"></div>
                <div class="status" data-bind="text: status"></div>
                <div class="progress" data-bind="text: progress"></div>
            </div>
        </div>
    </div>
    <script src="~/Scripts/jquery-2.1.0.js"></script>
    <script src="~/Scripts/knockout-3.1.0.debug.js"></script>
    <script>
        $(function () {
 
            function viewModel() {
                var self = this;
                self.files = ko.observableArray();
                self.selectorChange = function (item, e) {
                    self.files.removeAll();
                    $.each(e.target.files, function (i, file) {
                        //加入額外屬性
                        file.uploadedBytes = ko.observable(0); //已上傳Bytes
                        file.percentage = ko.computed(function () { //上傳百分比
                            return (file.uploadedBytes() * 100 / file.size).toFixed(1);
                        });
                        file.widthStyle = ko.computed(function () {
                            return "right:" + (100 - file.percentage()) + "%";
                        });
                        //上傳進度數字顯示
                        file.progress = ko.computed(function () {
                            var perc = file.percentage();
                            return file.uploadedBytes.peek() + "/" + file.size +
                                "(" + perc + "%)";
                        });
                        file.message = ko.observable();
                        file.status = ko.computed(function () {
                            var msg = file.message(), perc = file.percentage();
                            if (msg) return msg;
                            if (perc == 0) return "Waiting";
                            else if (perc == 100) return "Done";
                            else return "Uploading...";
                        });
                        self.files.push(file);
                    });
                };
 
                self.upload = function () {
                    $.each(self.files(), function (i, file) {
                        var reader = new FileReader();
                        reader.onload = function (e) {
                            var data = e.target.result;
                            //https://gist.github.com/HenrikJoreteg/2502497
                            //以XHR上傳原始格式
                            $.ajax({
                                type: "POST",
                                url: "@Url.Content("~/xhr2/upload")" + "?file=" + file.name,
                                contentType: "application/octect-stream",
                                processData: false, //不做任何處理,只上傳原始資料
                                data: data,
                                xhr: function () {
                                    //建立XHR時,加掛onprogress事件
                                    var xhr = $.ajaxSettings.xhr();
                                    xhr.upload.onprogress = function (evt) {
                                        file.uploadedBytes(evt.loaded);
                                    };
                                    return xhr;
                                }
                            });
                        };
                        reader.readAsArrayBuffer(file);
                    });
                };
            }
            var vm = new viewModel();
            ko.applyBindings(vm);
 
        });
    </script>
</body>
</html>

簡要說明程式重點:

  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技巧。將<div class='color-bar'>設成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的作業,這類操作中資料傳輸時間很短,大部分時間花在逐筆寫入資料庫,上傳進度條概念可否進一步改成反應伺服器端的處理進度呢? 很有趣的題目,若做得出來手邊有一票模組可以套用,但該怎麼玩呢? 下回待續。

相关 [html5] 推荐:

HTML5 logo 发布

- Greyby - 酷壳 - CoolShell.cn
2011年1月19日,W3C发布了HTML5的log,打开W3C的页面,下在的图片印入眼前. 我的第一感觉,就像是看到了小时候看的八一电影制片产的电影. 这分明是号召全世界的无产Web程序员们团结起来,不畏艰难,不怕牺牲,一定要把HTML5的革命事业进行到底. 所以,请各位Web程序员不但在你们的HTML5的网页上加上下面的徽章(关于各个徽章的含义,请参看这里).

html5 canvas入门

- - Marshal's Blog
可以把canvas看做div,不过,它的长和宽不能通过css来定义,要使用标签属性:. 或者javascript对象属性设置:. 使用canvas,只有一种操作方式,使用javascript. 获得canvas对象的上下文对象,该对象是操作canvas的主要对象:. 使用canvas画最简单的线, 点击运行示例,结果看起来是这样:.

HTML5新特性

- - CSDN博客推荐文章
 通过fillStyle和strokeStyle 属性可以轻松的设置矩形的填充和线条. 颜色值使用方法和CSS 一样:十六进制数、rgb()、rgba() 和 hsla. 通过 fillRect可以绘制带填充的矩形. 使用 strokeRect 可以绘制只有边框没有填充的矩形. 如果想清除部分 canvas可以使用clearRect.

【转载】HTML5 Messaging

- - HTML5研究小组
HTML5 的Message API能够让HTML5页面之间传递消息,甚至这些页面可以不在同一样域名下. 为了让消息能从一个页面发送到另一个页面,主动发送消息的页面必须拥有另一个页面的窗口引用. 然后发送 页面针对接受页调用 postMessage() 方法. postMessage() 方法中 origin 参数的值必须与页面所在的iframe的域名相匹配.

Adobe、标准和HTML5 -HTML5 and CSS3 开发

- - HTML5研究小组
“[提供商之间的]最激烈的竞争将与 标准密切相关. 大部分聪明人的眼睛将紧盯着技术标准. 但在计算机行业,新标准既可能是无限财富的源泉,也可能导致企业帝国的毁灭. 尽管存在着如此多的风险,标准仍然点燃了无限激情”. —The Economist, 1993年2月23日. 在编写这段有关标准化的话时,计算领域的主要争议是Unix是否是一个可行的操作系统(以及是否为IBM、DEC和HP的更多专用操作系统带来了挑战),以及哪个视窗平台(SUN/AT&T还是IBM/DEC/HP)将成为标准.

HTML5漫谈(4)–HTML5应用平台:PhoneGAP

- - HTML5研究小组
(  程宝平 chengbp @gmail.com). http://phonegap.com)按官方说法,是HTML5移动应用平台,它包括两部分:. 1)       应用开发框架:采用Web/HTML5技术编写应用,支持设备能力(如GPS、重力感应等)调用;支持能力插件灵活扩展. 图1 PhoneGAP支持设备能力API列表.

HTML5新特性之CSS+HTML5实例

- - CSDN博客Web前端推荐文章
1、新的DOCTYPE和字符集. HTML5的一项准则就是化繁为简,Web页面的DOCTYPE被极大的简化. HTML5引入了很多新的标签,根据内容和类型的不同,被分为7大类. 语义化标签可以简化HTML页面设计,并且将来搜索引擎在抓取和索引网页的时候,也会利用这些元素的优势. HTML5的宗旨之一就是存在即合理.

HTML5设计原理

- jessie - 蓝色理想
Jeremy Keith在 Fronteers 2010 上的主题演讲 下载PPT(PDF) 观看视频 今天我想跟大家谈一谈HTML5的设计. 主要分两个方面:一方面,当然了,就是HTML5. 我可以站在这儿只讲HTML5,但我并不打算这样做,因为如果你想了解HTML5的话,你可以Google,可以看书,甚至可以看规范.

HTML5的SEO探索

- Amo - HTML5研究小组
所有现代浏览器对HTML5的支持问题不大. HTML5被智能手机浏览器和越来越多的网站广泛的采用,甚至作为最优的选择. 但是,Googlebot,Bidubot等其他搜索引擎呢. 引擎是否会由于HTML5这任何额外因素,在搜索结果中优先推荐您的网站吗. 另一方面,少数搜索引擎会认为所有这些额外的H1标记的是垃圾网站吗.

文章: HTML5之美

- - InfoQ cn
如今大热的HTML5到底美在哪里. HTML5到底能为实际的移动开发带来哪些改变. 来自阿里云云手机服务运营部的前端开发工程师 正邪 (廖健)分享了他眼中的HTML5之美,主要讲诉HTML5的常见原理并从CSS、JavaScript和框架三个方面做了细致讲解:. 白伟民:酷狗音乐的HTML5实践(百度开发者大会广州站 5月31日 免费报名).