当前位置: 首页 > 工具软件 > OpenPGP.js > 使用案例 >

JavaScript API学习笔记

曾歌者
2023-12-01

JavaScript API

Web API数量之多令人难以置信,这里我们了解开发中常用的一些

Atomics与SharedArrayBuffer

多个上下文访问SharedArrayBuffer时,如果同时对缓冲区进行操作,可能出现资源争用问题

Atomic API通过强制同一时刻只能对缓冲区执行一个操作,让多个上下文安全地读写一个SharedArrayBuffer;Atomic API是ES2017中定义(类似于操作系统的原语)

SharedArrayBuffer

SharedArrayBuffer与ArrayBuffer具有相同的API,二者区别是ArrayBuffer必须在不同执行上下文切换,SharedArrayBuffer则可以被任意多个执行上下文同时使用

SharedArrayBuffer API和ArrayBuffer API相同,后者详情请看红宝书第六章,前者详情请看红宝书27章

原子操作基础

任何全局上下文都有Atomic对象,这个对象暴露了一系列静态方法用于执行线程安全操作

其中多数方法以一个TypedArray实例(一个SharedArrayBuffer的引用)作为第一个参数,以相关操作数作为后续参数

1、算数及位操作方法

Atomic API提供了一套简单的方法执行就地修改操作,在ECMA规范中,这些方法被定义为AtomicReadModifyWrite操作

在底层,这些方法会从SharedArrayBuffer中某个位置读取值,然后执行算数或位操作,然后把计算结果写回相同位置;这一套方法会按顺序执行,不会被其他线程阻断

算数方法:

//创建大小为1的缓冲区
let sab = new SharedArrayBuffer(1);
//基于缓冲区创建Uint8Array
let ta = new Uint8Array(sab);
//所有ArrayBuffer初始化为0

const index = 0;
const increment = 5;

//对索引0处的值执行原子加5
Atomics.add(ta, index, increment);	//5
//减5
Atomics.sub(ta, index, increment);	//0

位方法:

//创建缓冲区、Uint8Array,初始化为0

const index = 0;

//对索引0处的值执行原子或0b1111
Atomics.or(ta, index, 0b1111);	//15
//原子与0b1111
Atomics.and(ta, index, 0b1111);	//12
//异或0b1111
Atomics.xor(ta, index, 0b1111);	//3
2、原子读写

浏览器的js编译器和CPU架构本身都有权限重排指令以提升程序执行效率;正常情况下js单线程环境可以随时进行这种优化,但多线程下的指令重排可能导致资源争用,极难排错

Atomics API通过两种方式解决了这个问题:

​ 所有原子指令相互间永远不会重排

​ 使用原子读或原子写保证所有指令都不会对原子读写重新排序

Atomics.load()和Atomics.store()还可以构建“代码围栏”

3、原子交换

为保证连续、不间断的先后读写,Atomics API提供了两种方法:exchange()和compareExchange(),前者执行简单的交换,以保证其它线程不会中断值的交换;后者可以对自己想要的操作进行匹配,如果不匹配则不会进行写操作

代码请看红宝书p614

4、原子Futex操作与加锁

Atomics API提供了模仿Linux Futex(快速用户空间互斥量)的方法

所有的Futex操作只能用于Int32Array数组,而且只能用于工作线程内部

Atomics.wait()和Atomics.notify()类似于P和V操作

代码看红宝书615

跨上下文消息

跨文档消息,有时候也简称为XDM,例如在跨源的内嵌窗格中进行页面通信

XDM的核心是postMessage()方法

这个方法接收三个参数:消息、表示目标接收源的字符串和可选的可传输对象的数组;第二个参数对于安全非常重要,可以限制浏览器交付数据的目标,如果想要不限制目标则可以传“*”

接收到XDM消息后,window对象上会触发message事件;这个事件是异步触发的;传给onmessage事件处理程序的event对象包含以下3方面重要信息:

​ data:作为一个参数传递给postMessage()的字符串

​ origin:发送消息的文档源

​ source:发送消息的文档中window对象的代理

通常我们在接收到消息后的onmessage事件处理程序中检查发送窗口的源可以保证数据来自正确的地方

大多数情况下,event.source是某个window对象的代理,而非实际的window对象,因此不能通过它访问所有窗口下的信息,最好只用postMessage()方法

Encoding API

这个API主要用于实现字符串与定型数组之间的转换,有四个用于执行转换的全局类:TextEncoder、TextEncoderStream、TextDecoder和TextDecoderStream

文本编码

Encoding API提供了将字符串转换为定型数组二进制格式的方法:批量编码和流编码;转换时,编码器始终使用UTF-8

1、批量编码

通过TextEncoder实例完成的,实例上有一个encode()方法,接收一个字符串,并以Uint8Array格式返回每个字符UTF-8编码

有些字符(表情)在最终返回数组可能占很多个索引

编码实例还有encodeInto()方法,接收一个字符串和目标Uint8Array,返回一个字典,该字典包含read和written属性,分别从源字符串读取了多少字符和向目标数组写入了多少字符;如果空间不够,则会提前终止

使用其他类型数组会导致encodeInto()抛出错误

2、流编码

TextEncoderStream其实就是TransformStream形式的TextEncoder

相关代码查看红宝书p619

文本编码

同样可以使用批量解码和流解码,但是解码可以指定非常多的字符串编码,默认是UTF-8

