以往对于基于浏览器的应用而言,访问本地文件都是一件头疼的事情。虽然伴随着 Web 2.0 应用技术的不断发展,JavaScript 正在扮演愈来愈重要的角色,可是出于安全性的考虑,JavaScript 一直是没法访问本地文件的。因而,为了在浏览器中可以实现诸如拖拽并上传本地文件这样的功能,咱们就不得不求助于特定浏览器所提供的各类技术了。好比对于 IE,咱们须要经过 ActiveX 控件来获取对本地文件的访问能力,而对于 Firefox,一样也要借助插件开发。因为不一样浏览器的技术实现不尽相同,为了让程序可以支持多浏览器,咱们的程序就会变得十分复杂而难于维护。不过如今,这一切都由于 File API 的出现而获得了完全的改变。javascript
File API 是 Mozilla 向 W3C 提交的一个草案,旨在推出一套标准的 JavaScript API,其基本功能是实现用 JavaScript 对本地文件进行操做。出于安全性的考虑,该 API 只对本地文件提供有限的访问。有了它,咱们就能够很轻松的用纯 JavaScript 来实现本地文件的读取和上传了。目前,FireFox 3.6 是最早支持这一功能的浏览器。除此之外,最新版本的 Google Chrome 浏览器和 Safari 浏览器也有相应的支持。File API 有望成为 W3C 目前正在制定的将来 HTML 5 规范当中的一部分。html
本文后续章节将对 File API 作一个基本的介绍。并为读者演示:如何利用 File API 实现基于 file input 控件的本地文件读取与上传;以及利用拖拽实现从用户系统批量上传文件的功能。经过本文,读者能够了解最新的 File API 的概况以及使用,从而为构建具备更好用户体验的 Web 2.0 应用提供参考。java
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 种事件。参见表 1 及表 2。web
函数名称 | 功能 |
---|---|
readAsBinaryString() | 读取文件内容,读取结果为一个 binary string。文件每个 byte 会被表示为一个 [0..255] 区间内的整数。函数接受一个 File 对象做为参数。 |
readAsText() | 读取文件内容,读取结果为一串表明文件内容的文本。函数接受一个 File 对象以及文本编码名称做为参数。 |
readAsDataURL | 读取文件内容,读取结果为一个 data: 的 URL。DataURL 由 RFC2397 定义,具体能够参考 http://www.ietf.org/rfc/rfc2397.txt。 |
事件名称 | 事件说明 |
---|---|
Onloadstart | 文件读取开始时触发。 |
Progress | 当读取进行中时定时触发。事件参数中会含有已读取总数据量。 |
Abort | 当读取被停止时触发。 |
Error | 当读取出错时触发。 |
Load | 当读取成功完成时触发。 |
Loadend | 当读取完成时,不管成功或者失败都会触发。 |
接下来咱们用一个简单的例子展现 File API 的基本用法。这个示例包含两个代码文件,index.html 包含 Web 端的 HTML 代码和处理上传的 JavaScript 代码;upload.jsp 包含服务器端接收上传文件的代码。请参见附件中的 sourcecode.zip。在这个例子中,咱们将显示一个传统的带有 File 选择域的表单。当用户选择文件,点击提交后,咱们使用 File API 读取文件内容,并经过 XMLHttpRequest 对象,用 Ajax 的方式将文件上传到服务器上。图 2 展现了运行中的演示截图。数组
咱们逐步展现其中的代码。清单 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。安全
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> 元素中不含有文件,说明用户没有选择任何文件,此时将报错。服务器
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 对象:app
var reader = new FileReader();
在 onloadstart事件触发时,填充页面上表示读取数据总量的 <span> 元素。参见清单 4
reader.onloadstart = function() { console.log("onloadstart"); document.getElementById("bytesTotal").textContent = file.size; }
在 onprogress 事件触发时,更新页面上已读取数据量的 <span> 元素。参见清单 5
reader.onprogress = function(p) { console.log("onloadstart"); document.getElementById("bytesRead").textContent = p.loaded; }
最后的 onloadend 事件中,若是没有错误,咱们将读取文件内容,并经过 XMLHttpRequest 的方式上传。
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。
<%@ 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 文件。
<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 展现了这些事件处理函数。
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 事件的处理代码。
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); }
本文经过对 File API 规范的讲解,以及两个展现其使用方法的例子,为你们提早揭示了做为将来 HTML5 重要组成部分的 JavaScript File API 的全貌。利用它,结合其余 HTML5 的新特性,好比 Drag&Drop,咱们能够利用纯 JavaScript 方案,为用户提供更好使用体验的 Web 应用,与此同时,这样的一致化方案也使咱们避免了以往跨浏览器支持所花费的巨大代价。相信 File API 的出现和普遍应用,将会是将来的 Web 2.0 应用的大势所趋。