JavaScript File API實(shí)現(xiàn)文件上傳預(yù)覽
一、概述
以往對(duì)于基于瀏覽器的應(yīng)用而言,訪問(wèn)本地文件都是一件頭疼的事情。雖然伴隨著 Web 2.0 應(yīng)用技術(shù)的不斷發(fā)展,JavaScript 正在扮演越來(lái)越重要的角色,但是出于安全性的考慮,JavaScript 一直是無(wú)法訪問(wèn)本地文件的。于是,為了在瀏覽器中能夠?qū)崿F(xiàn)諸如拖拽并上傳本地文件這樣的功能,我們就不得不求助于特定瀏覽器所提供的各種技術(shù)了。比如對(duì)于 IE,我們需要通過(guò) ActiveX 控件來(lái)獲取對(duì)本地文件的訪問(wèn)能力,而對(duì)于 Firefox,同樣也要借助插件開發(fā)。由于不同瀏覽器的技術(shù)實(shí)現(xiàn)不盡相同,為了讓程序能夠支持多瀏覽器,我們的程序就會(huì)變得十分復(fù)雜而難于維護(hù)。不過(guò)現(xiàn)在,這一切都因?yàn)?File API 的出現(xiàn)而得到了徹底的改變。
File API 是 Mozilla 向 W3C 提交的一個(gè)草案,旨在推出一套標(biāo)準(zhǔn)的 JavaScript API,其基本功能是實(shí)現(xiàn)用 JavaScript 對(duì)本地文件進(jìn)行操作。出于安全性的考慮,該 API 只對(duì)本地文件提供有限的訪問(wèn)。有了它,我們就可以很輕松的用純 JavaScript 來(lái)實(shí)現(xiàn)本地文件的讀取和上傳了。目前,F(xiàn)ireFox 3.6 是最先支持這一功能的瀏覽器。除此以外,最新版本的 Google Chrome 瀏覽器和 Safari 瀏覽器也有相應(yīng)的支持。File API 有望成為 W3C 目前正在制定的未來(lái) HTML 5 規(guī)范當(dāng)中的一部分。
二、File API 概覽
File API 由一組 JavaScript 對(duì)象以及事件構(gòu)成。賦予開發(fā)人員操作在 <input type=”file” … /> 文件選擇控件中選定文件的能力。圖 1 展示了 File API 所有的 JavaScript 的組合關(guān)系。
類型 FileList 包含一組 File 對(duì)象。通常 FileList 對(duì)象可以從表單中的文件域(<input type=”file” .../>)中拿取。Blob 對(duì)象代表瀏覽器所能讀取的一組原始二進(jìn)制流。Blob 對(duì)象中,屬性 size 表示流的大小。函數(shù) slice() 可以將一個(gè)長(zhǎng)的 Blob 對(duì)象分割成小塊。File 對(duì)象繼承自 Blob 對(duì)象,在 Blob 對(duì)象基礎(chǔ)上增加了和 File 相關(guān)的屬性。其中,屬性 name 表示文件的名字,這個(gè)名字去掉了文件的路徑信息,而只保留了文件名。屬性 type 表示文件的 MIME 類型。屬性 urn 則代表這個(gè)文件的 URN 信息。為完成文件讀取的操作,一個(gè) FileReader 對(duì)象實(shí)例會(huì)關(guān)聯(lián) File 或 Blob 對(duì)象,并提供三種不同的文件讀取函數(shù)以及 6 種事件。
文件讀取函數(shù)具體內(nèi)容:
readAsBinaryString() 讀取文件內(nèi)容,讀取結(jié)果為一個(gè) binary string。文件每一個(gè) byte 會(huì)被表示為一個(gè) [0..255] 區(qū)間內(nèi)的整數(shù)。函數(shù)接受一個(gè) File 對(duì)象作為參數(shù)。
readAsText() 讀取文件內(nèi)容,讀取結(jié)果為一串代表文件內(nèi)容的文本。函數(shù)接受一個(gè) File 對(duì)象以及文本編碼名稱作為參數(shù)。
readAsDataURL 讀取文件內(nèi)容,讀取結(jié)果為一個(gè) data: 的 URL。DataURL 由 RFC2397 定義。
文件讀取事件具體內(nèi)容:
事件名稱 事件說(shuō)明
Onloadstart 文件讀取開始時(shí)觸發(fā)。
Progress 當(dāng)讀取進(jìn)行中時(shí)定時(shí)觸發(fā)。事件參數(shù)中會(huì)含有已讀取總數(shù)據(jù)量。
Abort 當(dāng)讀取被中止時(shí)觸發(fā)。
Error 當(dāng)讀取出錯(cuò)時(shí)觸發(fā)。
Load 當(dāng)讀取成功完成時(shí)觸發(fā)。
Loadend 當(dāng)讀取完成時(shí),無(wú)論成功或者失敗都會(huì)觸發(fā)。
三、File API 簡(jiǎn)單示例
接下來(lái)我們用一個(gè)簡(jiǎn)單的例子展示 File API 的基本用法。這個(gè)示例包含兩個(gè)代碼文件,index.html 包含 Web 端的 HTML 代碼和處理上傳的 JavaScript 代碼;upload.jsp 包含服務(wù)器端接收上傳文件的代碼。請(qǐng)參見附件中的 sourcecode.zip。在這個(gè)例子中,我們將顯示一個(gè)傳統(tǒng)的帶有 File 選擇域的表單。當(dāng)用戶選擇文件,點(diǎn)擊提交后,我們使用 File API 讀取文件內(nèi)容,并通過(guò) XMLHttpRequest 對(duì)象,用 Ajax 的方式將文件上傳到服務(wù)器上。圖 2 展示了運(yùn)行中的演示截圖。
我們逐步展示其中的代碼。清單 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> 標(biāo)簽來(lái)包含一個(gè)傳統(tǒng)的 <input type=”file” … /> 元素。在 <form> 中還有一個(gè) submit 元素。在 <form> 之外有一些 <span> 元素用來(lái)表示已讀取和總共的數(shù)據(jù)量。<form> 的 action 屬性指向了一個(gè) JavaScript 函數(shù) uploadAndSubmit()。這個(gè)函數(shù)完成了讀取文件并上傳的過(guò)程。函數(shù)代碼見清單 2。
清單 2 讀取文件并上傳的 JavaScript 函數(shù)
function uploadAndSubmit() { var form = document.forms["demoForm"]; if (form["file"].files.length > 0) { // 尋找表單域中的 <input type="file" ... /> 標(biāo)簽 var file = form["file"].files[0]; // try sending var reader = new FileReader(); reader.onloadstart = function() { // 這個(gè)事件在讀取開始時(shí)觸發(fā) console.log("onloadstart"); document.getElementById("bytesTotal").textContent = file.size; } reader.onprogress = function(p) { // 這個(gè)事件在讀取進(jìn)行中定時(shí)觸發(fā) console.log("onprogress"); document.getElementById("bytesRead").textContent = p.loaded; } reader.onload = function() { // 這個(gè)事件在讀取成功結(jié)束后觸發(fā) console.log("load complete"); } reader.onloadend = function() { // 這個(gè)事件在讀取結(jié)束后,無(wú)論成功或者失敗都會(huì)觸發(fā) if (reader.error) { console.log(reader.error); } else { document.getElementById("bytesRead").textContent = file.size; // 構(gòu)造 XMLHttpRequest 對(duì)象,發(fā)送文件 Binary 數(shù)據(jù) 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."); } }
在這個(gè)函數(shù)中,首先我們找到含有 <input type=”file” … /> 元素的 <form>,并找到含有上傳文件信息的 <input> 元素。如 <input> 元素中不含有文件,說(shuō)明用戶沒(méi)有選擇任何文件,此時(shí)將報(bào)錯(cuò)。
清單 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 返回的對(duì)象類型即為提到的 FileList。我們從中拿取第一個(gè)元素。之后,我們構(gòu)建 FileReader 對(duì)象:
var reader = new FileReader();
在 onloadstart事件觸發(fā)時(shí),填充頁(yè)面上表示讀取數(shù)據(jù)總量的 <span> 元素。參見清單 4
清單 4 onloadstart 事件
reader.onloadstart = function() { console.log("onloadstart"); document.getElementById("bytesTotal").textContent = file.size; } 在 onprogress 事件觸發(fā)時(shí),更新頁(yè)面上已讀取數(shù)據(jù)量的 <span> 元素。參見清單 5
清單 5 onprogress 事件
reader.onprogress = function(p) { console.log("onloadstart"); document.getElementById("bytesRead").textContent = p.loaded; }
最后的 onloadend 事件中,如果沒(méi)有錯(cuò)誤,我們將讀取文件內(nèi)容,并通過(guò) XMLHttpRequest 的方式上傳。
清單 6 onloadend 事件
reader.onloadend = function() { if (reader.error) { console.log(reader.error); } else { // 構(gòu)造 XMLHttpRequest 對(duì)象,發(fā)送文件 Binary 數(shù)據(jù) 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 的規(guī)范,我們也可以將事件 onloadend 的處理拆分為事件 error 以及事件 load 的處理。
在這個(gè)示例中,我們后臺(tái)使用一個(gè) JSP 來(lái)處理上傳。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) { // 讀取數(shù)據(jù) 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 請(qǐng)求中接受文件名字以及二進(jìn)制數(shù)據(jù)。將二進(jìn)制數(shù)據(jù)寫入到服務(wù)器的“D:\”路徑中。并返回文件的完整路徑。以上代碼可以在最新的 Firefox 3.6 中調(diào)試通過(guò)。
四、使用拖拽上傳文件
前面我們介紹了怎樣通過(guò) HTML5 的 File API 來(lái)讀取本地文件內(nèi)容并上傳到服務(wù)器,通過(guò)這種方式已經(jīng)能夠滿足大部分用戶的需求了。其中一個(gè)不足是用戶只能通過(guò)點(diǎn)擊“瀏覽”按鈕來(lái)逐個(gè)添加文件,如果需要批量上傳文件,會(huì)導(dǎo)致用戶體驗(yàn)不是很友好。而在桌面應(yīng)用中,用戶一般可以通過(guò)鼠標(biāo)拖拽的方式方便地上傳文件。拖拽一直是 Web 應(yīng)用的一個(gè)軟肋,一般瀏覽器都不提供對(duì)拖拽的支持。雖然 Web 程序員可以通過(guò)鼠標(biāo)的 mouseenter,mouseover 和 mouseout 等事件來(lái)實(shí)現(xiàn)拖拽效果,但是這種方式也只能使拖拽的范圍局限在瀏覽器里面。一個(gè)好消息是 HTML5 里面不僅加入了 File API,而且加入了對(duì)拖拽的支持,F(xiàn)irefox 3.5 開始已經(jīng)對(duì) File API 和拖拽提供了支持。下面我們先簡(jiǎn)要介紹一下拖拽的使用,然后用一個(gè)例子來(lái)說(shuō)明如何通過(guò)拖拽上傳文件。
1、拖拽簡(jiǎn)介
拖拽一般涉及兩個(gè)對(duì)象:拖拽源和拖拽目標(biāo)。
拖拽源:在 HTML5 草案里如果一個(gè)對(duì)象可以作為源被拖拽,需要設(shè)置 draggable 屬性為 true 來(lái)標(biāo)識(shí)該對(duì)象可作為拖拽源。然后偵聽源對(duì)象的 dragstart 事件,在事件處理函數(shù)里設(shè)置好 DataTransfer。在 DataTransfer 里可以設(shè)置拖拽數(shù)據(jù)的類型和值。比如是純文本的值,可以設(shè)置類型為"text/plain",url 則把類型設(shè)置為"text/uri-list"。 這樣,目標(biāo)對(duì)象就可以根據(jù)期望的類型來(lái)選擇數(shù)據(jù)了。
拖拽目標(biāo):一個(gè)拖拽目標(biāo)必須偵聽 3 個(gè)事件。
dragenter:目標(biāo)對(duì)象通過(guò)響應(yīng)這個(gè)事件來(lái)確定是否接收拖拽。如果接收則需要取消這個(gè)事件,停止時(shí)間的繼續(xù)傳播。
dragover:通過(guò)響應(yīng)這個(gè)事件來(lái)顯示拖拽的提示效果。
drop:目標(biāo)對(duì)象通過(guò)響應(yīng)這個(gè)事件來(lái)處理拖拽數(shù)據(jù)。在下面的例子里我們將在 drop 事件的處理函數(shù)里獲取 DataTransfer 對(duì)象,取出要上傳的文件。
由于本文主要介紹 File API,對(duì)這部分不作詳細(xì)解釋,感興趣的讀者可以參考 HTML5 草案(見參考資料)。
2、拖拽上傳文件實(shí)例
下面以一個(gè)較為具體的例子說(shuō)明如何結(jié)合拖拽和 File API 來(lái)上傳文檔。由于直接和桌面交互,所以我們不需要處理拖拽源,直接在目標(biāo)對(duì)象里從 DataTransfer 對(duì)象獲取數(shù)據(jù)即可。
首先,我們需要?jiǎng)?chuàng)建一個(gè)目標(biāo)容器用來(lái)接收拖拽事件,添加一個(gè) div 元素即可。然后用一個(gè)列表來(lái)展示上傳文件的縮略圖,進(jìn)度條及文件名。參見清單 8 的 HTML 代碼和圖 3 的效果圖。詳細(xì)代碼請(qǐng)參見附件中的 dnd.html 文件。
清單 8 拖曳目標(biāo)的 HTML 代碼
<div id="container"> <span>Drag and drop files here to upload.</span> <ul id="fileList"></ul> </div>
拖拽目標(biāo)創(chuàng)建好之后,我們需要偵聽其對(duì)應(yīng)的事件 dragenter,dragover 和 drop。在 dragenter 事件處理函數(shù)里,我們只是簡(jiǎn)單地清除文件列表,然后取消 dragenter 事件的傳播,表示我們接收該事件。更加妥當(dāng)?shù)淖鞣ㄊ桥袛?DataTransfer 里的數(shù)據(jù)是否為文件,這里我們假設(shè)所有拖拽源都是文件。dragover 事件里我們?nèi)∠撌录褂媚J(rèn)的拖拽顯示效果。在 drop 事件里我們注冊(cè)了 handleDrop 事件處理函數(shù)來(lái)獲取文件信息并上傳文件。清單 9 展示了這些事件處理函數(shù)。
清單 9 設(shè)置事件處理函數(shù)
function addDNDListeners() { var container = document.getElementById("container"); var fileList = document.getElementById("fileList"); // 拖拽進(jìn)入目標(biāo)對(duì)象時(shí)觸發(fā) container.addEventListener("dragenter", function(event) { fileList.innerHTML =''; event.stopPropagation(); event.preventDefault(); }, false); // 拖拽在目標(biāo)對(duì)象上時(shí)觸發(fā) container.addEventListener("dragover", function(event) { event.stopPropagation(); event.preventDefault(); }, false); // 拖拽結(jié)束時(shí)觸發(fā) container.addEventListener("drop", handleDrop, false); } window.addEventListener("load", addDNDListeners, false);
處理 drop 事件
用戶在拖拽結(jié)束時(shí)松開鼠標(biāo)觸發(fā) drop 事件。在 drop 事件里,我們可以通過(guò) event 參數(shù)的 DataTransfer 對(duì)象獲取 files 數(shù)據(jù),通過(guò)遍歷 files 數(shù)組可以獲取每個(gè)文件的信息。然后針對(duì)每個(gè)文件,創(chuàng)建 HTML 元素來(lái)顯示縮略圖,進(jìn)度條和文件名稱。File 對(duì)象的 getAsDataURL 可以將文件內(nèi)容以 URL 的形式返回,對(duì)圖片文件來(lái)說(shuō)可以用來(lái)顯示縮略圖。要注意的一點(diǎn)是,在 drop 事件處理函數(shù)里要取消事件的繼續(xù)傳播和默認(rèn)處理函數(shù),結(jié)束 drop 事件的處理。清單 10 展示了 drop 事件的處理代碼。
清單 10 drop 事件的處理
function handleDrop(event) { // 獲取拖拽的文件列表 var files = event.dataTransfer.files; event.stopPropagation(); event.preventDefault(); var fileList = document.getElementById("fileList"); // 展示文件縮略圖,文件名和上傳進(jìn)度,上傳文件 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) } }
上傳文件
我們可以通過(guò) XMLHttpRequest 對(duì)象的 sendAsBinary 方法來(lái)上傳文件,通過(guò)偵聽 upload 的 progress,load 和 error 事件來(lái)監(jiān)測(cè)文件上傳的進(jìn)度,成功完成或是否發(fā)生錯(cuò)誤。在 progress 事件處理函數(shù)里我們通過(guò)計(jì)算已經(jīng)上傳的比例來(lái)確定進(jìn)度條的位置。參見清單 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; // 設(shè)置上傳文件相關(guān)的事件處理函數(shù) 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) { // 將進(jìn)度換算成百分比 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); }
本文通過(guò)對(duì) File API 規(guī)范的講解,以及兩個(gè)展示其使用方法的例子,為大家提前揭示了作為未來(lái) HTML5 重要組成部分的 JavaScript File API 的全貌。利用它,結(jié)合其他 HTML5 的新特性,比如 Drag&Drop,我們可以利用純 JavaScript 方案,為用戶提供更好使用體驗(yàn)的 Web 應(yīng)用,與此同時(shí),這樣的一致化方案也使我們避免了以往跨瀏覽器支持所花費(fèi)的巨大代價(jià)。相信 File API 的出現(xiàn)和廣泛應(yīng)用,將會(huì)是未來(lái)的 Web 2.0 應(yīng)用的大勢(shì)所趨。
更多精彩內(nèi)容請(qǐng)參考專題《ajax上傳技術(shù)匯總》,《javascript文件上傳操作匯總》和《jQuery上傳操作匯總》進(jìn)行學(xué)習(xí)。
相關(guān)文章
TypeScript?使用?Tuple?Union?聲明函數(shù)重載
這篇文章主要介紹了TypeScript?使用?Tuple?Union?聲明函數(shù)重載,TypeScript 中為函數(shù)添加多個(gè)簽名后,依然需要添加相應(yīng)的代碼來(lái)判斷并從不同的簽名參數(shù)列表中獲取對(duì)應(yīng)的參數(shù),下文就來(lái)探索方法和技巧吧2022-04-04Cropper.js入門之在HTML中實(shí)現(xiàn)交互式圖像裁剪
這篇文章主要為大家介紹了Cropper.js入門之在HTML中實(shí)現(xiàn)交互式圖像裁剪示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05HTML5canvas 繪制一個(gè)圓環(huán)形的進(jìn)度表示實(shí)例
這篇文章主要介紹了HTML5canvas繪制一個(gè)圓環(huán)形的進(jìn)度表示實(shí)例的相關(guān)資料,需要的朋友可以參考下2016-12-12js 把字符串當(dāng)函數(shù)執(zhí)行的方法
一段字符串 里面包含了 要執(zhí)行的函數(shù)和參數(shù)等,需要去執(zhí)行這段字符串。2010-03-03D3.js實(shí)現(xiàn)簡(jiǎn)潔實(shí)用的動(dòng)態(tài)儀表盤的示例
本篇文章主要介紹了D3.js實(shí)現(xiàn)簡(jiǎn)潔實(shí)用的動(dòng)態(tài)儀表盤的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04簡(jiǎn)單時(shí)間提示DEMO從0開始一直進(jìn)行計(jì)時(shí)
點(diǎn)擊按鈕輸入框會(huì)從0開始一直進(jìn)行計(jì)時(shí),具體的實(shí)現(xiàn)示例如下,感興趣的朋友可以嘗試操作下哦2013-11-11微信小程序?qū)崿F(xiàn)視頻播放器發(fā)送彈幕
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)視頻播放器發(fā)送彈幕,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04