1. 程式人生 > >HTML5使用 JavaScript File API 實現檔案上傳

HTML5使用 JavaScript File API 實現檔案上傳

檔案來源:http://www.ibm.com/developerworks/cn/web/1101_hanbf_fileupload/

簡介: File API 是 Mozilla 向 W3C 提出的一個草案,旨在用標準 JavaScript API 實現本地檔案的讀取。File API 將極大地方便 Web 端的檔案上傳等操作,並有望成為未來的 HTML 5 規範的一部分。本文將介紹 File API 的概況,並用兩個例項展示 File API 的應用。

概述

以往對於基於瀏覽器的應用而言,訪問本地檔案都是一件頭疼的事情。雖然伴隨著 Web 2.0 應用技術的不斷髮展,JavaScript 正在扮演越來越重要的角色,但是出於安全性的考慮,JavaScript 一直是無法訪問本地檔案的。於是,為了在瀏覽器中能夠實現諸如拖拽並上傳本地檔案這樣的功能,我們就不得不求助於特定瀏覽器所提供的各種技術了。比如對於 IE,我們需要通過 ActiveX 控制元件來獲取對本地檔案的訪問能力,而對於 Firefox,同樣也要藉助外掛開發。由於不同瀏覽器的技術實現不盡相同,為了讓程式能夠支援多瀏覽器,我們的程式就會變得十分複雜而難於維護。不過現在,這一切都因為 File API 的出現而得到了徹底的改變。

File API 是 Mozilla 向 W3C 提交的一個草案,旨在推出一套標準的 JavaScript API,其基本功能是實現用 JavaScript 對本地檔案進行操作。出於安全性的考慮,該 API 只對本地檔案提供有限的訪問。有了它,我們就可以很輕鬆的用純 JavaScript 來實現本地檔案的讀取和上傳了。目前,FireFox 3.6 是最先支援這一功能的瀏覽器。除此以外,最新版本的 Google Chrome 瀏覽器和 Safari 瀏覽器也有相應的支援。File API 有望成為 W3C 目前正在制定的未來 HTML 5 規範當中的一部分。

本文後續章節將對 File API 做一個基本的介紹。併為讀者演示:如何利用 File API 實現基於 file input 控制元件的本地檔案讀取與上傳;以及利用拖拽實現從使用者系統批量上傳檔案的功能。通過本文,讀者可以瞭解最新的 File API 的概況以及使用,從而為構建具有更好使用者體驗的 Web 2.0 應用提供參考。

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 的基本用法。這個示例包含兩個程式碼檔案,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.onloadstart = function() { 
 // 這個事件在讀取開始時觸發
 console.log("onloadstart"); 
 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.onloadend = 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(); 

在 onloadstart事件觸發時,填充頁面上表示讀取資料總量的 <span> 元素。參見清單 4


清單 4 onloadstart 事件
				
 reader.onloadstart = function() 
 { 
 console.log("onloadstart"); 
 document.getElementById("bytesTotal").textContent = file.size; 
 } 

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


清單 5 onprogress 事件
				
 reader.onprogress = function(p) { 
 console.log("onloadstart"); 
 document.getElementById("bytesRead").textContent = p.loaded; 
 } 

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


清單 6 onloadend 事件
				
 reader.onloadend = 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 的規範,我們也可以將事件 onloadend 的處理拆分為事件 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 事件裡,我們可以通過 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 展示了上傳檔案的效果圖。

 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 應用的大勢所趨。


下載

描述名字大小下載方法
示例程式碼sourcecode.zip3KBHTTP

參考資料

學習

討論

作者簡介

韓冰峰,現在 IBM 中國軟體開發實驗室 Lotus 開發中心工作,目前從事 Lotus Connector 的開發。熱衷於 Web 2.0 相關技術的研究和實現。

張順,現在 IBM 中國軟體開發實驗室 Lotus 開發中心工作,目前從事 Lotus Quickr 的開發定製以及客戶支援工作。對 Web 服務,Web2.0 相關技術有濃厚的興趣。

莫映,現在IBM中國軟體開發實驗室Lotus開發中心工作,目前從事Lotus Quickr的Platform Service開發。熱衷於Web 2.0相關技術及智慧Web 2.0應用的構建。