在定义TextDecoder时,向构造函数传入解码格式

1、批量解码

TextDecoder实例有一个decode()方法,该方法接收一个定型数组参数,返回解码后的字符串,数组类型没有限制,使用UTF-8格式

得到的字符串填充了空格

2、流解码

TextDecoderStream其实就是TransformStream形式的TextDecoder

相关代码查看红宝书p620

流解码器经常与fetch()一起使用,因为响应体可以作为ReadableStream来处理

File API与Blob API

Web应用程序一个痛点是无法操作用户计算机上的文件;2000年以前,处理文件的唯一方法是把<input type=“file”>放到一个表单里

File API与Blob API是为了让Web开发者能以更加安全的方式访问客户端机器上的文件,从而更好地与这些文件交互而设计的

File类型

File API仍然以表单中文件输入字段为基础,但是增加了直接访问文件信息的能力,HTML5在DOM上为文件输入元素添加了files集合,这个集合会包含一组file对象,表示被选中的文件

每个File对象都有一些只读属性:

​ name:本地系统中的文件名

​ size:以字节计的文字大小

​ type:包含文件MIME类型的字符串

​ lastModifiedDate:表示文件最后修改事件字符串(只有Chrome实现了)

FIle API还提供了FileReader类型,让我们从实际文件中读取数据

FileReader类型

该类型表示异步文件读取机制;可以把它想象成XMLHttpRequest,只不过是变成了从文件系统读取文件

该类提供了几个读取文件数据的方法:

​ readAsText(file, encoding):从文件中读取纯文本内容并保存在result属性中,第二个编码参数是可选的

​ readAsDataURL(file):读取文件并将内容的数据URI保存在result属性中

​ readAsBinaryString(file):读取文件并将每个字符的二进制数据保存在result属性中

​ readAsArrayBuffer(file):读取文件并将文件以ArrayBuffer形式存在result属性中

因为读取方法是异步的,所以每个FileReader会发布几个事件,三个最有用的事件是progress、error、load,表示还有数据、发生错误、读取完成

progress事件50毫秒触发一次,与XHR的progress事件有相同信息:lengthComputable、loaded、total,此外还可以在事件中获取FileReader的result,即使尚未包含全部数据

error会在某种原因无法读取文件时触发,触发事件时,FileReader的error属性会包含错误信息,这个属性是一个对象,包含code属性;code的值可能是:1(未找到文件)、2(安全错误)、3(读取被中断)、4(文件不可读)、5(编码错误)

load事件会在文件成功加载后触发,如果error事件被触发,就不会触发load事件

如果想提前结束文件读取,可以在过程中调用abort()方法,从而触发abort事件

在load、error、abort事件触发后,还会触发loaded,该事件表示在上述3种情况下,所有读取操作都已经结束

FileReaderSync类型

该类型是FileReader的同步版本,这个类型拥有与FileReader相同的方法,只有在整个文件都加载到内存后才继续执行,该类型只在工作线程中可用,因为如果整个文件耗时太长则会影响全局

Blob与部分读取

某些情况下需要读取部分文件,为此,File对象提供了一个slice()方法,接收两个参数:起始字节和要读取的字节数,返回一个Blob实例,而Blob实际上是File的超类

blob表示二进制大对象,是js对不可修改二进制数据的封装类型;包含字符串的数组、ArrayBuffers、ArrayBufferViews,甚至其它Blob都可以用来创建blob;Blob构造函数接收一个options参数,并在其中指定MIME类型

new Blob(['foo']);	//Blob {size: 3, type: ""}new Blob(['{"a": "b"}'], { type: 'application/json' });	//Blob {size: 10, type: "application/json"}

Blob对象有一个size属性和type属性,还有slice()方法用于切分数据,也可以使用FileReader从Blob中读取数据

对象URL与Blob

对象URL有时也称作Blob URL,是指引用储存在File和Blob中数据的URL,对象URL优点是不用将文件内容读取到js也可以使用文件

要创建对象URL,可以使用window.URL.createObjectURL()方法并传入File或Blob对象,这个函数返回一个指向内存中地址的字符串,这个字符串是URL,可以在DOM中直接使用

用完数据后最好能够释放与之相关的内存,如果不想使用某个对象URL,最好把它传给window.URL.revokeObjectURL();当然,页面卸载时,所有对象的URL占用内存都会被释放

读取拖放文件

组合使用HTML5拖放API与FIleAPI可以创建读取文件信息的有趣功能,将文件拖动到页面上创建的放置目标上就能触发drop事件,可以通过event.dataTransfer.files属性读到,这个属性保存着一组File对象

必须取消dragenter、dragover、drop的默认行为

媒体元素

HTML5新增了两个与媒体相关的元素:audio和video

<vedio src="xxx.mpg" id="myVedio">Vedio player not available.</vedio>
<audio src="xx.mp3" id="myAudio">Audio player not available</audio>

每个元素至少要有一个src属性,表示要加载的媒体文件,也可以指定视频播放器的大小:width和height属性,以及视频在加载期间显示图片URI的poster属性;如果controls属性如果存在,则表示浏览器应该显示播放界面,让用户可以直接控制媒体;标签里的内容是在媒体播放器不可用时显式地替代内容

由于浏览器支持的媒体格式不同,所以可以指定多个不同的媒体源;这需要从元素中删除src属性,使用一个或多个source属性代替

