使用 JavaScript File API 實現文件上傳

 File API 概覽

File API 由一組 JavaScript 對象以及事件構成。賦予開發人員操作在 <input type=”file” … /> 文件選擇控件中選定文件的能力。圖 1 展示了 File API 所有的 JavaScript 的組合關係。


圖 1. File API 相關類圖
圖 1. File API 相關類圖 

類型 FileList 包含一組 File 對象。通常 FileList 對象可以從表單中的文件域(<input type=”file” .../>)中拿取。Blob 對象代表瀏覽器所能讀取的一組原始二進制流。Blob 對象中,屬性 size 表示流的大小。函數 slice() 可以將一個長的 Blob 對象分割成小塊。File 對象繼承自 Blob 對象,在 Blob 對象基礎上增加了和 File 相關的屬性。其中,屬性 name 表示文件的名字,這個名字去掉了文件的路徑信息,而只保留了文件名。屬性 type 表示文件的 MIME 類型。屬性 urn 則代表這個文件的 URN 信息。爲完成文件讀取的操作,一個 FileReader 對象實例會關聯 File 或 Blob 對象,並提供三種不同的文件讀取函數以及 6 種事件。參見表 1 及表 2。


表 1. 文件讀取函數

函數名稱 功能
readAsBinaryString() 讀取文件內容,讀取結果爲一個 binary string。文件每一個 byte 會被表示爲一個 [0..255] 區間內的整數。函數接受一個 File 對象作爲參數。
readAsText() 讀取文件內容,讀取結果爲一串代表文件內容的文本。函數接受一個 File 對象以及文本編碼名稱作爲參數。
readAsDataURL 讀取文件內容,讀取結果爲一個 data: 的 URL。DataURL 由 RFC2397 定義,具體可以參考 http://www.ietf.org/rfc/rfc2397.txt。



表 2. 文件讀取事件

事件名稱 事件說明
Onloadstart 文件讀取開始時觸發。
Progress 當讀取進行中時定時觸發。事件參數中會含有已讀取總數據量。
Abort 當讀取被中止時觸發。
Error 當讀取出錯時觸發。
Load 當讀取成功完成時觸發。
Loadend 當讀取完成時,無論成功或者失敗都會觸發。

 

 

File API 簡單示例

接下來我們用一個簡單的例子展示 File API 的基本用法。這個示例包含兩個代碼文件,index.html 包含 Web 端的 HTML 代碼和處理上傳的 JavaScript 代碼;upload.jsp 包含服務器端接收上傳文件的代碼。請參見附件中的 sourcecode.zip。在這個例子中,我們將顯示一個傳統的帶有 File 選擇域的表單。當用戶選擇文件,點擊提交後,我們使用 File API 讀取文件內容,並通過 XMLHttpRequest 對象,用 Ajax 的方式將文件上傳到服務器上。圖 2 展示了運行中的演示截圖。


圖 2 File API 演示
圖 2 File API 演示 

我們逐步展示其中的代碼。清單 1 給出了代碼的 HTML 部分。


清單 1 示例代碼的 HTML 部分

				
 <body> 
 <h1>File API Demo</h1> 
 <p> 
 <!-- 用於文件上傳的表單元素 --> 
 <form name="demoForm" id="demoForm" method="post" enctype="multipart/form-data" 
 action="javascript: uploadAndSubmit();"> 
 <p>Upload File: <input type="file" name="file" /></p> 
 <p><input type="submit" value="Submit" /></p> 
 </form> 
 <div>Progessing (in Bytes): <span id="bytesRead"> 
 </span> / <span id="bytesTotal"></span> 
 </div> 
 </p> 
 </body> 

 

可以看到,我們用普通的 <form> 標籤來包含一個傳統的 <input type=”file” … /> 元素。在 <form> 中還有一個 submit 元素。在 <form> 之外有一些 <span> 元素用來表示已讀取和總共的數據量。<form> 的 action 屬性指向了一個 JavaScript 函數 uploadAndSubmit()。這個函數完成了讀取文件並上傳的過程。函數代碼見清單 2。


清單 2 讀取文件並上傳的 JavaScript 函數

				
 function uploadAndSubmit() { 
 var form = document.forms["demoForm"]; 
    
 if (form["file"].files.length > 0) { 
 // 尋找表單域中的 <input type="file" ... /> 標籤
 var file = form["file"].files[0]; 
 // try sending 
 var reader = new FileReader(); 

 reader. = function() { 
 // 這個事件在讀取開始時觸發
 console.log(" 
 document.getElementById("bytesTotal").textContent = file.size; 
 } 
 reader.onprogress = function(p) { 
 // 這個事件在讀取進行中定時觸發
 console.log("onprogress"); 
 document.getElementById("bytesRead").textContent = p.loaded; 
 } 

 reader.onload = function() { 
    // 這個事件在讀取成功結束後觸發
 console.log("load complete"); 
 } 

 reader. = function() { 
    // 這個事件在讀取結束後,無論成功或者失敗都會觸發
 if (reader.error) { 
 console.log(reader.error); 
 } else { 
 document.getElementById("bytesRead").textContent = file.size; 
 // 構造 XMLHttpRequest 對象,發送文件 Binary 數據
 var xhr = new XMLHttpRequest(); 
 xhr.open(/* method */ "POST", 
 /* target url */ "upload.jsp?fileName=" + file.name 
 /*, async, default to true */); 
 xhr.overrideMimeType("application/octet-stream"); 
 xhr.sendAsBinary(reader.result); 
 xhr.onreadystatechange = function() { 
 if (xhr.readyState == 4) { 
 if (xhr.status == 200) { 
 console.log("upload complete"); 
 console.log("response: " + xhr.responseText); 
 } 
 } 
 } 
 } 
 } 

 reader.readAsBinaryString(file); 
 } else { 
 alert ("Please choose a file."); 
 } 
 } 

 

在這個函數中,首先我們找到含有 <input type=”file” … /> 元素的 <form>,並找到含有上傳文件信息的 <input> 元素。如 <input> 元素中不含有文件,說明用戶沒有選擇任何文件,此時將報錯。


清單 3 找到 <input> 元素

				
 var form = document.forms["demoForm"]; 

 if (form["file"].files.length > 0) 
 { 
 var file = form["file"].files[0]; 
… …
 } 
 else 
 { 
 alert ("Please choose a file."); 
 } 

 

這裏,從 form[“file”].files 返回的對象類型即爲提到的 FileList。我們從中拿取第一個元素。之後,我們構建 FileReader 對象:

 var reader = new FileReader(); 

 

在 <span> 元素。參見清單 4


清單 4 事件

				
 reader. = function() 
 { 
 console.log(" 
 document.getElementById("bytesTotal").textContent = file.size; 
 } 

 

在 onprogress 事件觸發時,更新頁面上已讀取數據量的 <span> 元素。參見清單 5


清單 5 onprogress 事件

				
 reader.onprogress = function(p) { 
 console.log(" 
 document.getElementById("bytesRead").textContent = p.loaded; 
 } 

 

最後的 事件中,如果沒有錯誤,我們將讀取文件內容,並通過 XMLHttpRequest 的方式上傳。


清單 6 事件

				
 reader. = function() 
 { 
 if (reader.error) 
 { 
 console.log(reader.error); 
 } 
 else 
 { 
 // 構造 XMLHttpRequest 對象,發送文件 Binary 數據
 var xhr = new XMLHttpRequest(); 
 xhr.open(/* method */ "POST", 
 /* target url */ "upload.jsp?fileName=" + file.name 
 /*, async, default to true */); 
 xhr.overrideMimeType("application/octet-stream"); 
 xhr.sendAsBinary(reader.result); 
… …
 } 
 } 

 

按照 File API 的規範,我們也可以將事件 的處理拆分爲事件 error 以及事件 load 的處理。

在這個示例中,我們後臺使用一個 JSP 來處理上傳。JSP 代碼如清單 7。


清單 7 處理上傳的 JSP 代碼

				
 <%@ page import="java.io.*" %><% 
   BufferedInputStream fileIn = new 
 BufferedInputStream(request.getInputStream()); 
   String fn = request.getParameter("fileName"); 
   
   byte[] buf = new byte[1024]; 

 

//接收文件上傳並保存到 d:\

   File file = new File("d:/" + fn); 
   
   BufferedOutputStream fileOut = new BufferedOutputStream(new 
 FileOutputStream(file)); 
   
   while (true) { 
       // 讀取數據
      int bytesIn = fileIn.read(buf, 0, 1024); 
      
      System.out.println(bytesIn); 
      
      if (bytesIn == -1) 
 { 
         break; 
      } 
 else 
 { 
         fileOut.write(buf, 0, bytesIn); 
      } 
   } 
   
   fileOut.flush(); 
   fileOut.close(); 
   
   out.print(file.getAbsolutePath()); 
 %> 

 

在這段 JSP 代碼中,我們從 POST 請求中接受文件名字以及二進制數據。將二進制數據寫入到服務器的“D:\”路徑中。並返回文件的完整路徑。以上代碼可以在最新的 Firefox 3.6 中調試通過。

 

使用拖拽上傳文件

前面我們介紹了怎樣通過 HTML5 的 File API 來讀取本地文件內容並上傳到服務器,通過這種方式已經能夠滿足大部分用戶的需求了。其中一個不足是用戶只能通過點擊“瀏覽”按鈕來逐個添加文件,如果需要批量上傳文件,會導致用戶體驗不是很友好。而在桌面應用中,用戶一般可以通過鼠標拖拽的方式方便地上傳文件。拖拽一直是 Web 應用的一個軟肋,一般瀏覽器都不提供對拖拽的支持。雖然 Web 程序員可以通過鼠標的 mouseenter,mouseover 和 mouseout 等事件來實現拖拽效果,但是這種方式也只能使拖拽的範圍侷限在瀏覽器裏面。一個好消息是 HTML5 裏面不僅加入了 File API,而且加入了對拖拽的支持,Firefox 3.5 開始已經對 File API 和拖拽提供了支持。下面我們先簡要介紹一下拖拽的使用,然後用一個例子來說明如何通過拖拽上傳文件。

拖拽簡介

拖拽一般涉及兩個對象:拖拽源和拖拽目標。

拖拽源:在 HTML5 草案裏如果一個對象可以作爲源被拖拽,需要設置 draggable 屬性爲 true 來標識該對象可作爲拖拽源。然後偵聽源對象的 dragstart 事件,在事件處理函數裏設置好 DataTransfer。在 DataTransfer 裏可以設置拖拽數據的類型和值。比如是純文本的值,可以設置類型爲"text/plain",url 則把類型設置爲"text/uri-list"。 這樣,目標對象就可以根據期望的類型來選擇數據了。

拖拽目標:一個拖拽目標必須偵聽 3 個事件。

dragenter:目標對象通過響應這個事件來確定是否接收拖拽。如果接收則需要取消這個事件,停止時間的繼續傳播。

dragover:通過響應這個事件來顯示拖拽的提示效果。

drop:目標對象通過響應這個事件來處理拖拽數據。在下面的例子裏我們將在 drop 事件的處理函數裏獲取 DataTransfer 對象,取出要上傳的文件。

由於本文主要介紹 File API,對這部分不作詳細解釋,感興趣的讀者可以參考 HTML5 草案(見參考資料)。

拖拽上傳文件實例

下面以一個較爲具體的例子說明如何結合拖拽和 File API 來上傳文檔。由於直接和桌面交互,所以我們不需要處理拖拽源,直接在目標對象裏從 DataTransfer 對象獲取數據即可。

首先,我們需要創建一個目標容器用來接收拖拽事件,添加一個 div 元素即可。然後用一個列表來展示上傳文件的縮略圖,進度條及文件名。參見清單 8 的 HTML 代碼和圖 3 的效果圖。詳細代碼請參見附件中的 dnd.html 文件。


清單 8 拖曳目標的 HTML 代碼

				
 <div id="container"> 
 <span>Drag and drop files here to upload.</span> 
 <ul id="fileList"></ul> 
 </div> 



圖 3 拖曳目標
圖 3 拖曳目標 

拖拽目標創建好之後,我們需要偵聽其對應的事件 dragenter,dragover 和 drop。在 dragenter 事件處理函數裏,我們只是簡單地清除文件列表,然後取消 dragenter 事件的傳播,表示我們接收該事件。更加妥當的作法是判斷 DataTransfer 裏的數據是否爲文件,這裏我們假設所有拖拽源都是文件。dragover 事件裏我們取消該事件,使用默認的拖拽顯示效果。在 drop 事件裏我們註冊了 handleDrop 事件處理函數來獲取文件信息並上傳文件。清單 9 展示了這些事件處理函數。


清單 9 設置事件處理函數

				
 function addDNDListeners() 
 { 
 var container = document.getElementById("container"); 
 var fileList = document.getElementById("fileList"); 
 // 拖拽進入目標對象時觸發
 container.addEventListener("dragenter", function(event) 
 { 
 fileList.innerHTML =''; 
 event.stopPropagation(); 
 event.preventDefault(); 
 }, false); 
 // 拖拽在目標對象上時觸發
 container.addEventListener("dragover", function(event) 
 { 
 event.stopPropagation(); 
 event.preventDefault(); 
 }, false); 
 // 拖拽結束時觸發
 container.addEventListener("drop", handleDrop, false); 
 } 
 window.addEventListener("load", addDNDListeners, false); 

 

處理 drop 事件

用戶在拖拽結束時鬆開鼠標觸發 drop 事件。在 drop 事件裏,我們可以通過 event 參數的 DataTransfer 對象獲取 files 數據,通過遍歷 files 數組可以獲取每個文件的信息。然後針對每個文件,創建 HTML 元素來顯示縮略圖,進度條和文件名稱。File 對象的 getAsDataURL 可以將文件內容以 URL 的形式返回,對圖片文件來說可以用來顯示縮略圖。要注意的一點是,在 drop 事件處理函數裏要取消事件的繼續傳播和默認處理函數,結束 drop 事件的處理。清單 10 展示了 drop 事件的處理代碼。


清單 10 drop 事件的處理

				
 function handleDrop(event) 
 { 
    // 獲取拖拽的文件列表
 var files = event.dataTransfer.files; 
 event.stopPropagation(); 
 event.preventDefault(); 
 var fileList = document.getElementById("fileList"); 
 // 展示文件縮略圖,文件名和上傳進度,上傳文件
 for (var i = 0; i < files.length; i++) 
 { 
 var file = files[i]; 
 var li = document.createElement('li'); 
 var progressbar = document.createElement('div'); 
 var img = document.createElement('img'); 
 var name = document.createElement('span'); 
 progressbar.className = "progressBar"; 
 img.src = files[i].getAsDataURL(); 
 img.width = 32; 
 img.height = 32; 
 name.innerHTML = file.name; 
 li.appendChild(img); 
 li.appendChild(name); 
 li.appendChild(progressbar); 
 fileList.appendChild(li); 
 uploadFile(file, progressbar) 
 } 
 } 

 

上傳文件

我們可以通過 XMLHttpRequest 對象的 sendAsBinary 方法來上傳文件,通過偵聽 upload 的 progress,load 和 error 事件來監測文件上傳的進度,成功完成或是否發生錯誤。在 progress 事件處理函數裏我們通過計算已經上傳的比例來確定進度條的位置。參見清單 11。圖 4 展示了上傳文件的效果圖。

清單 11 上傳文件

 function uploadFile(file, progressbar) 
 { 
 var xhr = new XMLHttpRequest(); 
 var upload = xhr.upload; 

 var p = document.createElement('p'); 
 p.textContent = "0%"; 
 progressbar.appendChild(p); 
 upload.progressbar = progressbar; 
 // 設置上傳文件相關的事件處理函數
 upload.addEventListener("progress", uploadProgress, false); 
 upload.addEventListener("load", uploadSucceed, false); 
 upload.addEventListener("error", uploadError, false); 
 // 上傳文件
 xhr.open("POST", "upload.jsp?fileName="+file.name); 
 xhr.overrideMimeType("application/octet-stream"); 
 xhr.sendAsBinary(file.getAsBinary()); 
 } 
 function uploadProgress(event) 
 { 
 if (event.lengthComputable) 
 { 
    // 將進度換算成百分比
 var percentage = Math.round((event.loaded * 100) / event.total); 
 console.log("percentage:" + percentage); 
 if (percentage < 100) 
 { 
 event.target.progressbar.firstChild.style.width = (percentage*2) + "px"; 
 event.target.progressbar.firstChild.textContent = percentage + "%"; 
 } 
 } 
 } 
 function uploadSucceed(event) 
 { 
 event.target.progressbar.firstChild.style.width = "200px"; 
 event.target.progressbar.firstChild.textContent = "100%"; 
 } 
 function uploadError(error) 
 { 
 alert("error: " + error); 
 } 



圖 4 上傳文件的效果圖
圖 4 上傳文件的效果圖 

小結

本文通過對 File API 規範的講解,以及兩個展示其使用方法的例子,爲大家提前揭示了作爲未來 HTML5 重要組成部分的 JavaScript File API 的全貌。利用它,結合其他 HTML5 的新特性,比如 Drag&Drop,我們可以利用純 JavaScript 方案,爲用戶提供更好使用體驗的 Web 應用,與此同時,這樣的一致化方案也使我們避免了以往跨瀏覽器支持所花費的巨大代價。相信 File API 的出現和廣泛應用,將會是未來的 Web 2.0 應用的大勢所趨。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章