1. 程式人生 > >HTML5利用FormData物件實現顯示進度條的檔案上傳【譯】

HTML5利用FormData物件實現顯示進度條的檔案上傳【譯】

這篇文章是我翻譯的外文,非本人原創
網上看到很多部落格都有轉載這篇文章
不過轉載的都是英文
所以我就決定翻譯一下
(翻譯和格式化也耗費了一番心血 (~﹃~)~zZ)
英文能力有限,大家湊合看吧(翻譯有略微改動)
(ps:關於下面版本相容的問題請無視,畢竟這篇文章也算有一段時間了)
譯文原址傳送門


HTML5終於解決了在上傳檔案的同時也能顯示上傳進度的問題
如今,大多網站都使用Flash播放器來實現這一功能
一些網站仍然使用html的form標籤
(enctype=multipart/form-data)
但是它需要伺服器端改動來使使用者顯示上傳進度

實際上,我們需要做的是掛接到伺服器的位元組流
伺服器正在接收一個檔案
所以我們可以知道有多少已接收位元組
並以某種方式傳達資訊返回給客戶端瀏覽器
而它仍然是正在上傳的檔案
這種解決方案非常好(尤其對於大檔案)
然而,它相當複雜
因為你基本上是接管整個伺服器端處理(當你挖掘到位元組流)

HTML5上傳檔案

XMLHttpRequest物件已經得到HTML5規範的擴充套件
特別是XMLHttpRequest的2級標準(目前最新版本)
已經包括了以下新特點:

  1. 處理位元組流(如File、Blob和FormData物件在上傳/下載過程中)
  2. 上傳/下載過程中的進度事件
  3. 跨域請求
  4. 匿名請求(不傳送HTTP請求)
  5. 超時請求

在這篇文章中,我們主要用到前兩條
特別是上傳檔案中要使用XMLHttpRequest並且向用戶提供上傳進度的資訊
注意,這種解決辦法不需要伺服器端做出任何變動
頂多也就是是處理multipart / form-data協議這種程度
所以,現有的伺服器端邏輯應保持不變
這就使得這種技術更容易適應

Html5FileUpload1

【圖1:檔案上傳(上傳剛開始)】

Html5FileUpload2

【圖2:檔案上傳(上傳已完成)】

上面的圖片提供給使用者以下資訊:

  • 檔案資訊
    • 檔名
    • 檔案大小
    • MIME型別
  • 進度條(已完成百分比)
  • 上傳速度/上傳頻寬
  • 剩餘估計時間
  • 已上傳位元組
  • 伺服器端響應(橘色框內)

最後一條看起來不重要,但其實這種環境下十分重要
請記住,使用者通常不會提交HTML表單後(ps:這裡我覺得應該是檔案上傳完畢)就馬上從該頁面離開
因為我們使用的XMLHttpRequest上傳發生在後臺,網頁仍舊沒有發生變化
如果你的業務流程可以用到它
那將會帶來很好的使用者體驗

HTML5進度事件

按照HTML5的進度事件規範
HTML5的進度事件提供以下相關資訊

  • total - 被轉移總位元組數
  • loaded - 已上傳位元組
  • lengthComputable - 指定上傳總資料大小(上傳檔案總大小已知)

你應該注意到,我們需要使用這兩條資訊來計算出所有我們要顯示給使用者的其他資訊
這是十分簡單的計算
但它確實涉及很多額外程式碼,並且需要建立定時器

考慮到使用者應該也可以瞭解進度資訊
HTML5的進度事件規範也注意到了這一需求
同時對於瀏覽器廠商來說,在每次進度增加一點後提供額外資訊十分簡單
所以我建議進度事件應修改如下:

  • total - 被轉移總位元組數
  • loaded - 已上傳位元組
  • lengthComputable - 指定上傳總資料大小(上傳檔案總大小已知)
  • transferSpeed - 傳輸速度
  • timeRemaining - 剩餘時間

功能實現

首先是一個非常簡單的HTML表單

<!DOCTYPE html>
<html>
<head>
    <title>Upload Files using XMLHttpRequest - Minimal</title>
</head>
<body>
  <form id="form1" enctype="multipart/form-data" method="post" action="Upload.aspx">
    <div class="row">
      <label for="fileToUpload">Select a File to Upload</label><br>
      <input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected();">
    </div>
    <div id="fileName"></div>
    <div id="fileSize"></div>
    <div id="fileType"></div>
    <div class="row">
      <input type="button" onclick="uploadFile()" value="Upload">
    </div>
    <div id="progressNumber"></div>
  </form>
</body>
</html>

【程式碼1:HTML】

注意,<input type="file">標籤具有onchange事件綁定了js函式fileSelected()
每次使用者通過瀏覽本地系統上的檔案就會觸發此事件
fileSelected()函式如下:

function fileSelected() {
  var file = document.getElementById('fileToUpload').files[0];
  if (file) {
    var fileSize = 0;
    if (file.size > 1024 * 1024){
      fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB';
    }else{
      fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB';
    }      
    document.getElementById('fileName').innerHTML = 'Name: ' + file.name;
    document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize;
    document.getElementById('fileType').innerHTML = 'Type: ' + file.type;
  }
}

【程式碼2:fileSelected()函式】

函式中的第一行做了一些你以前可能沒見過或沒做過的事情
實際上,一旦你有了一個<input type="file">元素的應用
你就可以訪問一個叫做FileList的物件,這是HTML5新規範的一部分
(HTML5 File API 的一部分)
FileList物件是檔案的集合
更具體地說,它是File物件的集合
File物件有以下屬性:

  • name - 檔名稱(包含任意路徑)
  • type - 檔案MIME型別(小寫)
  • size - 檔案位元組大小

這越來越有趣了,不是嗎?
我們可以在客戶端訪問這些資訊
事實上,File API(使用FileReader物件)同樣可以讓我們訪問的客戶端檔案內容(檔案流或位元組)
在這個例子中我們不會用FileReader來處理,所以我不打算用其他新物件來擊垮你的耐心
我們還有幾個新物件沒有討論

對你們來說,可以使用File物件提供的資訊
來阻止使用者上傳超過一定大小的檔案
或者你可以使用type屬性找出使用者試圖上傳和修改的檔案型別
以上的JavaScript方法填充於灰色文字(上面的圖1和圖2)
(使用File物件獲取選中檔案的資訊)

檔案的大小是位元組,所以有辦法將其轉換為我們更容易理解的形式
(例如使用15.65MB代替15795748864位元組)
使用者選擇檔案後,會通過點選上傳按鈕來上傳檔案
這裡還要注意上傳按鈕onclick事件綁定了js函式uploadFile()
uploadFile()函式如下:

function uploadFile() {
  var xhr = new XMLHttpRequest();
  var fd = document.getElementById('form1').getFormData();

  /* 事件監聽 */
  xhr.upload.addEventListener("progress", uploadProgress, false);
  xhr.addEventListener("load", uploadComplete, false);
  xhr.addEventListener("error", uploadFailed, false);
  xhr.addEventListener("abort", uploadCanceled, false);
  /* 下面的url一定要改成你要傳送檔案的伺服器url */
  xhr.open("POST", "UploadMinimal.aspx");
  xhr.send(fd);
}

【程式碼3:uploadFile()函式】

在這個函式的第二行,你將會看到另一個物件FormData
我們從來沒有見過,也沒有使用過它
其實你沒有真正地看到它,但我們通過一個方法getFormData()得到了它的一個引用
FormData物件類似於字典(資料結構)、名/值對的集合
名字的部分就是表單的名稱(HTML中定義)
值的部分就是是該欄位的值
值的部分可以是一個字串、數字、甚至是File物件(可以看程式碼4)

所以,當你在表單呼叫了getFormData()方法就能獲得FormData物件的引用(表單的鍵值對集合)
你可以使用這個引用作為你要傳送給伺服器端的資訊
這使得提交整個表單變得十分簡單
現在,你可以手動建立一個FormData例項物件傳送給伺服器
或者在傳送之前向FormData物件中新增額外資訊

關於FormData的注意事項
需要注意的是,寫這篇文章時Chrome6支援使用新的API上傳檔案
貌似不支援getFormDate()方法,但支援FormData物件
所以在程式碼6中手動建立了一個FormData例項
另一種例項化FormData的方法是在表單元素中傳遞引用
像這樣var fd = new FormData(document.getElementById('form1'));
這個建構函式的優點是可以用所有表單填充FormData例項(不必手動完成)
(類似於前面討論表單元素的getFormData方法,寫這篇文章時,Firefox支援這個方法)

程式碼4向你展示瞭如何建立一個FormData例項
分配任意欄位和值,包括File物件的引用(通過使用者<input type="file">選擇的檔案)

var fd = new FormData();
fd.append("author", "Shiv Kumar");
fd.append("name", "Html 5 File API/FormData");
fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]);

【程式碼4:手動建立一個FormData例項】

讓我們回到程式碼3,你會發現我們已經訂閱了XMLHttpRequest物件的一些事件
尤其要注意程式碼中“事件監聽”註釋後的第一行
xhr.upload.addEventListener("progress", uploadProgress, false);
我們訂閱的進度事件並不是XMLHttpRequest例項的,而是在XMLHttpRequest例項的upload屬性上
這是一個XMLHttpRequestUpload型別
(這個事件目標有一個progress事件,我們可以訂閱它來獲取當前發生的上傳資訊)
我們不必在這個物件上過於煩惱
只要記得,當使用XMLHttpRequest上傳資料到伺服器時
你必須訂閱它upload屬性的progress事件

