Notifications API 有被滥用的可能,因此默认会开启两项安全措施:
页面可以使用全局对象 Notification
向用户请求通知权限。这个对象有一个 requestPemission()
方法,该方法返回一个期约,用户在授权对话框上执行操作后这个期约会解决。granted
值意味着用户明确授权了显示通知的权限。除此之外的其他值意味着显示通知会静默失败。如果用户拒绝授权,这个值就是denied
。一旦拒绝,就无法通过编程方式挽回,因为不可能再触发授权提示。
Notification.requestPermission()
.then((permission) => {
console.log('User responded to permission request:', permission);
});
Notification 构造函数
用于创建和显示通知。其第一个参数为显示的标题,第二个可选参数为options配置项,包括设置通知的主体、图片和振动等。调用这个构造函数返回的 Notification 对象的 close()
方法可以关闭显示的通知。
const n = new Notification('will close in 1000ms!', {
body: 'Body text!',
image: 'path/to/image.png',
vibrate: true
});
setTimeoute(() => n.close(), 1000);
const n = new Notification('foo');
n.onshow = () => console.log('Notification was shown!');
n.onclick = () => console.log('Notification was clicked!');
n.onclose = () => console.log('Notification was closed!');
n.onerror = () => console.log('Notification experienced an error!');
Page Visibility API 旨在为开发者提供页面对用户是否可见的信息。
document.visibilityState
值,有三个hidden
/visible
/prerender
,表示下面 4 种状态之一:
visibilitychange 事件
,该事件会在文档从隐藏变可见(或反之)时触发。
document.hidden
,布尔值,表示页面是否隐藏,是为了向后兼容才继续被浏览器支持的,应该优先使用 document.visibilityState
检测页面可见性。
Streams API
是为了解决一个简单但又基础的问题而生的:Web 应用如何消费有序的小信息块而不是大块信息?主要有两种应用场景:
Streams API 实际是为映射低级 I/O 原语
而设计,包括适当时候对字节流的规范化,定义了三种流:
可读流
,可以通过某个公共接口读取数据块的流。数据在内部从底层源进入流,然后由消费者(consumer)进行处理。可写流
,可以通过某个公共接口写入数据块的流。生产者(producer)将数据写入流,数据在内部传入底层数据槽(sink)。转换流
, 由两种流组成,可写流用于接收数据(可写端),可读流用于输出数据(可读端)。这两个流之间是转换程序(transformer),可以根据需要检查和修改流内容。流的基本单位是块(chunk)
。块可是任意数据类型,但通常是定型数组。
所有流都会为已进入流但尚未离开流的块提供一个内部队列
。
如果块入列速度快于出列速度,则内部队列会不断增大。流不能允许其内部队列无限增大,因此它会使用反压(backpressure)
通知流入口停止发送数据,直到队列大小降到某个既定的阈值之下。这个阈值由排列策略决定,这个策略定义了内部队列可以占用的最大内存,即高水位线(high water mark)
。
ReadableStreamDefaultController
生成器的值可以通过可读流的控制器传入可读流。访问这个控制器最简单的方式就是创建ReadableStream
的一个实例,并在这个构造函数的 underlyingSource 参数(第一个参数)中定义start()
方法,然后在这个方法中使用作为参数传入的 controller。默认情况下,这个控制器参数是ReadableStreamDefaultController
的一个实例。调用控制器的 enqueue()
方法可以把值传入控制器。所有值都传完之后,调用 close()
关闭流。
async function* ints() {
// 每 1000 毫秒生成一个递增的整数
for (let i = 0; i < 5; ++i) {
yield await new Promise((resolve) => setTimeout(resolve, 1000, i));
}
}
const readableStream = new ReadableStream({
async start(controller) {
for await (let chunk of ints()) {
controller.enqueue(chunk);
}
controller.close();
}
});
ReadableStreamDefaultReader
ReadableStreamDefaultReader 实例
可以通过流的 getReader()
方法获取,调用这个方法会获得流的锁,保证只有这个读取器可以从流中读取值,消费者使用这个读取器实例的 read()
方法可以读出值
在传给 WritableStream 构造函数
的 underlyingSink
参数中,通过实现 write()
方法可以获得写入的数据:
const readableStream = new ReadableStream({
write(value) {
console.log(value);
}
});
要把获得的数据写入流,可以通过流的 getWriter()
方法获取WritableStreamDefaultWriter
的实例。这样会获得流的锁,确保只有一个写入器可以向流中写入数据
转换流用于组合可读流和可写流。数据块在两个流之间的转换是通过 transform()
方法完成的
async function* ints() {
// 每 1000 毫秒生成一个递增的整数
for (let i = 0; i < 5; ++i) {
yield await new Promise((resolve) => setTimeout(resolve, 1000, i));
}
}
const { writable, readable } = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk * 2);
}
});
最常见的用例是使用 pipeThrough()
方法把ReadableStream
接入TransformStream
。从内部看,ReadableStream 先把自己的值传给TransformStream 内部的WritableStream,然后执行转换,接着转换后的值又在新的 ReadableStream 上出现。
Performance
接口通过 JavaScript API 暴露了浏览器内部的度量指标,允许开发者直接访问这些信息并基于这些信息实现自己想要的功能。这个接口暴露在window.performance
对象上。
High Resolution Time API
定义了window.performance.now()
,这个方法返回一个微秒精度的浮点值。
performance.now()
计时器采用相对度量。这个计时器在执行上下文创建时从 0 开始计时。
performance.timeOrigin 属性
返回计时器初始化时全局系统时钟的值。
Performance Timeline API
使用一套用于度量客户端延迟的工具扩展了 Performance 接口。性能度量将会采用计算结束与开始时间差的形式。这些开始和结束时间会被记录为 DOMHighResTimeStamp值
,而封装这个时间戳的对象是 PerformanceEntry
的实例。
浏览器会自动记录各种 PerformanceEntry 对象,而使用 performance.mark()
也可以记录自定义的 PerformanceEntry
对象。在一个执行上下文中被记录的所有性能条目可以通过 performance.getEntries()
获取。
这个返回的集合代表浏览器的性能时间线(performance timeline)。每个PerformanceEntry
对象都有 name
、entryType
、startTime
和 duration
属性
User Timing API
,用于记录和分析自定义性能条目Navigation Timing API
, 提供了高精度时间戳,用于度量当前页面加载速度。Resource Timing API
, 提供了高精度时间戳,用于度量当前页面加载时请求资源的速度一套用于增强 DOM 行为的工具,包括影子 DOM
、自定义元素
和 HTML 模板
.
提前在页面中写出特殊标记,让浏览器自动将其解析为 DOM 子树,但跳过渲染。这正是 HTML 模板的核心思想,而标签
正是为这个目的而生的。
I'm inside a template!
在浏览器中通过开发者工具检查网页内容时,可以看到中的DocumentFragment:
#document-fragment
I'm inside a template!
通过元素的
content 属性
可以取得这个 DocumentFragment 的引用:
console.log(document.querySelector('#foo').content); // #document-fragment
const fragment = document.querySelector('#foo').content;
console.log(document.querySelector('p')); // null
console.log(fragment.querySelector('p')); // ...
影子 DOM(shadow DOM) Web 组件相当直观,通过它可以将一个完整的 DOM 树作为节点添加到父 DOM 树。这样可以实现 DOM 封装,意味着 CSS 样式和 CSS 选择符可以限制在影子 DOM子树而不是整个顶级 DOM 树中。
影子 DOM 与 HTML 模板很相似,因为它们都是类似 document 的结构,并允许与顶级 DOM 有一定程度的分离。不过,影子 DOM 与 HTML 模板还是有区别的,主要表现在影子 DOM 的内容会实际渲染到页面上,而 HTML 模板的内容不会。
考虑到安全及避免影子 DOM 冲突,并非所有元素都可以包含影子 DOM。能够包含影子DOM的元素:
影子 DOM 是通过attachShadow()
方法创建并添加给有效 HTML 元素的。容纳影子 DOM 的元素被称为影子宿主(shadow host)
。影子 DOM 的根节点被称为影子根(shadow root)
影子 DOM
一添加到元素中,浏览器就会赋予它最高优先级,优先渲染它的内容而不是原来的文本。为了显示文本内容,需要使用
标签指示浏览器在哪里放置原来的 HTML。
如果影子 DOM 中发生了浏览器事件(如 click),那么浏览器需要一种方式以让父 DOM 处理事件。不过,实现也必须考虑影子 DOM 的边界。为此,事件会逃出影子 DOM 并经过事件重定向(event retarget)在外部被处理。
5.3 自定义元素
浏览器会尝试将无法识别的元素作为通用元素整合进 DOM。
自定义元素要使用全局属性 customElements
,这个属性会返回 CustomElementRegistry
对象,调用customElements.define()
方法可以创建自定义元素。
因为每次将自定义元素添加到 DOM 中都会调用其类构造函数,所以很容易自动给自定义元素添加子 DOM 内容。虽然不能在构造函数中添加子 DOM(会抛出 DOMException),但可以为自定义元素添加影子 DOM 并将内容添加到这个影子 DOM 中
自定义元素有以下 5 个生命周期方法。
- constructor():在创建元素实例或将已有 DOM 元素升级为自定义元素时调用。
- connectedCallback():在每次将这个自定义元素实例添加到 DOM 中时调用。
- disconnectedCallback():在每次将这个自定义元素实例从 DOM 中移除时调用。
- attributeChangedCallback():在每次可观察属性的值发生变化时调用。在元素实例初始化时,初始值的定义也算一次变化。
- adoptedCallback():在通过 document.adoptNode()将这个自定义元素实例移动到新文档对象时调用。
6. Web Cryptography API
Web Cryptography API 描述了一套密码学工具,规范了 JavaScript 如何以安全和符合惯例的方式实现加密。这些工具包括生成、使用和应用加密密钥对,加密和解密消息,以及可靠地生成随机数。
6.1 生成随机数
Web Cryptography API 引入了 CSPRNG,这个 CSPRNG 可以通过crypto.getRandomValues()
在全局 Crypto 对象上访问。与 Math.random()
返回一个介于 0和 1之间的浮点数不同,getRandomValues()
会把随机值写入作为参数传给它的定型数组。定型数组的类不重要,因为底层缓冲区会被随机的二进制位填充。
const array = new Uint8Array(1);
for (let i=0; i<5; ++i) {
console.log(crypto.getRandomValues(array));
}
6.2 使用 SubtleCrypto 对象
Web Cryptography API
重头特性都暴露在了 SubtleCrypto
对象上,可以通过 window.crypto.subtle
访问:
console.log(crypto.subtle); // SubtleCrypto {}
这个对象包含一组方法,用于执行常见的密码学功能,如加密、散列、签名和生成密钥。因为所有密码学操作都在原始二进制数据上执行,所以SubtleCrypto
的每个方法都要用到 ArrayBuffer
和ArrayBufferView
类型。由于字符串是密码学操作的重要应用场景,因此 TextEncoder
和TextDecoder
是经常与 SubtleCrypto
一起使用的类,用于实现二进制数据与字符串之间的相互转换。