25.4 File API
var watchId = navigator.geolocation.watchPosition(function(position) { drawMapCenteredAt(position.coords.latitude, positions.coords.longitude); }, function(error) { console.log("Error code: " + error.code); console.log("Error message: " + error.message); });clearWatch(watchId);不能直接访问用户计算机中的文件,一直都是Web 应用开发中的一大障碍。2000 年以前,处理文件的唯一方式就是在表单中加入<input type="file">字段,仅此而已。File API(文件API)的宗旨是为Web 开发人员提供一种安全的方式,以便在客户端访问用户计算机中的文件,并更好地对这些文 件执行操作。支持File API 的浏览器有IE10+、Firefox 4+、Safari 5.0.5+、Opera 11.1+和Chrome。 File API 在表单中的文件输入字段的基础上,又添加了一些直接访问文件信息的接口。HTML5 在DOM 中为文件输入元素添加了一个files 集合。在通过文件输入字段选择了一或多个文件时,files集合中将包含一组File 对象,每个File 对象对应着一个文件。每个File 对象都有下列只读属性。
- name:本地文件系统中的文件名。
- size:文件的字节大小。
- type:字符串,文件的MIME 类型。
- lastModifiedDate:字符串,文件上一次被修改的时间(只有Chrome 实现了这个属性)。
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList, "change", function(event) { var files = EventUtil.getTarget(event).files, i = 0, len = files.length; while (i < len) {console.log(files[i].name + " (" + files[i].type + ", " + files[i].size + " bytes) ");i++; } });运行一下 这个例子把每个文件的信息输出到了控制台中。仅仅这一项功能,对Web 应用开发来说就已经是非常大的进步了。不过,File API 的功能还不止于此,通过它提供的FileReader 类型甚至还可以读取文件中的数据。
25.4.1 FileReader类型
FileReader 类型实现的是一种异步文件读取机制。可以把FileReader 想象成XMLHttpRequest,区别只是它读取的是文件系统,而不是远程服务器。为了读取文件中的数据,FileReader 提供了如下几个方法。- readAsText(file,encoding):以纯文本形式读取文件,将读取到的文本保存在result 属性中。第二个参数用于指定编码类型,是可选的。
- readAsDataURL(file):读取文件并将文件以数据URI 的形式保存在result 属性中。
- readAsBinaryString(file):读取文件并将一个字符串保存在result 属性中,字符串中的每个字符表示一字节。
- readAsArrayBuffer(file):读取文件并将一个包含文件内容的ArrayBuffer 保存在result 属性中。
这些读取文件的方法为灵活地处理文件数据提供了极大便利。例如,可以读取图像文件并将其保存为数据URI,以便将其显示给用户,或者为了解析方便,可以将文件读取为文本形式。
由于读取过程是异步的,因此FileReader 也提供了几个事件。其中最有用的三个事件是progress、error 和load,分别表示是否又读取了新数据、是否发生了错误以及是否已经读完了整个文件。
每过50ms 左右,就会触发一次progress 事件,通过事件对象可以获得与XHR 的progress 事件相同的信息(属性):lengthComputable、loaded 和total。另外,尽管可能没有包含全部数据,但每次progress 事件中都可以通过FileReader 的result 属性读取到文件内容。
由于种种原因无法读取文件,就会触发error 事件。触发error 事件时,相关的信息将保存到FileReader 的error 属性中。这个属性中将保存一个对象,该对象只有一个属性code,即错误码。
这个错误码是1 表示未找到文件,是2 表示安全性错误,是3 表示读取中断,是4 表示文件不可读,是5 表示编码错误。文件成功加载后会触发load 事件;如果发生了error 事件,就不会发生load 事件。以下是一个使用上述三个事件的例子。
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList, "change", function(event) { var info = "", output = document.getElementById("output"), progress = document.getElementById("progress"), files = EventUtil.getTarget(event).files, type = "default", reader = new FileReader(); if (/image/.test(files[0].type)) {reader.readAsDataURL(files[0]);type = "image"; } else {reader.readAsText(files[0]);type = "text"; } reader.onerror = function() {output.innerHTML = "Could not read file, error code is " + reader.error.code; }; reader.onprogress = function(event) {if (event.lengthComputable) {progress.innerHTML = event.loaded + "/" + event.total;} }; reader.onload = function() {var html = "";switch (type) {case "image":html = "<img src=\"" + reader.result + "\">";break;case "text":html = reader.result;break;}output.innerHTML = html; }; });运行一下
这个例子读取了表单字段中选择的文件,并将其内容显示在了页面中。如果文件有MIMI 类型,表示文件是图像,因此在load 事件中就把它保存为数据URI,并在页面中将这幅图像显示出来。如果文件不是图像,则以字符串形式读取文件内容,然后如实在页面中显示读取到的内容。这里使用了progress 事件来跟踪读取了多少字节的数据,而error 事件则用于监控发生的错误。
如果想中断读取过程,可以调用abort()方法,这样就会触发abort 事件。在触发load、error或abort 事件后,会触发另一个事件loadend。loadend 事件发生就意味着已经读取完整个文件,或者读取时发生了错误,或者读取过程被中断。 实现File API 的所有浏览器都支持readAsText()和readAsDataURL()方法。但IE10 PR 2 并未实现readAsBinaryString()和readAsArrayBuffer()方法。25.4.2 读取部分内容
有时候,我们只想读取文件的一部分而不是全部内容。为此,File 对象还支持一个slice()方法,这个方法在Firefox 中的实现叫mozSlice(),在Chrome 中的实现叫webkitSlice(),Safari 的5.1 及之前版本不支持这个方法。slice()方法接收两个参数:起始字节及要读取的字节数。这个方法返回一个Blob 的实例,Blob 是File 类型的父类型。下面是一个通用的函数,可以在不同实现中使用slice()方法:function blobSlice(blob, startByte, length) { if (blob.slice) {return blob.slice(startByte, length); } else if (blob.webkitSlice) {return blob.webkitSlice(startByte, length); } else if (blob.mozSlice) {return blob.mozSlice(startByte, length); } else {return null; } }Blob 类型有一个size 属性和一个type 属性,而且它也支持slice()方法,以便进一步切割数据。通过FileReader 也可以从Blob 中读取数据。下面这个例子只读取文件的32B 内容。
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList, "change", function(event) { var info = "", output = document.getElementById("output"), progress = document.getElementById("progress"), files = EventUtil.getTarget(event).files, reader = new FileReader(), blob = blobSlice(files[0], 0, 32); if (blob) {reader.readAsText(blob);reader.onerror = function() {output.innerHTML = "Could not read file, error code is " + reader.error.code;};reader.onload = function() {output.innerHTML = reader.result;}; } else {alert("Your browser doesn' t support slice()."); } });运行一下 只读取文件的一部分可以节省时间,非常适合只关注数据中某个特定部分(如文件头部)的情况。
25.4.3 对象URL
对象URL 也被称为blob URL,指的是引用保存在File 或Blob 中数据的URL。使用对象URL 的好处是可以不必把文件内容读取到JavaScript 中而直接使用文件内容。为此,只要在需要文件内容的地方提供对象URL 即可。要创建对象URL,可以使用window.URL.createObjectURL()方法,并传入File 或Blob 对象。这个方法在Chrome 中的实现叫window.webkitURL.createObjectURL(),因此可以通过如下函数来消除命名的差异:function createObjectURL(blob) { if (window.URL) {return window.URL.createObjectURL(blob); } else if (window.webkitURL) {return window.webkitURL.createObjectURL(blob); } else {return null; } }这个函数的返回值是一个字符串,指向一块内存的地址。因为这个字符串是URL,所以在DOM中也能使用。例如,以下代码可以在页面中显示一个图像文件:
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList, "change", function(event) { var info = "", output = document.getElementById("output"), progress = document.getElementById("progress"), files = EventUtil.getTarget(event).files, reader = new FileReader(), url = createObjectURL(files[0]); if (url) {if (/image/.test(files[0].type)) {output.innerHTML = "<img src=\"" + url + "\">";} else {output.innerHTML = "Not an image.";} } else {output.innerHTML = "Your browser doesn't support object URLs."; } });运行一下 直接把对象URL 放在<img>标签中,就省去了把数据先读到JavaScript 中的麻烦。另一方面,<img>标签则会找到相应的内存地址,直接读取数据并将图像显示在页面中。 如果不再需要相应的数据,最好释放它占用的内容。但只要有代码在引用对象URL,内存就不会释放。要手工释放内存,可以把对象URL 传给window.URL.revokeOjbectURL()(在Chrome 中是window.webkitURL.revokeObjectURL())。要兼容这两种方法的实现,可以使用以下函数:
function revokeObjectURL(url) { if (window.URL) {window.URL.revokeObjectURL(url); } else if (window.webkitURL) {window.webkitURL.revokeObjectURL(url); } }页面卸载时会自动释放对象URL 占用的内存。不过,为了确保尽可能少地占用内存,最好在不需要某个对象URL 时,就马上手工释放其占用的内存。 支持对象URL 的浏览器有IE10+、Firefox 4 和Chrome。
25.4.4 读取拖放的文件
围绕读取文件信息,结合使用HTML5 拖放API 和文件API,能够创造出令人瞩目的用户界面:在页面上创建了自定义的放置目标之后,你可以从桌面上把文件拖放到该目标。与拖放一张图片或者一个链接类似,从桌面上把文件拖放到浏览器中也会触发drop 事件。而且可以在event.dataTransfer. files中读取到被放置的文件,当然此时它是一个File 对象,与通过文件输入字段取得的File 对象一样。下面这个例子会将放置到页面中自定义的放置目标中的文件信息显示出来:var droptarget = document.getElementById("droptarget"); function handleEvent(event) { var info = "", output = document.getElementById("output"), files, i, len; EventUtil.preventDefault(event); if (event.type == "drop") {files = event.dataTransfer.files;i = 0;len = files.length;while (i < len) {info += files[i].name + " (" + files[i].type + ", " + files[i].size + " bytes)<br>";i++;}output.innerHTML = info; } } EventUtil.addHandler(droptarget, "dragenter", handleEvent); EventUtil.addHandler(droptarget, "dragover", handleEvent); EventUtil.addHandler(droptarget, "drop", handleEvent);运行一下 与之前展示的拖放示例一样,这里也必须取消dragenter、dragover 和drop 的默认行为。在drop 事件中,可以通过event.dataTransfer.files 读取文件信息。还有一种利用这个功能的流行做法,即结合XMLHttpRequest 和拖放文件来实现上传。
25.4.5 使用XHR上传文件
通过File API 能够访问到文件内容,利用这一点就可以通过XHR 直接把文件上传到服务器。当然啦,把文件内容放到send()方法中,再通过POST 请求,的确很容易就能实现上传。但这样做传递的是文件内容,因而服务器端必须收集提交的内容,然后再把它们保存到另一个文件中。其实,更好的做法是以表单提交的方式来上传文件。
这样使用FormData 类型就很容易做到了(第21 章介绍过FormData)。首先,要创建一个FormData对象,通过它调用append()方法并传入相应的File 对象作为参数。然后,再把FormData 对象传递给XHR 的send()方法,结果与通过表单上传一模一样。var droptarget = document.getElementById("droptarget"); function handleEvent(event) { var info = "", output = document.getElementById("output"), data, xhr, files, i, len; EventUtil.preventDefault(event); if (event.type == "drop") {data = new FormData();files = event.dataTransfer.files;i = 0;len = files.length;while (i < len) {data.append("file" + i, files[i]);i++;}xhr = new XMLHttpRequest();xhr.open("post", "FileAPIExample06Upload.php", true);xhr.onreadystatechange = function() {if (xhr.readyState == 4) {alert(xhr.responseText);}};xhr.send(data); } } EventUtil.addHandler(droptarget, "dragenter", handleEvent); EventUtil.addHandler(droptarget, "dragover", handleEvent); EventUtil.addHandler(droptarget, "drop", handleEvent);运行一下 这个例子创建一个FormData 对象,与每个文件对应的键分别是file0、file1、file2 这样的格式。注意,不用额外写任何代码,这些文件就可以作为表单的值提交。而且,也不必使用FileReader,只要传入File 对象即可。 使用FormData 上传文件,在服务器端就好像是接收到了常规的表单数据一样,一切按部就班地处理即可。换句话说,如果服务器端使用的是PHP,那么$_FILES 数组中就会保存着上传的文件。支持以这种方式上传文件的浏览器有Firefox 4+、Safari 5+和Chrome。