<vedio id="myVedio">
    <source src="xxx.webm" type="video/webm; codecs='vp8, vorbis'">
    <source src="xxx.ogv" type="video/ogg; codecs='theora, vorbis'">
    <source src="xxx.mpg">
    Vedio player not available.
</vedio>
<audio id="myAudio">
    <source src="xxx.ogg" type="audio/ogg">
    <source src="xxx.mp3" type="audio/mpeg">
    Audio player not available
</audio>
属性

video和audio元素提供了稳健的js接口,这两个属性有很多属性,可以用于明确媒体的当前状态

相关属性查看红宝书p627,这些属性也可以在元素标签上设定

事件

媒体元素还有很多事件,查看红宝书p628

自定义媒体播放器

audio和video的play()和pause()方法,可以手动控制媒体文件播放

检测编解码器

并不是所有的都支持所有的编解码器,所以我们通常提供多个媒体源;所以js API可以用来检测浏览器是否支持给定格式和编解码器

这两个媒体元素都有一个名为canPlayType()的方法,该方法接收一个格式/编解码器字符串,返回一个字符串值:”probably“、“maybe”、“”,其中空字符串就是假值

只给canPlayType()提供一个MIME类型的情况下,最可能返回的值时“maybe”和空字符串,文件只是一个包装音频和视频数据的容器,而真正决定文件是否可以播放的是编码,在同时提供MIME类型和编解码器的情况下,返回值可能性会提高到“probably”

编解码器必须放到引号中,同时可以在视频元素上使用canPlayType()检测视频格式

音频类型

audio元素有一个Audio的原生js构造函数,支持在任何时候播放音频;类似于Image,但是不需要插入文档即可工作

要使Audio播放音频,只需要创建一个新实例并传入音频文件

let audio = new Audio("xxx.mp3");
EventUtil.addHandler(audio, "canplaythrough", function(event) {
    audio.play();
})

创建Audio实例就会开始下载指定文件,下载完成后可以用play()播放音频

原生拖放

拖放事件

有的事件在被拖放的元素上触发,有的在放置目标上触发;在某个元素被拖动时,会按顺序触发事件:

​ dragstart

​ drag

​ dragend

把元素拖动到一个有效的放置目标上,会依次触发以下事件:

​ dragenter

​ dragover

​ dragleave或drop

dataTransfer对象

除非数据受影响,否则简单的拖放并没有实际意义;event对象中dataTransfer对象用于传递数据,该对象有两个主要方法:getData()和setData()

setData()的第一个参数,getData()的唯一参数是一个字符串,表示要设置的数据类型:允许任何MIME类型,而且会继续支持“text”和“URL”(IE规范“text”或“URL”)

dropEffect与effectAllowed

这两个属性可以确定能够对被拖动元素和放置目标执行什么操作

dropEffect属性高可以告诉浏览器允许哪种放置行为,这个属性有四个可能值:

​ none:被拖动元素不能放到这里,除文本框之外所有元素的默认值

​ move:被拖动元素应该移动到放置目标

​ copy:被拖动元素应该复制到放置目标

​ link:放置目标会导航到被拖动元素(仅在它是URL的情况下)

dropEffect属性必须在ondragenter事件处理程序中使用它

最重要的的一点是effectAllowed,因为没有设置它,dropEffect也没用;该属性表示被拖动元素是否允许dropEffect,该属性有几个值:

​ uninitialized:没有给被拖动元素设置动作

​ none:被拖动元素上没有允许的动作

​ copy:只允许copy这种dropEffect

​ link、move

​ copyLink:允许copy和link两种dropEffect

​ copyMove、linkMove

​ all:允许所有dropEffect

必须在ondragstart事件处理程序中设置

可拖动能力

默认情况下,图片、链接、文本是可拖动的,无需额外代码用户便可以拖动他们,文本只有被选中的时候才能拖动

HTML5在HTML元素上规定了一个draggable属性,表示元素是否可拖动;图片和链接的draggable属性自动被设置为true,其他的元素默认为false

其他成员

HTML5规范为dataTransfer对象定义了如下方法:

​ addElement(elem):为拖动操作添加元素;只传输元素,不会影响拖动操作外观

​ clearData(format):清除以特定格式储存的数据

​ setDragImage(elem, x, y):允许指定拖动发生时显示在光标下面的图片

​ types:当前储存的数据类型列表;这个集合类似数组,以字符串形式保存

Notification API

向用户显示通知,类似于alert()对话框,但是通知提供更灵活的自定义能力

通知权限

为了防止被滥用,默认会开启两项安全措施:

​ 通知只能运行在安全的上下文的代码中被触发

​ 通知必须按照每个源的原则明确得到用户允许

用户授权显示通知是通过浏览器内部的一个对话框完成的,除非用户没有明确给出允许或拒绝的答复,否则这个权限对每个域只会出现一次;浏览器会记住用户的请求,如果拒绝则无法重来

页面可以使用Notification向用户请求通知权限,每个对象都有一个requestPermission()方法,该方法返回一个期约,用户在授权对话框上执行操作后这个期约会被解决

Notification.requestPermission()
			.then((permission) => {
    console.log('user response:', permission);
})

“granted”值代表用户明确授权了显示通知的权限,除此之外的值意味着显示通知会静默失败;如果用户拒绝授权,这个值就是“denied”;一旦拒绝,就无法通过编程的方式挽回,因为不可能再触发授权提示

