本章内容
Atomics 与 SharedArrayBuffer
跨上下文消息
Encoding API
File API 与 Blob API
拖放
Notifications API
Page Visibility API
Streams API
计时 API
Web 组件
Web Cryptography API
文章目录
1 Atomics 与 SharedArrayBuffer
多个上下文访问 SharedArrayBuffer 时,如果同时对缓冲区执行操作,就可能出现资源争用问题。Atomics API 通过强制同一时刻只能对缓冲区执行一个操作,可以让多个上下文安全地读写一个SharedArrayBuffer。
1.1 SharedArrayBuffer
SharedArrayBuffer 与 ArrayBuffer 具有同样的 API。二者的主要区别是 ArrayBuffer 必须在不同执行上下文间切换,SharedArrayBuffer 则可以被任意多个执行上下文同时使用。
1.2 原子操作基础
任何全局上下文中都有 Atomics 对象,这个对象上暴露了用于执行线程安全操作的一套静态方法,其中多数方法以一个 TypedArray 实例(一个 SharedArrayBuffer 的引用)作为第一个参数,以相关操作数作为后续参数。
1. 算术及位操作方法
Atomics API 提供了一套方法用以执行就地修改操作。这些方法被定义为AtomicReadModifyWrite 操作。在底层,这些方法都会从SharedArrayBuffer 中某个位置读取值,然后执行算术或位操作,最后再把计算结果写回相同的位置。这些操作的原子本质意味着上述读取、修改、写回操作会按照顺序执行,不会被其他线程中断。
以下代码演示了所有算术方法:
// 创建大小为 1 的缓冲区
let sharedArrayBuffer = new SharedArrayBuffer(1);
// 基于缓冲创建 Uint8Array
let typedArray = new Uint8Array(sharedArrayBuffer);
// 所有 ArrayBuffer 全部初始化为 0
console.log(typedArray); // Uint8Array[0]
const index = 0;
const increment = 5;
// 对索引 0 处的值执行原子加 5
Atomics.add(typedArray, index, increment);
console.log(typedArray); // Uint8Array[5]
// 对索引 0 处的值执行原子减 5
Atomics.sub(typedArray, index, increment);
console.log(typedArray); // Uint8Array[0]
以下代码演示了所有位方法:
// 创建大小为 1 的缓冲区
let sharedArrayBuffer = new SharedArrayBuffer(1);
// 基于缓冲创建 Uint8Array
let typedArray = new Uint8Array(sharedArrayBuffer);
// 所有 ArrayBuffer 全部初始化为 0
console.log(typedArray); // Uint8Array[0]
const index = 0;
// 对索引 0 处的值执行原子或 0b1111
Atomics.or(typedArray, index, 0b1111);
console.log(typedArray); // Uint8Array[15]
// 对索引 0 处的值执行原子与 0b1111
Atomics.and(typedArray, index, 0b1100);
console.log(typedArray); // Uint8Array[12]
// 对索引 0 处的值执行原子异或 0b1111
Atomics.xor(typedArray, index, 0b1111);
console.log(typedArray); // Uint8Array[3]
2. 原子读和写
浏览器的 JavaScript 编译器和 CPU 架构本身都有权限重排指令以提升程序执行效率。正常情况下,JavaScript 的单线程环境是可以随时进行这种优化的。但多线程下的指令重排可能导致资源争用,而且极难排错。
Atomics API 通过两种主要方式解决了这个问题。
- 所有原子指令相互之间的顺序永远不会重排。
- 使用原子读或原子写保证所有指令(包括原子和非原子指令)都不会相对原子读/写重新排序。这意味着位于原子读/写之前的所有指令会在原子读/写发生前完成,而位于原子读/写之后的所有 指令会在原子读/写完成后才会开始。
除了读写缓冲区的值,Atomics.load()和 Atomics.store()还可以构建“代码围栏”。JavaScript引擎保证非原子指令可以相对于 load()或 store()本地重排,但这个重排不会侵犯原子读/写的边界。以下代码演示了这种行为:
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
// 执行非原子写
view[0] = 1;
// 非原子写可以保证在这个读操作之前完成,因此这里一定会读到 1
console.log(Atomics.load(view, 0)); // 1
// 执行原子写
Atomics.store(view, 0, 2);
// 非原子读可以保证在原子写完成后发生,因此这里一定会读到 2
console.log(view[0]); // 2
3. 原子交换
为了保证连续、不间断的先读后写, Atomics API 提供了两种方法: exchange() 和compareExchange()。Atomics.exchange()执行简单的交换,以保证其他线程不会中断值的交换:
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
// 在索引 0 处写入 3
Atomics.store(view, 0, 3);
// 从索引 0 处读取值,然后在索引 0 处写入 4
console.log(Atomics.exchange(view, 0, 4)); // 3
// 从索引 0 处读取值
console.log(Atomics.load(view, 0)); // 4
在多线程程序中,一个线程可能只希望在上次读取某个值之后没有其他线程修改该值的情况下才对共享缓冲区执行写操作。如果这个值没有被修改,这个线程就可以安全地写入更新后的值;如果这个值被修改了,那么执行写操作将会破坏其他线程计算的值。对于这种任务Atomics API 提供了 compareExchange()方法。这个方法只在目标索引处的值与预期值匹配时才会执行写操作。来看下面这个例子:
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
// 在索引 0 处写入 5
Atomics.store(view, 0, 5);
// 从缓冲区读取值
let initial = Atomics.load(view, 0);
// 对这个值执行非原子操作
let result = initial ** 2;
// 只在缓冲区未被修改的情况下才会向缓冲区写入新值
Atomics.compareExchange(view, 0, initial, result);
// 检查写入成功
console.log(Atomics.load(view, 0)); // 25
4. 原子 Futex 操作与加锁
如果没有某种锁机制,多线程程序就无法支持复杂需求。为此,Atomics API 提供了模仿 Linux Futex(快速用户空间互斥量,fast user-space mutex)的方法。这些方法本身虽然非常简单,但可以作为更复杂锁机制的基本组件。
注意 所有原子 Futex 操作只能用于 Int32Array 视图。而且,也只能用在工作线程内部。
2 跨上下文消息
跨文档消息,有时候也简称为 XDM(cross-document messaging),是一种在不同执行上下文(如不同工作线程或不同源的页面)间传递信息的能力。例如,www.wrox.com 上的页面想要与包含在内嵌窗格中的 p2p.wrox.com 上面的页面通信。在 XDM 之前,要以安全方式实现这种通信需要很多工作。XDM以安全易用的方式规范化了这个功能。
XDM 的核心是 postMessage()方法。接收 3 个参数:消息、表示目标接收源的字符串和可选的可传输对象的数组(只与工作线程相关)。第二个参数对于安全非常重要,其可以限制浏览器交付数据的目标。下面来看一个例子:
let iframeWindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "http://www.wrox.com");
接收到 XDM 消息后,window 对象上会触发 message 事件。这个事件是异步触发的,因此从消息发出到接收到消息(接收窗口触发 message 事件)可能有延迟。传给 onmessage 事件处理程序的 event对象包含以下 3 方面重要信息。
- data:作为第一个参数传递给 postMessage()的字符串数据。
- origin:发送消息的文档源,例如"http://www.wrox.com"。
- 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");
}
});
3 Encoding API
Encoding API 主要用于实现字符串与定型数组之间的转换。规范新增了 4 个用于执行转换的全局类:TextEncoder、TextEncoderStream、TextDecoder 和 TextDecoderStream。
注意 相比于批量(bulk)的编解码,对流(stream)编解码的支持很有限。
3.1 文本编码
Encoding API 提供了两种将字符串转换为定型数组二进制格式的方法:批量编码和流编码。把字符串转换为定型数组时,编码器始终使用 UTF-8。
1. 批量编码
所谓批量,指的是 JavaScript 引擎会同步编码整个字符串。对于非常长的字符串,可能会花较长时间。批量编码是通过 TextEncoder 的实例完成的:
const textEncoder = new TextEncoder();
这个实例上有一个 encode()方法,该方法接收一个字符串参数,并以 Uint8Array 格式返回每个字符的 UTF-8 编码:
const textEncoder = new TextEncoder();
const decodedText = 'foo';
const encodedText = textEncoder.encode(decodedText);
// f 的 UTF-8 编码是 0x66(即十进制 102)
// o 的 UTF-8 编码是 0x6F(即二进制 111)
console.log(encodedText); // Uint8Array(3) [102, 111, 111]
编码器是用于处理字符的,有些字符(如表情符号)在最终返回的数组中可能会占多个索引:
const textEncoder = new TextEncoder();
const decodedText = '☺';
const encodedText = textEncoder.encode(decodedText);
// ☺的 UTF-8 编码是 0xF0 0x9F 0x98 0x8A(即十进制 240、159、152、138)
console.log(encodedText); // Uint8Array(4) [240, 159, 152, 138]
编码器实例还有一个 encodeInto()方法,该方法接收一个字符串和目标 Unit8Array,返回一个字典,该字典包含 read 和 written 属性,分别表示成功从源字符串读取了多少字符和向目标数组写入了多少字符。如果定型数组的空间不够,编码就会提前终止,返回的字典会体现这个结果:
const textEncoder = new TextEncoder();
const fooArr = new Uint8Array(3);
const barArr = new Uint8Array(2);
const fooResult = textEncoder.encodeInto('foo', fooArr);
const barResult = textEncoder.encodeInto('bar', barArr);
console.log(fooArr); // Uint8Array(3) [102, 111, 111]
console.log(fooResult); // { read: 3, written: 3 }
2. 流编码
TextEncoderStream 其实就是 TransformStream 形式的 TextEncoder。将解码后的文本流通过管道输入流编码器会得到编码后文本块的流。
3.2 文本解码
两种将定型数组转换为字符串的方式:批量解码和流解码。解码器默认字符编码格式是 UTF-8。
1. 批量解码
批量,指的是 JavaScript 引擎会同步解码整个字符串。批量解码是通过 TextDecoder 的实例完成的:
这个实例上有一个 decode()方法,该方法接收一个定型数组参数,返回解码后的字符串:
const textDecoder = new TextDecoder();
// f 的 UTF-8 编码是 0x66(即十进制 102)
// o 的 UTF-8 编码是 0x6F(即二进制 111)
const encodedText = Uint8Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // foo
解码器不关心传入的是哪种定型数组,它只会专心解码整个二进制表示。下面例子解码得到的字符串中填充了空格:
const textDecoder = new TextDecoder();
// f 的 UTF-8 编码是 0x66(即十进制 102)
// o 的 UTF-8 编码是 0x6F(即二进制 111)
const encodedText = Uint32Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // "f o o "
表情符号例子:
const textDecoder = new TextDecoder();
// ☺的 UTF-8 编码是 0xF0 0x9F 0x98 0x8A(即十进制 240、159、152、138)
const encodedText = Uint8Array.of(240, 159, 152, 138);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // ☺
TextEncoder 不同,TextDecoder 可以兼容很多字符编码。比如下面的例子就使用了 UTF-16而非默认的 UTF-8:
const textDecoder = new TextDecoder('utf-16');
// f 的 UTF-8 编码是 0x0066(即十进制 102)
// o 的 UTF-8 编码是 0x006F(即二进制 111)
const encodedText = Uint16Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // foo
2. 流解码
TextDecoderStream 其实就是 TransformStream 形式的 TextDecoder。将编码后的文本流通过管道输入流解码器会得到解码后文本块的流。
4 File API 与 Blob API
File API 与 Blob API 是为了让 Web 开发者能以安全的方式访问客户端机器上的文件。
4.1 File 类型
HTML5 在DOM 上为文件输入元素添加了 files 集合。这个 files集合中会包含一组 File 对象,表示被选中的文件。每个 File 对象都有一些只读属性。
name:本地系统中的文件名。
size:以字节计的文件大小。
type:包含文件 MIME 类型的字符串。
lastModifiedDate:表示文件最后修改时间的字符串。这个属性只有 Chome 实现了。
4.2 FileReader 类型
FileReader类型表示一种异步文件读取机制。可以把FileReader 想象成类似于XMLHttpRequest,只不过是用于从文件系统读取文件,而是从服务器读取数据。FileReader 类型提供了几个读取文件数据的方法。
readAsText(file, encoding):从文件中读取纯文本内容并保存在 result 属性中。第二个参数表示编码,是可选的。
readAsDataURL(file):读取文件并将内容的数据 URI 保存在 result 属性中。
readAsBinaryString(file):读取文件并将每个字符的二进制数据保存在 result 属性中。
readAsArrayBuffer(file):读取文件并将文件内容以 ArrayBuffer 形式保存在 result 属性。
因为这些读取方法是异步的,所以每个 FileReader 会发布几个事件,其中 3 个最有用的事件是progress、error 和 load,分别表示还有更多数据、发生了错误和读取完成。
触发 error 事件时,FileReader 的 error属性会包含错误信息。这个属性是一个对象,只包含一个属性:code。这个错误码的值可能是 1(未找到文件)、2(安全错误)、3(读取被中断)、4(文件不可读)或 5(编码错误)。
4.3 FileReaderSync 类型
FileReaderSync 类型就是 FileReader 的同步版本。只有在整个文件都加载到内存之后才会继续执行。FileReaderSync 只在工作线程中可用,因为如果读取整个文件耗时太长则会影响全局。
4.4 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"}
4.5 对象 URL 与 Blob
对象 URL 有时候也称作 Blob URL,是指引用存储在 File 或 Blob 中数据的 URL。对象 URL 的优点是不用把文件内容读取到 JavaScript 也可以使用文件。只要在适当位置提供对象 URL 即可。
要创建对象 URL,可以使用 window.URL.createObjectURL()方法并传入 File 或 Blob 对象。这个函数返回的值是一个指向内存中地址的字符串。因为这个字符串是 URL,所以可以在 DOM 中直接使用。例如,以下代码使用对象 URL 在页面中显示了一张图片:
let filesList = document.getElementById("files-list");
filesList.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,则可以把它传给 window.URL.revokeObjectURL()。页面卸载时,所有对象 URL 占用的内存都会被释放。不过,最好在不使用时就立即释放内存,以便尽可能保持页面占用最少资源。
4.6 读取拖放文件
组合使用 HTML5 拖放 API 与 File API 可以创建读取文件信息的有趣功能。在页面上创建放置目标后,可以从桌面上把文件拖动并放到放置目标。这样会像拖放图片或链接一样触发 drop 事件。被放置的文件可以通过事件的 event.dataTransfer.files 属性读到,这个属性保存着一组 File 对象,就像文本输入字段一样。
5 媒体元素
<!-- 嵌入视频 -->
<video src="conference.mpg" id="myVideo">Video player not available.</video>
<!-- 嵌入音频 -->
<audio src="song.mp3" id="myAudio">Audio player not available.</audio>
由于浏览器支持的媒体格式不同,因此可以指定多个不同的媒体源。
<!-- 嵌入视频 -->
<video id="myVideo">
<source src="conference.webm" type="video/webm; codecs='vp8, vorbis'">
<source src="conference.ogv" type="video/ogg; codecs='theora, vorbis'">
<source src="conference.mpg">
Video player not available.
</video>
<!-- 嵌入音频 -->
<audio id="myAudio">
<source src="song.ogg" type="audio/ogg">
<source src="song.mp3" type="audio/mpeg">
Audio player not available.
</audio>
5.1 属性
属性 | 数据类型 | 说明 |
---|---|---|
autoplay | Boolean | 取得或设置 autoplay 标签 |
buffered | TimeRanges | 对象,表示已下载缓冲的时间范围 |
省略 |
5.2 事件
事件 | 何时触发 |
---|---|
abort | 下载被中断 |
canplay | 回放可以开始,readyState 为 2 |
省略 |
5.3 自定义媒体播放器
使用audio和video的 play()和 pause()方法,可以手动控制媒体文件的播放。综合使用属性、事件和这些方法,可以方便地创建自定义的媒体播放器。
5.4 检测编解码器
// 提供一个 MIME 类型的情况下,很可能是"maybe"
if (audio.canPlayType("audio/mpeg")) {
// 执行某些操作
}
// 同时提供 MIME 类型和编解码器的情况,可能是"probably"
if (audio.canPlayType("audio/ogg; codecs=\"vorbis\"")) {
// 执行某些操作
}
5.5 音频类型
let audio = new Audio("sound.mp3");
EventUtil.addHandler(audio, "canplaythrough", function(event) {
audio.play();
});
6 原生拖放
6.1 拖放事件
在某个元素被拖动时,会(按顺序)触发以下事件:
(1) dragstart
(2) drag
(3) dragend
在把元素拖动到一个有效的放置目标上时,会依次触发以下事件:
(1) dragenter
(2) dragover
(3) dragleave 或 drop
6.2 自定义放置目标
通过覆盖 dragenter 和 dragover 事件的默认行为,可以把任何元素转换为有效的放置目标。例如,如果有一个 ID 为"droptarget"的div元素,那么可以使用以下代码把它转换成一个放置目标:
let droptarget = document.getElementById("droptarget");
droptarget.addEventListener("dragover", (event) => {
event.preventDefault();
});
droptarget.addEventListener("dragenter", (event) => {
event.preventDefault();
});
6.3 dataTransfer 对象
为实现拖动操作中的数据传输,IE5 在 event对象上暴露了 dataTransfer 对象,用于从被拖动元素向放置目标传递字符串数据。dataTransfer 对象有两个主要方法:getData()和 setData()。
// 传递文本
event.dataTransfer.setData("text", "some text");
let text = event.dataTransfer.getData("text");
// 传递 URL
event.dataTransfer.setData("URL", "http://www.wrox.com/");
let url = event.dataTransfer.getData("URL");
6.4 dropEffect 与 effectAllowed
用于确定能够对被拖动元素和放置目标执行什么操作。为此,可以使用两个属性:dropEffect 与 effectAllowed。
dropEffect 属性可以告诉浏览器允许哪种放置行为。这个属性有以下 4 种可能的值。
“none”:被拖动元素不能放到这里。这是除文本框之外所有元素的默认值。
“move”:被拖动元素应该移动到放置目标。
“copy”:被拖动元素应该复制到放置目标。
“link”:表示放置目标会导航到被拖动元素(仅在它是 URL 的情况下)。
除非同时设置 effectAllowed,否则 dropEffect 属性也没有用。effectAllowed 属性表示对被拖动元素是否允许 dropEffect。这个属性有如下几个可能的值。
“uninitialized”:没有给被拖动元素设置动作。
“none”:被拖动元素上没有允许的操作。
“copy”:只允许"copy"这种 dropEffect。
“link”:只允许"link"这种 dropEffect。
“move”:只允许"move"这种 dropEffect。
“copyLink”:允许"copy"和"link"两种 dropEffect。
“copyMove”:允许"copy"和"move"两种 dropEffect。
“linkMove”:允许"link"和"move"两种 dropEffect。
“all”:允许所有 dropEffect
6.5 可拖动能力
我们也可以让其他元素变得可以拖动。HTML5 在所有 HTML 元素上规定了一个 draggable 属性,表示元素是否可以拖动。图片和链接的 draggable 属性自动被设置为 true,而其他所有元素此属性的默认值为 false。如果想让其他元素可拖动,或者不允许图片和链接被拖动,都可以设置这个属性。
<!-- 禁止拖动图片 -->
<img src="smile.gif" draggable="false" alt="Smiley face">
<!-- 让元素可以拖动 -->
<div draggable="true">...</div>
6.6 其他成员
HTML5 规范还为 dataTransfer 对象定义了下列方法。
addElement(element):为拖动操作添加元素。这纯粹是为了传输数据,不会影响拖动操作的外观。
clearData(format):清除以特定格式存储的数据。
setDragImage(element, x, y):允许指定拖动发生时显示在光标下面的图片。这个方法接收 3 个参数:要显示的 HTML 元素及标识光标位置的图片上的 x 和 y 坐标。这里的 HTML 元素可以是一张图片,此时显示图片;也可以是其他任何元素,此时显示渲染后的元素。
types:当前存储的数据类型列表。这个集合类似数组,以字符串形式保存数据类型,比如"text"。
7 Notifications API
Notifications API 用于向用户显示通知。无论从哪个角度看,这里的通知都很类似 alert()对话框。Notifications API 在 Service Worker 中非常有用。渐进 Web 应用(PWA,Progressive Web Application)通过触发通知可以在页面不活跃时向用户显示消息,看起来就像原生应用。
7.1 通知权限
默认会开启两项安全措施:
通知只能在运行在安全上下文的代码中被触发;
通知必须按照每个源的原则明确得到用户允许。
页面可以使用全局对象 Notification 向用户请求通知权限。这个对象有一个 requestPemission()方法,该方法返回一个期约,用户在授权对话框上执行操作后这个期约会解决。
Notification.requestPermission()
.then((permission) => {
console.log('User responded to permission request:', permission);
});
“granted"值意味着用户明确授权了显示通知的权限。除此之外的其他值意味着显示通知会静默失败。如果用户拒绝授权,这个值就是"denied”。一旦拒绝,就无法通过编程方式挽回,因为不可能再触发授权提示
7.2 显示和隐藏通知
Notification 构造函数用于创建和显示通知。
new Notification('Title text!');
可以通过 options 参数对通知进行自定义,包括设置通知的主体、图片和振动等:
new Notification('Title text!', {
body: 'Body text!',
image: 'path/to/image.png',
vibrate: true
});
关闭显示的通知:
const n = new Notification('I will close in 1000ms');
setTimeout(() => n.close(), 1000);
7.3 通知生命周期回调
通知并非只用于显示文本字符串,也可用于实现交互。Notifications API 提供了 4 个用于添加回调的生命周期方法:
onshow 在通知显示时触发;
onclick 在通知被点击时触发;
onclose 在通知消失或通过 close()关闭时触发;
onerror 在发生错误阻止通知显示时触发。
8 Page Visibility API
开发者不知道用户什么时候真正在使用页面。如果页面被最小化或隐藏在其他标签页后面,那么轮询服务器或更新动画等功能可能就没有必要了。Page Visibility API 旨在为开发者提供页面对用户是否可见的信息。
1、document.visibilityState 值,表示下面 4 种状态之一。
页面在后台标签页或浏览器中最小化了。
页面在前台标签页中。
实际页面隐藏了,但对页面的预览是可见的(例如在 Windows 7 上,用户鼠标移到任务栏图标上会显示网页预览)。
页面在屏外预渲染。
2、visibilitychange 事件,该事件会在文档从隐藏变可见(或反之)时触发。
3、document.hidden 布尔值,表示页面是否隐藏。这可能意味着页面在后台标签页或浏览器中被最小化了。这个值是为了向后兼容才继续被浏览器支持的,应该优先使用 document.visibilityState检测页面可见性。
document.visibilityState 的值是以下三个字符串之一:
“hidden”
“visible”
“prerender”
9 Streams API
Streams API 是为了解决一个简单但又基础的问题而生的:Web 应用如何消费有序的小信息块而不是大块信息?这种能力主要有两种应用场景。
大块数据可能不会一次性都可用。网络请求的响应就是一个典型的例子。网络负载是以连续信息包形式交付的,而流式处理可以让应用在数据一到达就能使用,而不必等到所有数据都加载完毕。
大块数据可能需要分小部分处理。视频处理、数据压缩、图像编码和 JSON 解析都是可以分成小部分进行处理,而不必等到所有数据都在内存中时再处理的例子。
9.1 理解流
提到流,可以把数据想像成某种通过管道输送的液体。JavaScript 中的流借用了管道相关的概念,因为原理是相通的。Stream API 直接解决的问题是处理网络请求和读写磁盘。
Stream API 定义了三种流。
可读流:可以通过某个公共接口读取数据块的流。数据在内部从底层源进入流,然后由消费者(consumer)进行处理。
可写流:可以通过某个公共接口写入数据块的流。生产者(producer)将数据写入流,数据在内部传入底层数据槽(sink)。
转换流:由两种流组成,可写流用于接收数据(可写端),可读流用于输出数据(可读端)。这两个流之间是转换程序(transformer),可以根据需要检查和修改流内容。
块、内部队列和反压
流的基本单位是块(chunk)。块可是任意数据类型,但通常是定型数组。块不是固定大小的,也不一定按固定间隔到达。在理想的流当中,块的大小通常近似相同,到达间隔也近似相等。不过好的流实现需要考虑边界情况。
流不平衡是常见问题,但流也提供了解决这个问题的工具。所有流都会为已进入流但尚未离开流的块提供一个内部队列。如果块入列速度快于出列速度,则内部队列会不断增大。流不能允许其内部队列无限增大,因此它会使用反压(backpressure)通知流入口停止发送数据,直到队列大小降到某个既定的阈值之下。这个阈值由排列策略决定,这个策略定义了内部队列可以占用的最大内存,即高水位线(high water mark)。
9.2 可读流
可读流是对底层数据源的封装。底层数据源可以将数据填充到流中,允许消费者通过流的公共接口读取数据。
- ReadableStreamDefaultController
- ReadableStreamDefaultReader
9.3 可写流
可写流是底层数据槽的封装。底层数据槽处理通过流的公共接口写入的数据。
- 创建 WritableStream
- WritableStreamDefaultWriter
9.4 转换流
转换流用于组合可读流和可写流。数据块在两个流之间的转换是通过 transform()方法完成的。
9.5 通过管道连接流
流可以通过管道连接成一串。最常见的用例是使用 pipeThrough()方法把 ReadableStream 接入TransformStream。
10 计时 API
页面性能始终是 Web 开发者关心的话题。Performance 接口通过 JavaScript API 暴露了浏览器内部的度量指标,允许开发者直接访问这些信息并基于这些信息实现自己想要的功能。这个接口暴露在window.performance 对象上。所有与页面相关的指标,包括已经定义和将来会定义的,都会存在于这个对象上。
Performance 接口由多个 API 构成:
High Resolution Time API
Performance Timeline API
Navigation Timing API
User Timing API
Resource Timing API
Paint Timing API
10.1 High Resolution Time API
Date.now()方法只适用于日期时间相关操作,而且是不要求计时精度的操作。
- Date.now()只有毫秒级精度,如果 foo()执行足够快,则两个时间戳的值会相等。
- 如果在执行时,系统时钟被向后或向前调整了(如切换到夏 令时),则捕获的时间戳不会考虑这种情况,因此时间差中会包含这些调整。
window.performance.now(),这个方法返回一个微秒精度的浮点值。因此,使用这个方法先后捕获的时间戳更不可能出现相等的情况。而且这个方法可以保证时间戳单调增长。由于这个计时器在不同上下文中初始化时可能存在时间差,因此不同上下文之间如果没有共享参照点则不可能直接比较 performance.now()。
注意 通过使用 performance.now()测量 L1 缓存与主内存的延迟差,幽灵漏洞(Spectre)可以执行缓存推断攻击。为弥补这个安全漏洞,所有的主流浏览器有的选择降低performance.now()的精度,有的选择在时间戳里混入一些随机性。
10.2 Performance Timeline API
Performance Timeline API 使用一套用于度量客户端延迟的工具扩展了 Performance 接口。性能度量将会采用计算结束与开始时间差的形式。这些开始和结束时间会被记录为 DOMHighResTimeStamp值,而封装这个时间戳的对象是 PerformanceEntry 的实例。
11 Web 组件
11.1 HTML 模板
在 Web 组件之前,一直缺少基于 HTML 解析构建 DOM 子树,然后在需要时再把这个子树渲染出来的机制。好的方式:提前在页面中写出特殊标记,让浏览器自动将其解析为 DOM 子树,但跳过渲染。这正是 HTML 模板的核心思想,而标签正是为这个目的而生的。下面是一个简单的HTML 模板的例子:
<template id="foo">
<p>I'm inside a template!</p>
</template>
1. 使用 DocumentFragment
因为template的内容不属于活动文档,所以 document.querySelector()等 DOM 查询方法不会发现其中的p标签。这是因为p存在于一个包含在 HTML 模板中的 DocumentFragment 节点内。
<template id="foo">
#document-fragment
<p>I'm inside a template!</p>
</template>
通过template元素的 content 属性可以取得这个 DocumentFragment 的引用,DocumentFragment 上的 DOM 匹配方法可以查询其子树中的节点:
const fragment = document.querySelector('#foo').content;
console.log(document.querySelector('p')); // null
console.log(fragment.querySelector('p')); // <p>...<p>
我们想以最快的方式给某个 HTML 元素添加多个子元素。如果连续调用 document.appendChild(),则不仅费事,还会导致多次布局重排。而使用 DocumentFragment 可以一次性添加所有子节点,最多只会有一次布局重排:
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'));
2. 使用template标签
const fooElement = document.querySelector('#foo');
const barTemplate = document.querySelector('#bar');
const barFragment = barTemplate.content;
console.log(document.body.innerHTML);
// <div id="foo">
// </div>
// <template id="bar">
// <p></p>
// <p></p>
// <p></p>
// </template>
3. 模板脚本
脚本执行可以推迟到将 DocumentFragment 的内容实际添加到 DOM 树。下面的例子演示了这个过程:
// 页面 HTML:
//
// <div id="foo"></div>
// <template id="bar">
// <script>console.log('Template script executed');</script>
// </template>
const fooElement = document.querySelector('#foo');
const barTemplate = document.querySelector('#bar');
const barFragment = barTemplate.content;
console.log('About to add template');
fooElement.appendChild(barFragment);
console.log('Added template');
// About to add template
// Template script executed
// Added template
11.2 影子 DOM
概念上讲,影子 DOM(shadow DOM) Web 组件相当直观,通过它可以将一个完整的 DOM 树作为节点添加到父 DOM 树。这样可以实现 DOM 封装,意味着 CSS 样式和 CSS 选择符可以限制在影子 DOM子树而不是整个顶级 DOM 树中。
1. 理解影子 DOM
<div>
<p>Make me red!</p>
</div>
<div>
<p>Make me blue!</p>
</div>
<div>
<p>Make me green!</p>
</div>
给每个p标签单独设置颜色,通常使用独特的class。但这也仅是一个折中的办法而已。理想情况下,应该能够把 CSS 限制在使用它们的 DOM 上:这正是影子 DOM 最初的使用场景。
2. 创建影子 DOM
影子 DOM 是通过 attachShadow()方法创建并添加给有效 HTML 元素的。容纳影子 DOM 的元素被称为影子宿主(shadow host)。影子 DOM 的根节点被称为影子根(shadow root)。
attachShadow()方法需要一个shadowRootInit 对象,返回影子DOM的实例。shadowRootInit对象必须包含一个 mode 属性,值为"open"或"closed"。对"open"影子 DOM的引用可以通过 shadowRoot属性在 HTML 元素上获得,而对"closed"影子 DOM 的引用无法这样获取。
document.body.innerHTML = `
<div id="foo"></div>
<div id="bar"></div>
`;
const foo = document.querySelector('#foo');
const bar = document.querySelector('#bar');
const openShadowDOM = foo.attachShadow({ mode: 'open' });
const closedShadowDOM = bar.attachShadow({ mode: 'closed' });
console.log(openShadowDOM); // #shadow-root (open)
console.log(closedShadowDOM); // #shadow-root (closed)
console.log(foo.shadowRoot); // #shadow-root (open)
console.log(bar.shadowRoot); // null
一般来说,需要创建保密(closed)影子 DOM 的场景很少。虽然这可以限制通过影子宿主访问影子 DOM,但恶意代码有很多方法绕过这个限制,恢复对影子 DOM 的访问。简言之,不能为了安全而创建保密影子 DOM。对iframe施加的跨源限制更可靠。
3. 使用影子 DOM
重新创建了红/绿/蓝子树的示例:
for (let color of ['red', 'green', 'blue']) {
const div = document.createElement('div');
const shadowDOM = div.attachShadow({ mode: 'open' });
document.body.appendChild(div);
shadowDOM.innerHTML = `
<p>Make me ${color}</p>
<style>
p {
color: ${color};
}
</style>
`;
}
4. 合成与影子 DOM 槽位
影子 DOM 是为自定义 Web 组件设计的,为此需要支持嵌套 DOM 片段。
默认情况下,嵌套内容会隐藏。来看下面的例子,其中的文本在 1000 毫秒后会被隐藏:
document.body.innerHTML = `
<div>
<p>Foo</p>
</div>
`;
setTimeout(() => document.querySelector('div').attachShadow({ mode: 'open' }), 1000);
影子 DOM 一添加到元素中,浏览器就会赋予它最高优先级,优先渲染它的内容而不是原来的文本。在这个例子中,由于影子 DOM 是空的,因此div会在 1000 毫秒后变成空的。
为了显示文本内容,需要使用slot标签指示浏览器在哪里放置原来的 HTML。下面的代码修改了前面的例子,让影子宿主中的文本出现在了影子 DOM 中:
document.body.innerHTML = `
<div id="foo">
<p>Foo</p>
</div>
`;
document.querySelector('div')
.attachShadow({ mode: 'open' })
.innerHTML = `<div id="bar">
<slot></slot>
<div>`
注意,虽然在页面检查窗口中看到内容在影子 DOM中,但这实际上只是 DOM内容的投射(projection)。实际的元素仍然处于外部 DOM 中。
除了默认槽位,还可以使用命名槽位(named slot)实现多个投射。这是通过匹配的 slot/name 属性对实现的。带有 slot="foo"属性的元素会被投射到带有 name="foo"的slot上。下面的例子演示了如何改变影子宿主子元素的渲染顺序:
document.body.innerHTML = `
<div>
<p slot="foo">Foo</p>
<p slot="bar">Bar</p>
</div>
`;
document.querySelector('div')
.attachShadow({ mode: 'open' })
.innerHTML = `
<slot name="bar"></slot>
<slot name="foo"></slot>
`;
// Renders:
// Bar
// Foo
5. 事件重定向
如果影子 DOM 中发生了浏览器事件(如 click),那么浏览器需要一种方式以让父 DOM 处理事件。不过,实现也必须考虑影子 DOM 的边界。为此,事件会逃出影子 DOM 并经过事件重定向(event retarget)在外部被处理。逃出后,事件就好像是由影子宿主本身而非真正的包装元素触发的一样。下面的代码演示了这个过程:
// 创建一个元素作为影子宿主
document.body.innerHTML = `
<div οnclick="console.log('Handled outside:', event.target)"></div>
`;
// 添加影子 DOM 并向其中插入 HTML
document.querySelector('div')
.attachShadow({ mode: 'open' })
.innerHTML = `
<button οnclick="console.log('Handled inside:', event.target)">Foo</button>
`;
// 点击按钮时:
// Handled inside: <button οnclick="..."></button>
// Handled outside: <div οnclick="..."></div>
事件重定向只会发生在影子 DOM 中实际存在的元素上。使用标签从外部投射进来的元素不会发生事件重定向。
11.3 自定义元素
1. 创建自定义元素
其中胡乱编的 HTML 标签会变成一个 HTMLElement 实例:
<x-foo >I'm inside a nonsense element.</x-foo >
`;
console.log(document.querySelector('x-foo') instanceof HTMLElement); // true
自定义元素要使用全局属性 customElements,这个属性会返回 CustomElementRegistry 对象:
console.log(customElements); // CustomElementRegistry {}
调用 customElements.define()方法可以创建自定义元素。下面的代码创建了一个简单的自定义元素,这个元素继承 HTMLElement:
class FooElement extends HTMLElement {}
customElements.define('x-foo', FooElement);
document.body.innerHTML = `
<x-foo >I'm inside a nonsense element.</x-foo >
`;
console.log(document.querySelector('x-foo') instanceof FooElement); // true
自定义元素的威力源自类定义。例如,可以通过调用自定义元素的构造函数来控制这个类在 DOM中每个实例的行为:
constructor() {
super();
console.log('x-foo')
}
}
customElements.define('x-foo', FooElement);
document.body.innerHTML = `
<x-foo></x-foo>
<x-foo></x-foo>
<x-foo></x-foo>
`;
// x-foo
// x-foo
// x-foo
注意 在自定义元素的构造函数中必须始终先调用 super()。如果元素继承了 HTMLElement或相似类型而不会覆盖构造函数,则没有必要调用 super(),因为原型构造函数默认会做这件事。很少有创建自定义元素而不继承 HTMLElement 的。
2. 添加 Web 组件内容
因为每次将自定义元素添加到 DOM 中都会调用其类构造函数,所以很容易自动给自定义元素添加子 DOM 内容。虽然不能在构造函数中添加子 DOM(会抛出 DOMException),但可以为自定义元素添加影子 DOM 并将内容添加到这个影子 DOM 中:
//(初始的 HTML)
// <template id="x-foo-tpl">
// <p>I'm inside a custom element template!</p>
// </template>
const template = document.querySelector('#x-foo-tpl');
const template = document.querySelector('#x-foo-tpl');
class FooElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('x-foo', FooElement);
document.body.innerHTML += `<x-foo></x-foo`;
// 结果 DOM:
// <body>
// <template id="x-foo-tpl">
// <p>I'm inside a custom element template!</p>
// </template>
// <x-foo>
// #shadow-root (open)
// <p>I'm inside a custom element template!</p>
// <x-foo>
// </body>
3. 使用自定义元素生命周期方法
可以在自定义元素的不同生命周期执行代码。带有相应名称的自定义元素类的实例方法会在不同生命周期阶段被调用。自定义元素有以下 5 个生命周期方法。
constructor():在创建元素实例或将已有 DOM 元素升级为自定义元素时调用。
connectedCallback():在每次将这个自定义元素实例添加到 DOM 中时调用。
disconnectedCallback():在每次将这个自定义元素实例从 DOM 中移除时调用。
attributeChangedCallback():在每次可观察属性的值发生变化时调用。在元素实例初始化时,初始值的定义也算一次变化。
adoptedCallback():在通过 document.adoptNode()将这个自定义元素实例移动到新文档对象时调用。
4. 反射自定义元素属性
自定义元素既是 DOM 实体又是 JavaScript 对象,因此两者之间应该同步变化。换句话说,对 DOM的修改应该反映到 JavaScript 对象,反之亦然。要从 JavaScript 对象反射到 DOM,常见的方式是使用获取函数和设置函数。下面的例子演示了在 JavaScript 对象和 DOM 之间反射 bar 属性的过程:
document.body.innerHTML = `<x-foo></x-foo>`;
class FooElement extends HTMLElement {
constructor() {
super();
this.bar = true;
}
get bar() {
return this.getAttribute('bar');
}
set bar(value) {
this.setAttribute('bar', value)
}
}
customElements.define('x-foo', FooElement);
console.log(document.body.innerHTML);
// <x-foo bar="true"></x-foo>
另一个方向的反射(从 DOM 到 JavaScript 对象)需要给相应的属性添加监听器。为此,可以使用observedAttributes()获取函数让自定义元素的属性值每次改变时都调用 attributeChangedCallback()。
5. 升级自定义元素
Web 组件在 CustomElementRegistry 上额外暴露了一些方法。这些方法可以用来检测自定义元素是否定义完成,然后可以用它来升级已有元素。
如果自定义元素已经有定义,那么 CustomElementRegistry.get()方法会返回相应自定义元素的类。类似地,CustomElementRegistry.whenDefined()方法会返回一个期约,当相应自定义元素有定义之后解决。
12 Web Cryptography API
Web Cryptography API 描述了一套密码学工具,规范了 JavaScript 如何以安全和符合惯例的方式实现加密。这些工具包括生成、使用和应用加密密钥对,加密和解密消息,以及可靠地生成随机数。
12.1 生成随机数
在需要生成随机值时,很多人会使用 Math.random()。这个方法在浏览器中是以伪随机数生成器(PRNG,PseudoRandom Number Generator)方式实现的。所谓“伪”指的是生成值的过程不是真的随机,只是模拟了随机的特性。浏览器的 PRNG 并未使用真正的随机源,只是对一个内部状态应用了固定的算法。每次调用 Math.random(),这个内部状态都会被一个算法修改,而结果会被转换为一个新的随机值。例如,V8 引擎使用了一个名为 xorshift128+的算法来执行这种修改。
由于算法本身是固定的,其输入只是之前的状态,因此随机数顺序也是确定的。xorshift128+使用128 位内部状态,而算法的设计让任何初始状态在重复自身之前都会产生 2128–1 个伪随机值。这种循环被称为置换循环(permutation cycle),而这个循环的长度被称为一个周期(period)。很明显,如果攻击者知道 PRNG 的内部状态,就可以预测后续生成的伪随机值。如果开发者无意中使用 PRNG 生成了私有密钥用于加密,则攻击者就可以利用 PRNG 的这个特性算出私有密钥。
伪随机数生成器主要用于快速计算出看起来随机的值。不过并不适合用于加密计算。为解决这个问题,密码学安全伪随机数生成器(CSPRNG)额外增加了一个熵作为输入,例如测试硬件时间或其他无法预计行为的系统特性。这样一来,计算速度明显比常规 PRNG 慢很多,但 CSPRNG 生成的值就很难预测,可以用于加密了。
CSPRNG 可以通过 crypto.getRandomValues()在全局 Crypto 对象上访问。Math.random()返回一个介于 0和 1之间的浮点数不同,getRandomValues()会把随机值写入作为参数传给它的定型数组。定型数组的类不重要,因为底层缓冲区会被随机的二进制位填充。
下面的例子展示了生成 5 个 8 位随机值:
const array = new Uint8Array(1);
for (let i=0; i<5; ++i) {
console.log(crypto.getRandomValues(array));
}
// Uint8Array [41]
// Uint8Array [250]
// Uint8Array [51]
// Uint8Array [129]
// Uint8Array [35]
getRandomValues()最多可以生成 216(65 536)字节,超出则会抛出错误:
const fooArray = new Uint8Array(2 ** 16);
console.log(window.crypto.getRandomValues(fooArray)); // Uint32Array(16384) [...]
const barArray = new Uint8Array((2 ** 16) + 1);
console.log(window.crypto.getRandomValues(barArray)); // Error
要使用 CSPRNG 重新实现 Math.random(),可以通过生成一个随机的 32 位数值,然后用它去除最大的可能值 0xFFFFFFFF。这样就会得到一个介于 0 和 1 之间的值:
function randomFloat() {
// 生成 32 位随机值
const fooArray = new Uint32Array(1);
// 最大值是 2^32 –1
const maxUint32 = 0xFFFFFFFF;
// 用最大可能的值来除
return crypto.getRandomValues(fooArray)[0] / maxUint32;
}
console.log(randomFloat()); // 0.5033651619458955
12.2 使用 SubtleCrypto 对象
Web Cryptography API 重头特性都暴露在了 SubtleCrypto 对象上,可以通过 window.crypto. subtle 访问:
console.log(crypto.subtle); // SubtleCrypto {}
这个对象包含一组方法,用于执行常见的密码学功能,如加密、散列、签名和生成密钥。因为所有密码学操作都在原始二进制数据上执行,所以 SubtleCrypto 的每个方法都要用到 ArrayBuffer 和ArrayBufferView 类型。由于字符串是密码学操作的重要应用场景,因此 TextEncoder和TextDecoder 是经常与 SubtleCrypto 一起使用的类,用于实现二进制数据与字符串之间的相互转换。
注意 SubtleCrypto 对象只能在安全上下文(https)中使用。在不安全的上下文中,subtle 属性是 undefined。
1. 生成密码学摘要
计算数据的密码学摘要是非常常用的密码学操作。这个规范支持 4 种摘要算法:SHA-1 和 3 种SHA-2。
SHA-1(Secure Hash Algorithm 1):架构类似 MD5 的散列函数。接收任意大小的输入,生成160 位消息散列。由于容易受到碰撞攻击,这个算法已经不再安全。
SHA-2(Secure Hash Algorithm 2):构建于相同耐碰撞单向压缩函数之上的一套散列函数。规范支持其中 3 种:SHA-256、SHA-384 和 SHA-512。生成的消息摘要可以是 256 位(SHA-256)、384 位(SHA-384)或 512 位(SHA-512)。这个算法被认为是安全的,广泛应用于很多领域和协议,包括 TLS、PGP 和加密货币(如比特币)。
SubtleCrypto.digest()方法用于生成消息摘要。要使用的散列算法通过字符串"SHA-1"、“SHA-256”、"SHA-384"或"SHA-512"指定。下面的代码展示了一个使用 SHA-256 为字符串"foo"生成消息摘要的例子:
(async function() {
const textEncoder = new TextEncoder();
const message = textEncoder.encode('foo');
const messageDigest = await crypto.subtle.digest('SHA-256', message);
console.log(new Uint32Array(messageDigest));
})();
// Uint32Array(8) [1806968364, 2412183400, 1011194873, 876687389,
// 1882014227, 2696905572, 2287897337, 2934400610]
通常,在使用时,二进制的消息摘要会转换为十六进制字符串格式。通过将二进制数据按 8 位进行分割,然后再调用 toString(16)就可以把任何数组缓冲区转换为十六进制字符串:
(async function() {
const textEncoder = new TextEncoder();
const message = textEncoder.encode('foo');
const messageDigest = await crypto.subtle.digest('SHA-256', message);
const hexDigest = Array.from(new Uint8Array(messageDigest))
.map((x) => x.toString(16).padStart(2, '0'))
.join('');
console.log(hexDigest);
})();
// 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae
2. CryptoKey 与算法
SubtleCrypto 对象使用 CryptoKey 类的实例来生成密钥。CryptoKey 类支持多种加密算法,允许控制密钥抽取和使用。
CryptoKey 类支持多种算法,下面是一些:
RSA(Rivest-Shamir-Adleman):公钥密码系统,使用两个大素数获得一对公钥和私钥,可用于签名/验证或加密/解密消息。RSA 的陷门函数被称为分解难题(factoring problem)。
RSA-PSS:RSA 的另一个应用,用于签名和验证消息。
ECC(Elliptic-Curve Cryptography):公钥密码系统,使用一个素数和一个椭圆曲线获得一对公钥和私钥,可用于签名/验证消息。ECC 的陷门函数被称为椭圆曲线离散对数问题(elliptic curve discrete logarithm problem)。ECC 被认为优于 RSA。虽然 RSA 和 ECC 在密码学意义上都很强,但 ECC 密钥比 RSA 密钥短,而且 ECC 密码学操作比 RSA 操作快。
3. 生成 CryptoKey
使用 SubtleCrypto.generateKey()方法可以生成随机 CryptoKey,这个方法返回一个期约,解决为一个或多个 CryptoKey 实例。使用时需要给这个方法传入一个指定目标算法的参数对象、一个表示密钥是否可以从 CryptoKey 对象中提取出来的布尔值,以及一个表示这个密钥可以与哪个SubtleCrypto 方法一起使用的字符串数组(keyUsages)。
4. 导出和导入密钥
如果密钥是可提取的,那么就可以在 CryptoKey 对象内部暴露密钥原始的二进制内容。使用exportKey()方法并指定目标格式(“raw”、“pkcs8”、“spki"或"jwk”)就可以取得密钥。这个方法返回一个期约。
5. 从主密钥派生密钥
使用 SubtleCrypto 对象可以通过可配置的属性从已有密钥获得新密钥。SubtleCrypto 支持一个 deriveKey()方法和一个 deriveBits()方法,前者返回一个解决为 CryptoKey 的期约,后者返回一个解决为 ArrayBuffer 的期约。
6. 使用非对称密钥签名和验证消息
通过 SubtleCrypto 对象可以使用公钥算法用私钥生成签名,或者用公钥验证签名。这两种操作分别通过 SubtleCrypto.sign()和 SubtleCrypto.verify()方法完成。
7. 使用对称密钥加密和解密
SubtleCrypto 对象支持使用公钥和对称算法加密和解密消息。这两种操作分别通过 SubtleCrypto. encrypt()和 SubtleCrypto.decrypt()方法完成。
8. 包装和解包密钥
SubtleCrypto 对象支持包装和解包密钥,以便在非信任渠道传输。这两种操作分别通过 SubtleCrypto.wrapKey()和SubtleCrypto.unwrapKey()方法完成。
13 小结
除了定义新标签,HTML5 还定义了一些 JavaScript API。这些 API 可以为开发者提供更便捷的 Web接口,暴露堪比桌面应用的能力。本章主要介绍了以下 API。
Atomics API 用于保护代码在多线程内存访问模式下不发生资源争用。
postMessage() API 支持从不同源跨文档发送消息,同时保证安全和遵循同源策略。
Encoding API 用于实现字符串与缓冲区之间的无缝转换(越来越常见的操作)。
File API 提供了发送、接收和读取大型二进制对象的可靠工具。
媒体元素和拥有自己的 API,用于操作音频和视频。并不是每个浏览器都会支持所有媒体格式,使用 canPlayType()方法可以检测浏览器支持情况。
拖放 API 支持方便地将元素标识为可拖动,并在操作系统完成放置时给出回应。可以利用它创建自定义可拖动元素和放置目标。
Notifications API 提供了一种浏览器中立的方式,以此向用户展示消通知弹层。
Streams API 支持以全新的方式读取、写入和处理数据。
Timing API 提供了一组度量数据进出浏览器时间的可靠工具。
Web Components API 为元素重用和封装技术向前迈进提供了有力支撑。
Web Cryptography API 让生成随机数、加密和签名消息成为一类特性。