4.File API:
不能直接访问用户计算机中的文件,一直都是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 实现了这个属性)。
举个例子,通过侦听change 事件并读取files 集合就可以知道选择的每个文件的信息:
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 类型甚至还可以读取文件中的数据。
- 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()方法。
- 读取部分内容:
有时候,我们只想读取文件的一部分而不是全部内容。为此,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().");
}
});
只读取文件的一部分可以节省时间,非常适合只关注数据中某个特定部分(如文件头部)的情况。
- 对象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。
- 读取拖放的文件:围绕读取文件信息,结合使用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 和拖放文件来实现上传。
- 使用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。
5.Web 计时:页面性能一直都是Web 开发人员最关注的领域。但直到最近,度量页面性能指标的唯一方式,就是提高代码复杂程度和巧妙地使用JavaScript 的Date 对象。Web Timing API 改变了这个局面,让开发人员通过JavaScript 就能使用浏览器内部的度量结果,通过直接读取这些信息可以做任何想做的分析。与本章介绍过的其他API 不同,Web Timing API 实际上已经成为了W3C 的建议标准,只不过目前支持它的浏览器还不够多。
Web 计时机制的核心是window.performance 对象。对页面的所有度量信息,包括那些规范中已经定义的和将来才能确定的,都包含在这个对象里面。Web Timing 规范一开始就为performance 对象定义了两个属性。
其中,performance.navigation 属性也是一个对象,包含着与页面导航有关的多个属性,如下所示。
1.redirectCount:页面加载前的重定向次数。
2.type:数值常量,表示刚刚发生的导航类型。
- performance.navigation.TYPE_NAVIGATE (0):页面第一次加载。
- performance.navigation.TYPE_RELOAD (1):页面重载过。
- performance.navigation.TYPE_BACK_FORWARD (2):页面是通过“后退”或“前进”按钮打开的。
另外,performance.timing 属性也是一个对象,但这个对象的属性都是时间戳(从软件纪元开始经过的毫秒数),不同的事件会产生不同的时间值。这些属性如下所示。
- navigationStart:开始导航到当前页面的时间。
- unloadEventStart:前一个页面的unload 事件开始的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0。
- unloadEventEnd:前一个页面的unload 事件结束的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0。
- redirectStart:到当前页面的重定向开始的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0。
- redirectEnd:到当前页面的重定向结束的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0。
- fetchStart:开始通过HTTP GET 取得页面的时间。
- domainLookupStart:开始查询当前页面DNS 的时间。
- domainLookupEnd:查询当前页面DNS 结束的时间。
- connectStart:浏览器尝试连接服务器的时间。
- connectEnd:浏览器成功连接到服务器的时间。
- secureConnectionStart:浏览器尝试以SSL 方式连接服务器的时间。不使用SSL 方式连接时,这个属性的值为0。
- requestStart:浏览器开始请求页面的时间。
- responseStart:浏览器接收到页面第一字节的时间。
- responseEnd:浏览器接收到页面所有内容的时间。
- domLoading:document.readyState 变为"loading"的时间。
- domInteractive:document.readyState 变为"interactive"的时间。
- domContentLoadedEventStart:发生DOMContentLoaded 事件的时间。
- domContentLoadedEventEnd:DOMContentLoaded 事件已经发生且执行完所有事件处理程序的时间。
- domComplete:document.readyState 变为"complete"的时间。
- loadEventStart:发生load 事件的时间。
- loadEventEnd:load 事件已经发生且执行完所有事件处理程序的时间。
通过这些时间值,就可以全面了解页面在被加载到浏览器的过程中都经历了哪些阶段,而哪些阶段可能是影响性能的瓶颈。给大家推荐一个使用Web Timing API 的绝好示例, 地址是http://webtimingdemo.appspot.com/。
支持Web Timing API 的浏览器有IE10+和Chrome。
6.Web Workers:
随着Web 应用复杂性的与日俱增,越来越复杂的计算在所难免。长时间运行的JavaScript 进程会导致浏览器冻结用户界面,让人感觉屏幕“冻结”了。Web Workers 规范通过让JavaScript 在后台运行解决了这个问题。浏览器实现Web Workers 规范的方式有很多种,可以使用线程、后台进程或者运行在其他处理器核心上的进程,等等。具体的实现细节其实没有那么重要,重要的是开发人员现在可以放心地运行JavaScript,而不必担心会影响用户体验了。
目前支持Web Workers 的浏览器有IE10+、Firefox 3.5+、Safari 4+、Opera 10.6+、Chrome 和iOS 版的Safari。
- 使用Worker:实例化Worker 对象并传入要执行的JavaScript 文件名就可以创建一个新的Web Worker。例如:
var worker = new Worker("stufftodo.js");
这行代码会导致浏览器下载stufftodo.js,但只有Worker 接收到消息才会实际执行文件中的代码。要给Worker 传递消息,可以使用postMessage()方法(与XDM中的postMessage()方法类似):
worker.postMessage(“start! ");
消息内容可以是任何能够被序列化的值,不过与XDM 不同的是,在所有支持的浏览器中,postMessage()都能接收对象参数(Safari 4 是支持Web Workers 的浏览器中最后一个只支持字符串参数的)。因此,可以随便传递任何形式的对象数据,如下面的例子所示:
worker.postMessage({
type: "command",
message: "start! "
});
一般来说,可以序列化为JSON 结构的任何值都可以作为参数传递给postMessage()。换句话说,这就意味着传入的值是被复制到Worker 中,而非直接传过去的(与XDM类似)。
Worker 是通过message 和error 事件与页面通信的。这里的message 事件与XDM中的message事件行为相同,来自Worker 的数据保存在event.data 中。Worker 返回的数据也可以是任何能够被序列化的值:
worker.onmessage = function(event){
var data = event.data;
//对数据进行处理
}
Worker 不能完成给定的任务时会触发error 事件。具体来说,Worker 内部的JavaScript 在执行过程中只要遇到错误,就会触发error 事件。发生error 事件时,事件对象中包含三个属性:filename、lineno 和message,分别表示发生错误的文件名、代码行号和完整的错误消息。
worker.onerror = function(event){
console.log("ERROR: " + event.filename + " (" + event.lineno + "): " +
event.message);
};
建议大家在使用Web Workers 时,始终都要使用onerror 事件处理程序,即使这个函数(像上面例子所示的)除了把错误记录到日志中什么也不做都可以。否则,Worker 就会在发生错误时,悄无声息地失败了。
任何时候,只要调用terminate()方法就可以停止Worker 的工作。而且,Worker 中的代码会立即停止执行,后续的所有过程都不会再发生(包括error 和message 事件也不会再触发)。
worker.terminate(); //立即停止Worker 的工作
- Worker全局作用域:
关于Web Worker,最重要的是要知道它所执行的JavaScript 代码完全在另一个作用域中,与当前网页中的代码不共享作用域。在Web Worker 中,同样有一个全局对象和其他对象以及方法。但是,WebWorker 中的代码不能访问DOM,也无法通过任何方式影响页面的外观。
Web Worker 中的全局对象是worker 对象本身。也就是说,在这个特殊的全局作用域中,this 和self 引用的都是worker 对象。为便于处理数据,Web Worker 本身也是一个最小化的运行环境。
- 最小化的navigator 对象,包括onLine、appName、appVersion、userAgent 和platform属性;
- 只读的location 对象;
- setTimeout()、setInterval()、clearTimeout()和clearInterval()方法;
- XMLHttpRequest 构造函数。
显然,Web Worker 的运行环境与页面环境相比,功能是相当有限的。
当页面在worker 对象上调用postMessage()时,数据会以异步方式被传递给worker,进而触发worker 中的message 事件。为了处理来自页面的数据,同样也需要创建一个onmessage 事件处理程序。
//Web Worker 内部的代码
self.onmessage = function(event){
var data = event.data;
//处理数据
};
大家看清楚,这里的self 引用的是Worker 全局作用域中的worker 对象(与页面中的Worker 对象不同一个对象)。Worker 完成工作后,通过调用postMessage()可以把数据再发回页面。例如,下面的例子假设需要Worker 对传入的数组进行排序,而Worker 在排序之后又将数组发回了页面:
//Web Worker 内部的代码
self.onmessage = function(event){
var data = event.data;
//别忘了,默认的sort()方法只比较字符串
data.sort(function(a, b){
return a – b;
});
self.postMessage(data);
};
传递消息就是页面与Worker 相互之间通信的方式。在Worker 中调用postMessage()会以异步方式触发页面中Worker 实例的message 事件。如果页面想要使用这个Worker,可以这样:
//在页面中
var data = [23,4,7,9,2,14,6,651,87,41,7798,24],
worker = new Worker("WebWorkerExample01.js");
worker.onmessage = function(event){
var data = event.data;
//对排序后的数组进行操作
};
//将数组发送给worker 排序
worker.postMessage(data);
排序的确是比较消耗时间的操作,因此转交给Worker 做就不会阻塞用户界面了。另外,把彩色图像转换成灰阶图像以及加密解密之类的操作也是相当费时的。
在Worker 内部,调用close()方法也可以停止工作。就像在页面中调用terminate()方法一样,Worker 停止工作后就不会再有事件发生了。
//Web Worker 内部的代码
self.close();
- 包含其他脚本:既然无法在Worker 中动态创建新的<script>元素,那是不是就不能向Worker 中添加其他脚本了呢?不是,Worker 的全局作用域提供这个功能,即我们可以调用importScripts()方法。这个方法接收一个或多个指向JavaScript 文件的URL。每个加载过程都是异步进行的,因此所有脚本加载并执行之后,importScripts()才会执行。例如:
//Web Worker 内部的代码
importScripts("file1.js", "file2.js");
即使file2.js 先于file1.js 下载完,执行的时候仍然会按照先后顺序执行。而且,这些脚本是在Worker 的全局作用域中执行,如果脚本中包含与页面有关的JavaScript 代码,那么脚本可能无法正确运行。请记住,Worker 中的脚本一般都具有特殊的用途,不会像页面中的脚本那么功能宽泛。
- Web Workers的未来:
Web Workers 规范还在继续制定和改进之中。本节所讨论的Worker 目前被称为“专用Worker”(dedicated worker),因为它们是专门为某个特定的页面服务的,不能在页面间共享。该规范的另外一个概念是“共享Worker”(shared worker),这种Worker 可以在浏览器的多个标签中打开的同一个页面间共享。虽然Safari 5、Chrome 和Opera 10.6 都实现了共享Worker,但由于该规范尚未完稿,因此很可能还会有变动。
另外,关于在Worker 内部能访问什么不能访问什么,到如今仍然争论不休。有人认为Worker 应该像页面一样能够访问任意数据,不光是XHR,还有localStorage、sessionStorage、Indexed DB、Web Sockets、Server-Send Events 等。好像支持这个观点的人更多一些,因此未来的Worker 全局作用域很可能会有更大的空间。