显示和隐藏通知

Notification构造函数用于创建和显示通知;最简单的通知形式只是显示一个标体,这个标题内容可以作为第一个参数传给Notification构造函数

new Notification('Title text!');
//这样会立即显示通知

//可以通过options参数对通知进行自定义,包括设置通知主体、图片、振动
new Notification('Title text', {
    body: 'body text',
    image: 'path/to/image.png',
    vibrate: true
});

调用构造函数返回的Notification对象close()方法可以返回关闭显示的通知

通知声明周期回调

通知并非只用于显示文本字符串,也可以用于实现交互,有四个用于添加回调的声明周期的方法:

​ onshow:在通知显示时触发

​ onclick:在通知被点击时触发

​ onclose:在通知消失或通过close()关闭时触发

​ onerror:在发生错误时阻止通知显示时触发

Page Visibility API

该API旨在为开发者提供页面对用户是否可见的信息,该API由三部分构成:

​ document.visibilityState值,表示下面4中状态之一:

​ 页面在后台标签页或浏览器中最小化

​ 页面在前台标签页中

​ 实际页面隐藏了,但对页面的预览是可见的

​ 页面在屏外预渲染

​ visibilitychange事件,该事件会在文档从隐藏变可见时触发

​ document.hidden布尔值,表示页面是否隐藏;这个值是为了向后兼容才继续被浏览器支持的

页面的状态,需要监听visibilitychange事件,document.visibilityState的值为下列字符串:hidden、visible、prerender

Stream API

这个API是为了消费有序的小信息而不是大块信息,有两种场景需要使用:

​ 大块数据可能不会一次性都可用;例如网络请求,网络负载是以连续信息包形式交付的,而流式处理可以让应用在数据一达到就能使用,而不必等到所有数据都加载完毕

​ 大块数据可能需要分小部分处理;视频处理、数据压缩、图像编码和JSON解析都是可以分成小部分进行处理,而不必等到所有数据都在内存中时在处理

Stream API则没有那么快得到支持

理解流

这些API实际是为映射低级I/O原语而设计,包括适当时候对字节流的规范化;Stream API直接解决的问题是处理网络请求和读写磁盘

​ 可读流:可以通过某个公共接口读取数据块的流,数据在内部从底层源入流,然后由消费者进行处理

​ 可写流:可以通过某个公共接口写入数据块的流,生产者将数据写入流,数据在内部传入底层数据槽

​ 转换流:有两种流组成,可写流用于接收数据(可写端);可读流用于输出数据(可读端);这两个流之间是转换程序,可以根据需要检查和修改流的内容

块、内部队列和反压

流的基本单位是块;块是可任意数据类型,但通常是定型数组;每个块都是离散的流片段,可以作为一个整体来处理;块不是固定大小,也不一定按固定时间间隔到达;在理想的流当中,块的大小通常近似相同,到达间隔也近似相等;不过好的流实现要考虑边界的情况

前面提到各种类型的流都有入口和出口的概念;有时候由于数据进出速率不同,可能会出现不匹配的情况,所以可能出现三种情形:

​ 流出口处理数据的速度比入口提供数据的速度快;流出口经常空闲,但是只会浪费一点内存或计算机资源,因此这种流的不平衡是可以接受的

​ 流入和流出均衡;这是理想状态

​ 流入口提供数据的速度比出口处理数据的速度快;这种流不平衡是固有的问题,所以会在某个地方出现数据积压,流必须相应做出处理

流不平衡是常见的问题,但流也提供解决这个问题的工具;所有流都会为已进入流但尚未离开流的块提供一个内部队列;对于均衡流,这个内部队列中会有零个或少量排队的块,因为流出口块的出列的速度与流入口块入列的速度近似相等,这种流内部队列所占用的内存相对比较小

如果块入列速度快于出列速度,则内部队列会不断增大;流不能允许其内部队列无线增大,因此它会使用反压通知流入口停止发送数据,直到队列大小降到某个既定阈值之下;这个阈值由排列策略决定,这个策略定义了内部队列可以占用的最大内存,即高水位线

可读流

可读流是对底层数据源的封装;底层数据源可以将数据填充到流中,允许消费者通过流的公共接口读取数据

1、ReadableStreamDefaultController
async function* ints() {    for (let i = 0; i < 5; ++i) {        yield await new Promise((resolve) => setTimeout(resolve, 1000, i));    }}//这个生成器的值可以通过可读流的控制器传入可读流;访问这个控制器最简单的方式是创建ReadableStream的一个实例,并在这个构造函数的underlyingSource参数(第一个参数)中定义start()方法,然后在这个方法中使用作为参数传入的controller;这个控制器参数是ReadableStreamDefaultController的一个实例const readableStream = new ReadableStream({    start(controller) {        console.log(controller);    }});//调用控制器的enqueue()方法可以把值传入控制器,所有值传完后,调用close()关闭流async function* ints() {    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();    }})
2、ReadableStreamDefaultReader

该实例通过流的getReader()方式获取,调用这个方法会获得流的锁,保证只有这个读取器可以从流中读取值:

console.log(readableStream.locked);	//falseconst readableStreamDefaultReader = readableStream.getReader();console.log(readableStream.locked);	//true//消费者(async function() {    while(true) {        const { done, value } = await readableStreamDefaultReader.read();        if (done) {            break;        } else {            console.log(value);        }    }})();//0//1//2//3//4
可写流
1、创建WriteableStream
async function* ints() {    for (let i = 0; i < 5; ++i) {        yield await new Promise((resolve) => setTimeout(resolve, 1000, i));    }}//这些值通过可写流的公共接口可以写入流,再传给WriteableStream构造函数的underlyingSink参数中,通过write()方法可以获得写入的数据const readableStream = new ReadableStream({    write(value) {        console.log(value);    }});
2、WriteableStreamDefaultWriter

可以通过getWriter()方法获取WriteableStreamDefaultWriter的实例;这样可以获得流的锁,确保只有一个写入器的可以向流中写入数据:

console.log(writableStream.locked);	//falseconst writableStreamDefaultWriter = writableStream.getWriter();console.log(writableStream.locked);	//true//生产者(async function() {    for await (let chunk of ints()) {        await writableStreamDefaultWriter.ready;        writableStreamDefaultWriter.write(chunk);    }        writableStreamDefaultWriter.close();})();
转换流

转换流用于组合可读流和可写流;数据块在两个流之间的转换是通过transform()方法完成的

async function* ints() {    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);    }})//消费者(async function() {    while(true) {        const { done, value } = await readableStreamDefaultReader.read();                if (done) {            break;        } else {            console.log(value);        }    }})();//生产者(async function() {    for await (let chunk of ints()) {        await writableStreamDefaultWriter.ready;        writableStreamDefaultWriter.write(chunk);    }        writableStreamDefaultWriter.close();})
通过管道连接流

pipeThrough()方法把ReadableStream接入TransformStream。从内部看,ReadableStream先把自己的值传给TransformStream内部的WritableStream,然后执行转换,接着转换后的值又在新的ReadableStream上出现

const doublingStream = new TransformSteam({    transform(chunk, controller) {        constroller.enqueue(chunk * 2);    }});//通过管道连接流const pipedStream = integerStream.pipeThrough(doublingStream);//从连接流的输出获得读取器const pipedStreamDefaultReader = pipedStream.getReader();//消费者(async function() {    while(true) {        const { done, value } = await pipedStreamDefaultReader.read();                if (done) {            break;        } else {            console.log(value);s        }    }})();//0//2//4//6//8

使用pipeTo()方法也可以将ReadableStream连接到WritableStream

const writableStream = new WritableStream({    write(value) {        console.log(value);    }});const pipedStream = integerStream.pipeTo(writableStream);//0//1//2//3//4

计时API

Performance接口通过Javascript API暴露了浏览器内部的度量指标,允许开发者直接访问这些信息并基于这些信息实现自己想要的功能;所有与页面相关的指标,包括已经定义和将来会定义的,都会存在于这个对象上

Performance接口由多个API构成

High Resolution Time API

Date.now()方法只适用于日期时间相关操作,而且是不要求计时精度的操作;但是有一些方法会导致意外的情况出现(连续执行返回相同值等)

所以出现了window.performance.now(),这个方法返回一个微秒精度的浮点值,连续使用这个方法不可能返回相同值,而且时间单调增长

performance.now()计时器采用相对度量

performance.timeOrigin属性返回计时器初始化时全局系统时钟的值

通过使用performance.now()测量L1缓存与主内存的延迟差,幽灵漏洞(Spectre)可以执行缓存推断攻击;为了弥补这个安全漏洞,所有的主流浏览器选择降低该方法的精度,有的选择在时间戳里混入一些随机性

Performance Timeline API

这是一套用于度量客户端延迟的工具扩展了Performance接口;性能度量将会使用计算结束与开始时间差的形式;这些开始时间和结束时间会被记录为DOMHighResTimeStamp值,而封装这个时间戳的对象是PerformanceEntry的实例

浏览器会自动记录各种PerformanceEntry对象,performance.mark()也可以记录自定义PerformanceEntry对象;一个执行上下文中被记录的所有性能条目可以通过performance.getEntries()获取

返回的集合代表浏览器的性能时间线;每个PerformanceEntry对象都有name、entryType、startTime和duration属性

PerformanceEntry实际上是一个抽象基类,所有记录条目其实都是其他类的具体实现(红宝书p646),相关其他类都有大量属性,用于描述与相应条目有关的元数据

1、User Timing API

这个API用于记录和分析自定义性能条目;如前所述,记录自定义性能条目要使用performance.mark()方法

performance.mark('foo');performance.getEntriesByType('mark')[0];//PerformanceMark {//    name: 'foo',//    entryType: 'mark',//    startTime: 296.0000000....,//    duration: 0//}//计算开始和结束的时间差,最新的标记会被推到getEntriesByType()返回的数组的开始位置performance.mark('foo');//...performance.mark('bar');let marks = performance.getEntriesByType('mark');marks[0].startTime - marks[1].startTime;

可以生成PerformanceMeasure(性能度量)条目,由对应名字作为两个标记之间的持续时间;由performance.measure()方法生成

performance.mark('foo');//...performance.mark('bar');performance.measure('baz', 'foo', 'bar');const dura = performance.getEntriesByType('measure');console.log(dura);/**PerformanceMeasure {	name: 'baz',	entryType: 'measure',	startTime: 298.0000...,	duration: 1.34.....}**/
2、Navigation Timing API