有關progress事件的注意事項
XMLHttpRequest物件也有它自己的progress事件
當你從伺服器下載資料時可以訂閱該事件(我們這裡不會涉及)

function uploadProgress(evt) {
  if (evt.lengthComputable) {
    var percentComplete = Math.round(evt.loaded * 100 / evt.total);
    document.getElementById('progressNumber').innerHTML = percentComplete.toString() + '%';
  }
  else {
    document.getElementById('progressNumber').innerHTML = '無法計算';
  }
}

function uploadComplete(evt) {
  /* 當伺服器響應後,這個事件就會被觸發 */
  alert(evt.target.responseText);
}

function uploadFailed(evt) {
  alert("上傳檔案發生了錯誤嘗試");
}

function uploadCanceled(evt) {
  alert("上傳被使用者取消或者瀏覽器斷開連線");
}  

【程式碼5:各種事件處理函式的實現】

上面的程式碼已經寫得很清楚了,就不再贅述了

完整簡約的解決方案

下面的程式碼清單是包括能夠支援的最小檔案與進度指示器上傳所需的JavaScript和HTML整個頁面
我儘量保證它的簡潔
所以如果你想要使用自己的佈局和顯示資訊可以藉此擴充套件
HTML5還引入了progress標籤用於顯示進度
progress元素有max和value屬性,因此使用它可以更方便的顯示進度
但是,在寫這篇文章的時候,只有Chrome6支援這個元素
所以我在這個簡約的解決方案沒有使用它

更改伺服器端指令碼的URL
請務必將URL更改為指向你上傳檔案的伺服器端URL
在下面的程式碼清單中
UploadMinimal.aspx的uploadFile()方法:
xhr.open("POST", "UploadMinimal.aspx");

<!DOCTYPE html>
<html>
<head>
    <title>Upload Files using XMLHttpRequest - Minimal</title>

    <script type="text/javascript">
      function fileSelected() {
        var file = document.getElementById('fileToUpload').files[0];
        if (file) {
          var fileSize = 0;
          if (file.size > 1024 * 1024)
            fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB';
          else
            fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB';

          document.getElementById('fileName').innerHTML = 'Name: ' + file.name;
          document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize;
          document.getElementById('fileType').innerHTML = 'Type: ' + file.type;
        }
      }

      function uploadFile() {
        var fd = new FormData();
        fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]);
        var xhr = new XMLHttpRequest();
        xhr.upload.addEventListener("progress", uploadProgress, false);
        xhr.addEventListener("load", uploadComplete, false);
        xhr.addEventListener("error", uploadFailed, false);
        xhr.addEventListener("abort", uploadCanceled, false);
        xhr.open("POST", "UploadMinimal.aspx");
        xhr.send(fd);
      }

      function uploadProgress(evt) {
        if (evt.lengthComputable) {
          var percentComplete = Math.round(evt.loaded * 100 / evt.total);
          document.getElementById('progressNumber').innerHTML = percentComplete.toString() + '%';
        }
        else {
          document.getElementById('progressNumber').innerHTML = '無法計算';
        }
      }

      function uploadComplete(evt) {
        /* 當伺服器響應後,這個事件就會被觸發 */
        alert(evt.target.responseText);
      }

      function uploadFailed(evt) {
        alert("上傳檔案發生了錯誤嘗試");
      }

      function uploadCanceled(evt) {
        alert("上傳被使用者取消或者瀏覽器斷開連線");
      }
    </script>
</head>
<body>
  <form id="form1" enctype="multipart/form-data" method="post" action="Upload.aspx">
    <div class="row">
      <label for="fileToUpload">Select a File to Upload</label><br />
      <input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected();"/>
    </div>
    <div id="fileName"></div>
    <div id="fileSize"></div>
    <div id="fileType"></div>
    <div class="row">
      <input type="button" onclick="uploadFile()" value="Upload" />
    </div>
    <div id="progressNumber"></div>
  </form>
</body>
</html>

【程式碼6:完整簡約的程式碼清單】

嗯,這是幾乎涵蓋了所有新版HTML5功能的簡約版本
在圖片2中大家也看到了,獲取其他資訊需要數學知識
這是相當多的額外的工作,不僅僅獲得這些資訊需要用到數學,顯示和動畫等等也要用
例如獲取上傳的速率(上傳速度)
我做了以下幾點:

  1. 在uploadProgress(evt)事件中,儲存evt.loaded和evt.total作為全域性變數
  2. 建立了一個每秒觸發的計時器事件
  3. 在定時器的回撥中,獲取了傳輸位元組的差值(與1s之前的差)
  4. 每秒上傳位元組數得到上傳速度

在這裡,我還設定了定時器每500毫秒獲取從而更加精細
我就不再說剩餘時間怎樣求了
但是demo中都有(ps:應該是說他圖片裡的酷炫版本)
希望這篇文章對大家有所幫助

若翻譯有不好的地方,還請大家包涵指正