postMessage()
postMessage()方法接收 3 个参数:消息、表示目标接收源的字符串(现在为任何结构的数据)和可选的可传输对象的数组(只 与工作线程相关)。第二个参数对于安全非常重要,其可以限制浏览器交付数据的目标。下面来看一个 例子:
let iframeWindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "<http://www.wrox.com>");
传给 onmessage 事件处理程序的 event对象包含以下 3 方面重要信息。
data:作为第一个参数传递给 postMessage()的字符串数据。
source:发送消息的文档中 window 对象的代理。这个代理对象主要用于在发送上一条消息的 窗口中执行 postMessage()方法。如果发送窗口有相同的源,那么这个对象应该就是 window 对象。
window.addEventListener("message", (event) => {
// 确保来自预期发送者
if (event.origin == "http://www.wrox.com") {
// 对数据进行一些处理
processMessage(event.data);
// 可选:向来源窗口发送一条消息
event.source.postMessage("Received!", "http://p2p.wrox.com");
}
});
postMessage()的第一个参数的最初实现始终是一个字符串。后来, 第一个参数改为允许任何结构的数据传入,不过并非所有浏览器都实现了这个改变。为此,最好就是只 通过 postMessage()发送字符串。如果需要传递结构化数据,那么最好先对该数据调用 JSON.stringify(),通过 postMessage()传过去之后,再在 onmessage 事件处理程序中调用 JSON.parse()。
File API 与 Blob API
File类型
File API 仍然以表单中的文件输入字段为基础,但是增加了直接访问文件信息的能力。HTML5 在 DOM 上为文件输入元素添加了 files 集合。当用户在文件字段中选择一个或多个文件时,这个 files 集合中会包含一组 File 对象,表示被选中的文件。每个 File 对象都有一些只读属性。
lastModifiedDate:表示文件最后修改时间的字符串。这个属性只有 Chome 实现了。 例如,通过监听 change 事件然后遍历 files 集合可以取得每个选中文件的信息:
<form>
<label for="file-input">Select one or more files:</label>
<input type="file" id="file-input" name="files[]" multiple>
</form>
<ul id="files-list"></ul>
<script>
let fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", (event) => {
let files = event.target.files,
i = 0,
len = files.length;
while (i < len) {
const f = files[i];
const li = document.createElement("li");
li.textContent = `${f.name} (${f.type}, ${f.size} bytes)`;
filesList.appendChild(li);
i++;
}
});
</script>
这个例子简单地在控制台输出了每个文件的信息。仅就这个能力而言,已经可以说是 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 会发布几个事件,其中 3 个最有用的事件是 progress、error 和 load,分别表示还有更多数据、发生了错误和读取完成。 progress 事件每 50 毫秒就会触发一次,其与 XHR 的 progress 事件具有相同的信息: lengthComputable、loaded和total。此外,在progress事件中可以读取FileReader的result 属性,即使其中尚未包含全部数据。 error 事件会在由于某种原因无法读取文件时触发。触发 error 事件时,FileReader 的 error 属性会包含错误信息。这个属性是一个对象,只包含一个属性:code。这个错误码的值可能是 1(未找 到文件)、2(安全错误)、3(读取被中断)、4(文件不可读)或 5(编码错误)。 load 事件会在文件成功加载后触发。如果 error 事件被触发,则不会再触发 load 事件。下面的 例子演示了所有这 3 个事件:
<form>
<label for="file-input">Select one or more files:</label>
<input type="file" id="file-input" name="files[]" multiple>
</form>
output: <div id="output"></div>
progress: <div id="progress"></div>
<script>
let fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", (event) => {
let info = "",
output = document.getElementById("output"),
progress = document.getElementById("progress"),
files = event.target.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 () {
let html = "";
switch (type) {
case "image":
html = `<img src="${reader.result}">`;
break;
case "text":
html = reader.result;
break;
}
output.innerHTML = html;
};
});
</script>
以上代码从表单字段中读取一个文件,并将其内容显示在了网页上。如果文件的 MIME 类型表示它 是一个图片,那么就将其读取后保存为数据 URI,在 load 事件触发时将数据 URI 作为图片插入页面中。 如果文件不是图片,则读取后将其保存为文本并原样输出到网页上。progress 事件用于跟踪和显示读 取文件的进度,而 error 事件用于监控错误。 如果想提前结束文件读取,则可以在过程中调用 abort()方法,从而触发 abort 事件。在 load、 error 和 abort 事件触发后,还会触发 loadend 事件。loadend 事件表示在上述 3 种情况下,所有读 取操作都已经结束。readAsText()和 readAsDataURL()方法已经得到了所有主流浏览器支持。
FileReaderSync类型
FileReaderSync 类型就是 FileReader 的同步版本。这个类型拥有与 FileReader 相同的方法,只有在整个文件都加载到内存之后才会继续执行。FileReaderSync 只在工作线程中可用, 因为如果读取整个文件耗时太长则会影响全局。 假设通过 postMessage()向工作线程发送了一个 File 对象。以下代码会让工作线程同步将文件 读取到内存中,然后将文件的数据 URL 发回来:
// worker.js
self.onmessage = (messageEvent) => {
const syncReader = new FileReaderSync();
console.log(syncReader); // FileReaderSync {}
// 读取文件时阻塞工作线程
const result = syncReader.readAsDataURL(messageEvent.data);
// PDF 文件的示例响应
console.log(result); // data:application/pdf;base64,JVBERi0xLjQK...
// 把URL发回去
self.postMessage(result);
};
Blob与部分读取
某些情况下,可能需要读取部分文件而不是整个文件。为此,File 对象提供了一个名为 slice() 的方法。slice()方法接收两个参数:起始字节和要读取的字节数。这个方法返回一个 Blob 的实例, 而 Blob 实际上是 File 的超类。 blob 表示二进制大对象(binary larget object),是 JavaScript 对不可修改二进制数据的封装类型。包 含字符串的数组、ArrayBuffers、ArrayBufferViews,甚至其他 Blob 都可以用来创建 blob。Blob 构造函数可以接收一个 options 参数,并在其中指定 MIME 类型:
console.log(new Blob(['foo']));
// Blob {size: 3, type: ""}
console.log(new Blob(['{"a": "b"}'], { type: 'application/json' }));
// {size: 10, type: "application/json"}
console.log(new Blob(['<p>Foo</p>', '<p>Bar</p>'], { type: 'text/html' }));
// {size: 20, type: "text/html"}
Blob 对象有一个 size 属性和一个 type 属性,还有一个 slice()方法用于进一步切分数据。另 外也可以使用 FileReader 从 Blob 中读取数据。下面的例子只会读取文件的前 32 字节:
let fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", (event) => {
let info = "",
output = document.getElementById("output"),
progress = document.getElementById("progress"),
files = event.target.files,
reader = new FileReader()
const blob = new Blob(files, { type: "text/plain" });
const slice = blob.slice(0, 32);
if (slice) {
reader.readAsText(slice);
reader.onerror = function () {
output.innerHTML = "Could not read file, error code is " +
reader.error.code;
};
reader.onload = function () {
output.innerHTML = reader.result;
};
} else {
console.log("Your browser doesn't support slice()."); 22
}
});
对象URL与Blob
对象URL有时候也称作Blob URL,是指引用存储在File或Blob中数据的 URL。对象URL的优点是不用把文件内容读取到JavaScript也可以使用文件。只要在适当位置提供对象URL即可。要创建对 象URL,可以使用 window.URL.createObjectURL()方法并传入File或Blob对象。这个函数返回的值是一个指向内存中地址的字符串。因为这个字符串是URL,所以可以在DOM中直接使用。例如, 以下代码使用对象URL在页面中显示了一张图片:
fileInput.addEventListener("change", (event) => {
let info = "",
output = document.getElementById("output"),
progress = document.getElementById("progress"),
files = event.target.files,
reader = new FileReader(),
url = window.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.revokeObjectURL()。页面卸载时, 所有对象 URL 占用的内存都会被释放。不过,最好在不使用时就立即释放内存,以便尽可能保持页面 占用最少资源。
读取拖放文件
组合使用 HTML5 拖放 API 与 File API 可以创建读取文件信息的有趣功能。在页面上创建放置目标 后,可以从桌面上把文件拖动并放到放置目标。这样会像拖放图片或链接一样触发 drop 事件。被放置 的文件可以通过事件的 event.dataTransfer.files 属性读到,这个属性保存着一组 File 对象,就 像文本输入字段一样。 下面的例子会把拖放到页面放置目标上的文件信息打印出来:
let droptarget = document.getElementById("file-input");
function handleEvent(event) {
let info = "",
output = document.getElementById("output"),
files, i, len;
event.preventDefault();
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;
}
}
droptarget.addEventListener("dragenter", handleEvent);
droptarget.addEventListener("dragover", handleEvent);
droptarget.addEventListener("drop", handleEvent);
与后面要介绍的拖放的例子一样,必须取消 dragenter、dragover 和 drop 的默认行为。在 drop 事件处理程序中,可以通过 event.dataTransfer.files 读到文件,此时可以获取文件的相关 信息。
自定义媒体播放器
<div class="mediaplayer">
<div class="video">
<video id="player" src="./media/movice.mp4" poster="./media/movice.png" width="300" height="200">
Video player not available.
</video>
</div>
<div class="controls">
<input type="button" value="Play" id="video-btn">
<span id="curtime">0</span>/<span id="duration">0</span>
</div>
</div>
<script>
// 取得元素的引用
let player = document.getElementById("player"),
btn = document.getElementById("video-btn"),
curtime = document.getElementById("curtime"),
duration = document.getElementById("duration");
// 更新时长
duration.innerHTML = player.duration;
console.log(player);
// 为按钮添加事件处理程序
btn.addEventListener("click", (event) => {
if (player.paused) {
player.play();
btn.value = "Pause";
} else {
player.pause();
btn.value = "Play";
}
});
// 周期性更新当前时间
setInterval(() => {
curtime.innerHTML = player.currentTime;
}, 250);
</script>
HTML 模板
DocumentFragment
<template id="foo">
#document-fragment
<p>I'm inside a template!</p>
</template>
<script>
const fragment = document.querySelector('#foo').content;
console.log(document.querySelector('p')); // null
console.log(fragment.querySelector('p')); // <p>...<p>
DocumentFragment 也是批量向 HTML 中添加元素的高效工具。比如,我们想以最快的方式给某 个 HTML 元素添加多个子元素。如果连续调用 document.appendChild(),则不仅费事,还会导致多 次布局重排。而使用 DocumentFragment 可以一次性添加所有子节点,最多只会有一次布局重排:
//开始状态:
// <div id="foo"></div>
//期待的最终状态:
// <div id="foo">
// <p></p>
// <p></p>
// <p></p>
// </div>
//也可以使用document.createDocumentFragment()
const fragment = new DocumentFragment();
const foo = document.querySelector('#foo');
//为DocumentFragment添加子元素不会导致布局重排
fragment.appendChild(document.createElement('p'));
fragment.appendChild(document.createElement('p'));
fragment.appendChild(document.createElement('p'));
console.log(fragment.children.length); // 3
foo.appendChild(fragment);
console.log(fragment.children.length); // 0
console.log(document.body.innerHTML);
// <div id="foo">
// <p></p>
// <p></p>
// <p></p>
// </div>