这个API提供了高精度时间戳,用于度量当前页面加载速度;浏览器会在导航事件发生时自动记录PerformanceNavigationTiming条目;这个对象会捕获大量时间戳,用于描述页面是何时以及如何加载的

performance.getEntriesByType('navigation');

3、Resource Timing API

这个API提供了高精度时间戳,用于度量当前页面加载时请求资源的速度,浏览器会在加载资源时自动记录PerformanceResourceTiming;这个对象会捕获大量时间戳,用于描述资源加载速度

performance.getEntriesByType('resource')[0];

Web组件

一套用于增强DOM行为的工具,包括影子DOM、自定义HTML元素和模板;这一套API特别混乱:

​ 没有统一的“Web Component”规范:每个Web组件都在一个不同的规范中定义

​ 有些Web组件如影子DOM和自定义元素,已经出现了向后不兼容的版本问题

​ 浏览器实现极其不一致

由于这些问题,浏览器在使用Web组件通常要引入一个Web组件库,作为腻子脚本,模拟在浏览器钟缺失的Web组件

本章只介绍最新Web组件

HTML模板

Web组件之前,一直缺少基于HTML解析构建DOM子树,然后在需要时再把这个子树渲染出来的机制,一种间接方案是使用innerHTML,但这种方式存在严重的安全隐患;另一种方式是使用document.createElement()构建每个元素,然后添加到孤儿根节点,但是这样很麻烦

更好的方式是在页面中写出特殊标记,让浏览器自动将其解析为DOM子树,但跳过渲染;这是HTML模板的核心思想,而<template>标签正是为这个目标而生的

<template id="foo">    <p>balabala</p></template>
1、使用DocumentFragment

上面的例子不会被渲染,因为template不属于活动文档,DOM查询方法不会发现其钟p标签,因为它存在于一个包含在HTML模板中DocumentFragment节点内

在浏览器中通过开发者工具检查网页内容时,可以看到template中的DocumentFragment

通过template元素的content属性可以取得这个DocumentFragment的引用

document.querySelector('#foo').content;	//document-fragment

此时的DocumentFragment就像一个对应的最小化document对象;换句话说,DocumentFragment上DOM匹配方法可以查询其子树中的节点

const fragment = document.querySelector('#foo').content;document.querySelector('p');	//nullfragment.querySelector('p');	//<p>...</p>

使用DocumentFragment可以一次性添加所有子节点,最多只会触发一次布局重排

2、使用template标签

可以将内容放置到template标签内,然后将其转移到对应功能区

3、模板脚本

添加的内容需要进行某些初始化

以上三者相关案例查阅红宝书p649

影子DOM

通过它可以将一个完整的DOM树作为节点添加到父DOM树

不过影子DOM内容会实际渲染到页面上,而HTML模板不会

1、理解影子DOM

最初使用场景:有一些相同结构的DOM结构需要应用不同CSS样式,需要给每个DOM结构添加唯一的类名

2、创建影子DOM

为了安全和避免影子DOM冲突,不是所有的元素都能包含影子DOM,尝试给无效元素或已经有了影子DOM的元素添加影子DOM会导致抛出错误

以下元素可以容纳影子DOM:任何以有效名称创建的自定义元素、article、aside、blockquote、body、div、footer、h1-h6、header、main、nav、p、section、span

影子DOM是通过attachShadow()方法创建并添加给有效的HTML元素的;容纳影子DOM的元素被称为影子宿主(shadow host);影子DOM的根节点称为影子根(shadow root)

attachShadow()方法需要一个shadowRootInit对象,返回影子DOM的实例;shadowRootInit对象必须包含一个mode属性,值为”open“或”closed“;对“open”影子DOM的引用可以通过shadowRoot属性在HTML元素上获得,对“closed”影子DOM的引用无法这样获取

//foo - HTML元素、bar - HTML元素const openShadowDOM = foo.attachShadow({ mode: 'open' });const closedShadowDOM = bar.attachShadow({ mode: 'closed' });openShadowDOM;	//#shadow-root (open)closedShadowDOM;	//#shadow-root (closed)foo.shadowRoot;	//#shadow-root (open)bar.shadowRoot;	//null

创建保密(closed)影子DOM的场景很少;这虽然可以限制通过宿主访问影子DOM,但是恶意代码有很多方法可以绕过这个限制,恢复对影子DOM的访问;不能为了安全而使用保密影子DOM

如果想保护独立的DOM树不受未信任代码的影响,对iframe施加的跨源限制更加可靠

3、使用影子DOM

把影子DOM添加到元素后,可以像使用常规DOM一样使用影子DOM

示例代码查阅红宝书p653

影子DOM并非铁板一块,HTML元素可以在DOM树间无限值移动

4、合成与影子DOM槽位

影子DOM是为自定义Web组件设计的,为此需要支持嵌套DOM片段

影子DOM一添加到元素中,浏览器就会赋予它最高优先级,优先渲染它的内容而不是原来的文本;为了显示原来的内容需要使用<slot>标签指示浏览器在哪里放置原来的HTML

document.body.innerHTML = `<div id = "foo"><p>Foo</p></div>`;document.querySelector('div').attachShadow({ mode: 'open' }).innerHTML = `<div id = "bar"><slot></slot></div>`//html//<div id="foo">//    #shadow-root (open)//		<div id="bar">//            <p>Foo</p>//		</div>//</div>

虽然检查窗口中看到内容子在影子DOM中,但是实际上这只是DOM内容的投射,实际元素仍然在外部DOM中

除了默认槽位,还可以使用命名槽位实现多个投射,这是通过匹配的slot/name属性实现的;带有slot=“foo”的属性会被投射到带有name=“foo”的slot上

详细代码查阅红宝书p655

5、事件重定向

如果影子中发生浏览器事件(比如click),浏览器需要一种方式让父DOM处理事件;为此事件会逃出影子DOM并经过事件重定向在外部被处理

重定向事件只会发生在影子DOM中实际存在的元素上;使用slot标签从外部投射进来的元素不会发生事件重定向

例子查阅红宝书p657

自定义元素
1、创建自定义元素

浏览器会尝试将无法识别的元素作为通用元素整合进DOM,这些元素不会做任何HTML元素之外的事;这类元素会变成一个HTMLElement实例

可以在自定义标签出现时为它定义复杂的行为,也可以在DOM中将其纳入元素声明周期管理;自定义元素要使用全局属性customElements,这个属性可以返回CustomElementRegistry对象

调用customElements.define()方法可以创建自定义元素,这个元素继承HTMLElement:

class FooElement extends HTMLElement {}customElements.define('x-foo', FooElement);

自定义元素名必须至少包含一个不在头和尾的连字符,而且元素标签不能自关闭

在自定义构造函数中必须先调用super();如果元素继承了HTMLElement或相似类型而不会覆盖构造函数,则没有必要调用super(),因为原型构造函数会默认做这件事

2、添加Web组件内容

每次将自定义元素添加到DOM中都会调用其类构造函数,所以很容易给自定义元素添加子DOM内容;虽然不能在构造函数中添加子DOM(会抛出DOMException),但是可以为自定义元素添加影子DOM,并将内容添加到这个影子DOM中

相关例子在红宝书p659

3、使用自定义元素生命周期方法

可以在自定义元素不同的生命周期执行代码,自定义元素有5个生命周期方法:

​ constructor():在创建元素实例或将已有DOM元素升级为自定义元素时调用

​ connectedCallback():在每次将这个自定义元素实例添加到DOM时调用

​ disconnectedCallback():在每次将这个自定义元素移除的时候调用

​ attributeChangedCallback():在每次可观察属性的值发生变化时调用,在元素实例初始化时,初始值的定义也算一次变化

​ adoptedCallback():在通过document.adoptNode()将这个自定义元素实例移动到新文档对象时调用

4、反射自定义元素属性

自定义元素既是DOM实体又是JavaScript对象,因此两者之间应该同步变化;对DOM的修改应该反映到JavaScript对象,反之亦然

要从JavaScript对象反射到DOM,常见的方式是使用获取函数和设置函数

另一个方向的反射(从DOM到JavaScript)需要给相应的属性添加监听器;可以使用observedAttributes()获取函数让自定义元素的属性值每次改变时都调用attributeChangedCallback()

样例代码请看红宝书p661

5、升级自定义元素

并非始终可以先定义自定义元素,再在DOM中使用相应的元素标签;Web组件在CustomElementRegistry上额外暴露一些方法,可以用来检测自定义元素是否定义完成,然后可以用它来升级已有元素

如果自定义元素已经有定义,那么CustomElementRegistry.get()方法会返回相应的自定义元素的类;CustomElementRegistry.whenDefined()方法会返回一个期约,当自定义元素有定义后解决

customElements.whenDefined('x-foo').then(() => console.log('defined'));customElements.get('x-foo');//undefinedcustomElments.define('x-foo', class {});//defined!customElements.get('x-foo');//class FooElement {}

连接到DOM的元素在自定义元素有定义时会自动升级,如果想在元素连接到DOM之前强制升级,可以使用CustomElementRegistry.upgrade()方法

//在自定义元素有定义之前会创建HTMLUnknownElement对象const fooElement = document.createElement('x-foo');//创建自定义元素class FooElement extends HTMLElement {}customElements.define('x-foo', FooElement);fooElement instanceof FooElement;	//falsecustomElements.upgrade(fooElement);fooElement instanceof FooElement;	//true

Web Cryptography API

该API描述了一套密码学工具,规范了JavaScript如何以安全和符合惯例的方式实现加密;这些工具包括生成、使用、应用加密密钥对,加密和解密消息,以及可靠的生成随机数

加密接口的组织方式有点奇怪,其外部是一个Crypto对象,内部是一个SubtleCrypto对象;在Web Cryptography API标准化之前,window.crypto属性在不同浏览器上实现差异很大;为实现跨浏览器兼容,标准API都暴露在SubtleCrypto对象上

生成随机数

很多人会使用Math.random(),这个方法在浏览器中是以伪随机数生成器(PRNG)方式实现的;所以不是真的随机,PRNG生成的值只是模拟了随机的特性;浏览器PRNG并未使用正真的随机源,只是对一个内部状态应用了固定的算法;每次调用Math.random(),这个内部状态就会被一个算法修改,而结果会被转换为一个新的随机值;V8引擎使用了一个名为xorshift128+的算法

为了解决这个问题,密码学安全伪随机数生成器(CSPRNG)额外增加了一个熵作为输入,例如测试硬件时间或其它无法预计行为的系统特性;这样计算速度会比PRNG慢很多,但是安全系数会高很多,可以用于加密了

CSPRNG可以使用crypto.getRandomValues()在全局crypto对象上访问,传入一个定型数组,这个方法会将这个传入定型数组填满;这个方法最多生成2^16字节,超出则会抛出错误

使用SubtleCrypto对象

Web Cryptography API重要特性都暴露在SubtleCrypto对象上,通过window.crypto.subtle访问

这个对象包含一组方法,用于常见的密码学功能,如加密、散列、签名、生成密钥;因为密码学操作都在原始二进制数据上执行,所以SubtleCrypto的每个方法都要用到ArrayBuffer和ArrayBufferView类型

对于字符串,TextEncoder和TextDecoder是经常与SubtleCrypto一起使用的类,用于实现二进制数据与字符串之间的相互转换

SubtleCrypto只能在安全的上下文(https)中使用

1、生成密码学摘要

计算数据的密码学摘要是非常常用的密码学操作,这个规范支持4种摘要算法:SHA-1和3种SHA-2

​ SHA-1(Secure Hash Algorithm 1):类似MD5的散列函数;接收任意大小的输入。生成160位消息散列;由于容易受到碰撞攻击,这个算法已经不再安全

​ SHA-2:构建于相同耐碰撞单向压缩函数之上的一套散列函数;规范支持其中3种:SHA-256、SHA-384、SHA-512;生成的消息摘要可以是256位、384位、512位;这个算法被认为最安全的,广泛应用于很多领域和协议,包括TLS、PGP和加密货币

SubtleCrypto.digest()方法用于生成消息摘要,要使用的散列算法通过字符串“SHA-1”、“SHA-256”…指定

2、CryptoKey算法

SubtleCrypto对象使用CryptoKey类的实例来生成密钥;CryptoKey支持多种加密算法,允许控制密钥抽取和使用

CryptoKey支持很多种算法,详情查阅红宝书p666

3、生成CryptoKey

使用SubtleCrypto.generateKey()方法可以生成随机CryptoKey,这个方法返回一个期约,解决为一个或多个CryptoKey实例

这个方法接收三个参数:指定目标算法的参数对象、表示密钥是否可以从CryptoKey对象中提取出来的布尔值、表示这个密钥可以与哪个SubtleCrypto方法一起使用的字符串数组(KeyUsages)

由于不同的密码系统需要不同的输入来生成密钥,上述参数对象为每种密码系统规定了必须的输入

​ RSA密码系统使用RsaHashedKeyGenParams对象

​ ECC密码系统使用EcKeyGenParams对象

​ HMAC密码系统使用HmacKeyGenParams对象

​ AES密码系统使用AesKeyGenParams对象

KeyUsages对象用于说明密钥可以与哪一个算法一起使用,至少要包含下列中的一个字符串:encrypt、decrypt、sign、verify、deriveKey、deriveBits、wrapKey、unwrapKey

4、导出和导入密钥

如果密钥是可取的,就可以在CryptoKey对象内部暴露密钥原始的二进制内容,使用exportKey()方法并指定目标格式(“raw”、“pkcs8”、“spki”、“jwk”)就可以取得这个密钥,这个方法返回一个期约,解决后的ArrayBuffer中包含密钥

与exportKey()方法相反的操作要使用importkey()方法实现;这个方法的签名实际上是generateKey()和exportKey()的组合

相关代码实例在红宝书p669

5、从主密钥派生密钥

使用SubtleCrypto对象可以通过可配置的属性从已有密钥获得新密钥;该对象支持一个derivekey()方法和一个deriveBits()方法,前者返回一个解决为CryptoKey的期约,后者返回一个解决为ArrayBuffer的期约

调用deriveKey()实际上与调用deriveBits()之后再把结果传给importkey()相同

deriveBits()方法接收一个算法发参数对象、主密钥、输出的位长度作为参数;当两个人分别拥有自己的密钥对,但希望获得共享的加密密钥时可以使用这个方法

deriveKey()方法是类似的,只不过返回的是CryptoKey实例而不是ArrayBuffer

相关示例可以查阅红宝书p670

6、使用非对称密钥签名和验证消息

通过SubtleCrypto对象可以使用公钥算法用私钥生成签名,或者用公钥验证签名;这两种操作分别通过SubtleCrypto.sign()和SubtleCrypto.verify()方法完成签名消息需要传入参数对象以指定算法和必要的值、CryptoKey、要签名的ArrayBuffer或ArrayBufferView

希望通过这个签名验证消息的人可以使用公钥和SubtleCrypto.verify()方法;这个方法的签名几乎sign()相同,只是必须要提供公钥以及签名

示例查看红宝书p671

7、使用对称密钥加密和解密

SubtleCrypto对象支持使用公钥和对称算法加密和解密信息;这两种操作通过SubtleCrypto.encrypt()和SubtleCrypto.decrypt()方法完成

加密消息需要传入参数对象以指定算法和必要的值、加密密钥和要加密的数据

相关示例查看红宝书p672

8、包装和解包密钥

SubtleCrypto对象支持包装和解包密钥,以便在非信任渠道传输;这两种操作分别通过SubtleCrypto.wrapKey()和SubtleCrypto.unwrapKey()方法完成

包装密钥需要传入一个格式字符串、要包装的CryptoKey实例、要执行包装的CryptoKey、一个参数对象用于指定算法和必要的值

相关示例参阅红宝书p673

 类似资料: