在傳統網頁上傳大檔案,得等到全部傳完才會有回應,等待期間沒消沒息,搞不清楚是沒傳完還是當掉常為人詬病,也嚴重破壞使用者體驗。想在傳輸過程回報上傳進度,過去有些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
.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; }
data-bind="event: { change: selectorChange }" />
$(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);
});
簡要說明程式重點: