一、概述
以往對於基於浏覽器的應用而言,訪問本地文件都是一件頭疼的事情。雖然伴隨著 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 由一組 JavaScript 對象以及事件構成。賦予開發人員操作在 <input type=”file” … /> 文件選擇控件中選定文件的能力。圖 1 展示了 File API 所有的 JavaScript 的組合關系。
類型 FileList 包含一組 File 對象。通常 FileList 對象可以從表單中的文件域(<input type=”file” .../>)中拿取。Blob 對象代表浏覽器所能讀取的一組原始二進制流。Blob 對象中,屬性 size 表示流的大小。函數 slice() 可以將一個長的 Blob 對象分割成小塊。File 對象繼承自 Blob 對象,在 Blob 對象基礎上增加了和 File 相關的屬性。其中,屬性 name 表示文件的名字,這個名字去掉了文件的路徑信息,而只保留了文件名。屬性 type 表示文件的 MIME 類型。屬性 urn 則代表這個文件的 URN 信息。為完成文件讀取的操作,一個 FileReader 對象實例會關聯 File 或 Blob 對象,並提供三種不同的文件讀取函數以及 6 種事件。
文件讀取函數具體內容:
readAsBinaryString() 讀取文件內容,讀取結果為一個 binary string。文件每一個 byte 會被表示為一個 [0..255] 區間內的整數。函數接受一個 File 對象作為參數。
readAsText() 讀取文件內容,讀取結果為一串代表文件內容的文本。函數接受一個 File 對象以及文本編碼名稱作為參數。
readAsDataURL 讀取文件內容,讀取結果為一個 data: 的 URL。DataURL 由 RFC2397 定義。
文件讀取事件具體內容:
事件名稱 事件說明
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 展示了運行中的演示截圖。
我們逐步展示其中的代碼。清單 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 和拖拽提供了支持。下面我們先簡要介紹一下拖拽的使用,然後用一個例子來說明如何通過拖拽上傳文件。
1、拖拽簡介
拖拽一般涉及兩個對象:拖拽源和拖拽目標。
拖拽源:在 HTML5 草案裡如果一個對象可以作為源被拖拽,需要設置 draggable 屬性為 true 來標識該對象可作為拖拽源。然後偵聽源對象的 dragstart 事件,在事件處理函數裡設置好 DataTransfer。在 DataTransfer 裡可以設置拖拽數據的類型和值。比如是純文本的值,可以設置類型為"text/plain",url 則把類型設置為"text/uri-list"。 這樣,目標對象就可以根據期望的類型來選擇數據了。
拖拽目標:一個拖拽目標必須偵聽 3 個事件。
dragenter:目標對象通過響應這個事件來確定是否接收拖拽。如果接收則需要取消這個事件,停止時間的繼續傳播。
dragover:通過響應這個事件來顯示拖拽的提示效果。
drop:目標對象通過響應這個事件來處理拖拽數據。在下面的例子裡我們將在 drop 事件的處理函數裡獲取 DataTransfer 對象,取出要上傳的文件。
由於本文主要介紹 File API,對這部分不作詳細解釋,感興趣的讀者可以參考 HTML5 草案(見參考資料)。
2、拖拽上傳文件實例
下面以一個較為具體的例子說明如何結合拖拽和 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>
拖拽目標創建好之後,我們需要偵聽其對應的事件 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); }
本文通過對 File API 規范的講解,以及兩個展示其使用方法的例子,為大家提前揭示了作為未來 HTML5 重要組成部分的 JavaScript File API 的全貌。利用它,結合其他 HTML5 的新特性,比如 Drag&Drop,我們可以利用純 JavaScript 方案,為用戶提供更好使用體驗的 Web 應用,與此同時,這樣的一致化方案也使我們避免了以往跨浏覽器支持所花費的巨大代價。相信 File API 的出現和廣泛應用,將會是未來的 Web 2.0 應用的大勢所趨。