第20章 JavaScript API
js的API非常丰富,但不一定都被浏览器实现了。本节选择Web开发中较常使用的API进行描述。
# File API与Blob API
浏览器可以通过<input type="file">来处理文件。
# File类型
当用户通过input选择一个或多个文件时,事件处理程序将得到files
数组,每个file对象的属性有
- name:本地系统中的文件名。
- size:文件大小,单位是字节。
- type:包含文件MIME类型的字符串。
<input type="file" />
<script>
const input = document.querySelector('input')
input.addEventListener('change', (event) => {
const { files } = event.target
for (let file of files) {
console.log('name:', file.name) // name: JavaScript高级程序设计(第4版).pdf
console.log('size:', file.size) // size: 14354022
console.log('type:', file.type) // type: application/pdf
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
# FileReader类型
FileReader类型表示一种异步文件读取机制。可以从文件系统读取文件,它有几个方法:
- readAsText(file, encoding):从文件中读取纯文本内容并保存在result属性。
- readAsDataURL(file):读取文件并将内容的数据URI保存在result属性中。
- readAsBinaryString(file):读取文件并将每个字符的二进制数据保存在result属性中。
- readAsArrayBuffer(file):读取文件并将文件内容以ArrayBuffer形式保存在result属性。
这几个方法可以用来灵活处理文件数据,比如想要得到图片,可以读取为数据URI;而想要解析文件内容,可以读取为文本。
读取方法是异步的,可以监听到一些有用的事件,如
- progress事件,每50ms触发一次。
- error事件,读取文件发生异常时触发。
- load事件,在文件加载成功后触发。
下面这个demo介绍了这3个事件的使用。
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.progress {
width: 200px;
height: 30px;
border: 1px solid;
background-color: #fff;
}
.bar {
width: 0;
height: 100%;
background-color: lightgreen;
}
</style>
</head>
<body>
<input type="file" />
<div class="progress">
<div class="bar"></div>
</div>
<div class="content"></div>
</body>
</html>
<script>
const input = document.querySelector('input')
const bar = document.querySelector('.bar')
const content = document.querySelector('.content')
input.addEventListener('change', (event) => {
const {
files: [file],
} = event.target
console.log('file', file)
const { size, type } = file
const isImg = /image/.test(type)
console.log(isImg)
let reader = new FileReader()
if (isImg) {
reader.readAsDataURL(file)
} else {
reader.readAsText(file)
}
reader.onerror = (e) => {
console.log('e', e)
console.log('error code', reader.error.code)
}
reader.onprogress = (ev) => {
console.log('progress', ev)
console.log(ev.loaded)
const width = ev.loaded / ev.total
console.log('width', width * 100 + '%')
bar.style.width = width * 100 + '%'
}
reader.onload = (ev) => {
console.log('onload', ev)
let html = ''
if (isImg) {
html = `<img src="${reader.result}" />`
} else {
html = `<p>${reader.result}}</p>`
}
content.innerHTML = html
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# FileReaderSync类型
是FileReader的同步版本。
# Blob数据
Blob是File的超类。它表示二进制大对象,不可修改。常用于数据切分和转化。
file对象继承了blob对象的slice方法,可以对数据进行切分。
注意红宝书此处有误(blobSlice),参考 (opens new window)。
<input type="file" />
<div class="content"></div>
<script>
const input = document.querySelector('input')
const content = document.querySelector('.content')
input.addEventListener('change', (event) => {
const {
files: [file],
} = event.target
console.log('file', file)
const { size, type } = file
// 切分前32字节
const reader = new FileReader()
const blobSlice = file.slice(0, 32) // 此处红宝书上有误,已修改
if (blobSlice) {
reader.readAsText(blobSlice)
reader.onerror = () => {
console.log('error')
}
reader.onload = () => {
console.log('load', reader.result)
content.innerHTML = reader.result
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 对象URL与Blob
对象URL即Blob URL,是指引用存储在File或Blob中数据的URL。
它的优点在于,只需要提供对象URL,那么就可以在任何js中进行使用,而不需要每次都读取文件。
创建对象URL:使用window.URL.createObjectURL
,传入File或Blob对象,返回一个指向内存地址的字符串。这个内存地址可以直接在DOM中使用。
如果不想使用URL了,可以调用window.URL.revokeObjectURL()
释放内存。
<input type="file" />
<div class="content"></div>
<script>
const input = document.querySelector('input')
const content = document.querySelector('.content')
input.addEventListener('change', (event) => {
const {
files: [file],
} = event.target
console.log('file', file)
const { size, type } = file
const isImg = /image/.test(type)
console.log(isImg)
if (isImg) {
const url = window.URL.createObjectURL(file)
const img = document.createElement('img')
img.src = URL.createObjectURL(file)
img.height = 60
img.onload = function () {
URL.revokeObjectURL(this.src)
}
// 注意:直接使用innerHTML不会生效
// content.innerHTML = `<img src="${url}}" />`
content.appendChild(img)
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 媒体元素
HTML5新增了<audio>和<video>标签来支持音频和视频。
此外还可以直接使用Audio构造函数,这样就不用插入DOM即可调用。
不过,由于h5兼容性,需要用户交互(点击)后才能播放。
<body>
<button class="play">play</button>
<button class="pause">pause</button>
</body>
<script>
let audio = new Audio('https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-hello-uniapp/2cc220e0-c27a-11ea-9dfb-6da8e309e0d8.mp3')
const playBtn = document.querySelector('.play')
const pauseBtn = document.querySelector('.pause')
playBtn.onclick = function(){
audio.play()
}
pauseBtn.onclick = function(){
audio.pause()
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 原生拖放
拖放过程中有2类元素:被拖放元素和放置目标。
# 拖放事件
某个元素被拖动时,会触发下面的事件:
- dragstart,按住鼠标并开始拖动的那一刻触发。
- drag,只要目标还在被拖动就会持续触发,类似于mousemove。
- dragend,当拖动停止时,会触发该事件。
把元素拖动到一个有效的放置目标上时,会依次触发以下事件:
- dragenter,把元素拖动到放置目标上时触发。
- dragover,元素在放置目标范围内被拖动期间会持续触发。
- dragleave,当元素被拖动到放置目标外触发。
<!DOCTYPE html>
<html lang="en">
<head>
<style>
div {
width: 100px;
height: 100px;
}
.red {
background-color: red;
}
.blue {
background-color: blue;
}
</style>
</head>
<body>
<div class="red"></div>
<div class="blue"></div>
</body>
</html>
<script>
const red = document.querySelector('.red')
const blue = document.querySelector('.blue')
red.onclick = function () {
console.log(11)
}
red.ondragstart = function () {
console.log('dragstart')
}
red.ondrag = function (e) {
console.log('drag', e.x, e.y)
}
red.ondragend = function () {
console.log('dragend')
}
blue.ondragenter = function (e) {
e.preventDefault()
console.log('dragenter')
}
blue.ondragover = function (e) {
e.preventDefault()
console.log('dragover', e.x, e.y)
}
blue.ondragleave = function () {
console.log('dragleave')
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
注意,元素默认是不允许放置的。可以通过阻止默认事件来修改,把元素转换为有效的放置目标。
# dataTransfer对象
在拖动过程中,event.dataTransfer对象用于从被拖动元素想放置目标传递字符串数据。
dataTransfer对象有2个主要方法: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')
2
3
4
5
6
7
HTML5除了这2种类型,还扩展到了允许任何MIME类型。
# 可拖动
通常只有图片、链接和文本是可拖动的。如果想让其他元素具有可拖动能力,可以设置draggable
属性为true。
# Page Visibility API
页面是否可见,可以监听visivilitychange
事件。
document.addEventListener('visibilitychange', function () {
console.log(document.visibilityState)
document.title = document.hidden ? '用户离开了' : '用户回来了'
})
2
3
4
document.visibilityState的值有3种:
- 'hidden'
- 'visible'
- 'prerender'
# Streams API (略)
# Performance API
Performance API 用于度量页面性能。
# High Resolution Time API
可以用来记录代码的执行时间。
- performance.now(),返回一个微秒精度的浮点值。它采用相对度量时间。
- Date.now(),与上一个相比,只有毫秒级精度,且如果系统时钟被调整,会受到影响。
// performance.now()
const t0 = performance.now()
for (let i = 0; i < 100; i++) {
const j = i * 2
}
const t1 = performance.now()
console.log(t0) // 18.19999998807907
console.log(t1) // 18.5
console.log(t1 - t0) // 0.30000001192092896
// Date.now()
const t01 = Date.now()
for (let i = 0; i < 100; i++) {
const j = i * 2
}
const t11 = Date.now()
console.log(t01) // 1647739237084
console.log(t11) // 1647739237084
console.log(t11 - t01) // 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Performance Timeline API
PerformanceEntry对象记录了浏览器的时间性能属性,也可以自定义。
其中默认的PerformanceNavigationTiming会捕获大量时间戳,用于描述页面是何时加载的。
// 浏览器的PerformanceEntry对象,是浏览器性能时间栈
const [entry] = performance.getEntries()
console.log(entry)
// {
// connectEnd: 1.5999999642372131,
// connectStart: 1.5999999642372131,
// decodedBodySize: 0,
// domComplete: 1872.7999999523163,
// domContentLoadedEventEnd: 1872.7999999523163,
// domContentLoadedEventStart: 1872.7999999523163,
// domInteractive: 1872.699999988079,
// domainLookupEnd: 1.5999999642372131,
// domainLookupStart: 1.5999999642372131,
// duration: 1872.8999999761581,
// encodedBodySize: 0,
// entryType: 'navigation',
// fetchStart: 1.5999999642372131,
// initiatorType: 'navigation',
// loadEventEnd: 1872.8999999761581,
// loadEventStart: 1872.8999999761581,
// name: '',
// nextHopProtocol: '',
// redirectCount: 0,
// redirectEnd: 0,
// redirectStart: 0,
// requestStart: 1.5999999642372131,
// responseEnd: 5.099999964237213,
// responseStart: 1.5999999642372131,
// secureConnectionStart: 0,
// serverTiming: [],
// startTime: 0,
// transferSize: 300,
// type: 'reload',
// unloadEventEnd: 0,
// unloadEventStart: 0,
// workerStart: 0,
// }
// 自定义的PerformanceEntry对象
performance.mark('foo')
for (let i = 0; i < 1e6; i++) {}
performance.mark('bar')
const [startMark, endMark] = performance.getEntriesByType('mark')
console.log(endMark.startTime - startMark.startTime) // 2.100000023841858
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
此外,还可以使用Resource Timing API来度量当前页面加载时请求资源的速度。
const [entry] = performance.getEntriesByType('resource')
console.log(entry)
// {
// connectEnd: 0,
// connectStart: 0,
// decodedBodySize: 0,
// domainLookupEnd: 0,
// domainLookupStart: 0,
// duration: 824.3999999761581,
// encodedBodySize: 0,
// entryType: 'resource',
// fetchStart: 9.199999988079071,
// initiatorType: 'link',
// name: 'https://vuex.vuejs.org/assets/style.34a39eef.css',
// nextHopProtocol: '',
// redirectEnd: 0,
// redirectStart: 0,
// requestStart: 0,
// responseEnd: 833.5999999642372,
// responseStart: 0,
// secureConnectionStart: 0,
// serverTiming: [],
// startTime: 9.199999988079071,
// transferSize: 0,
// workerStart: 0,
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Web组件
指用于增强DOM行为的工具,还没有形成统一规范。包括HTML模板、影子DOM、自定义元素。
# HTML模板
其核心内容是:
- 可以让浏览器解析为DOM树
- 但不会渲染
- 可以批量向HTML中转移
这个特性和fragment非常像,不过实现方式是使用HTML模板,如下:
<template id="foo">
<p>I am a p tag</p>
</template>
2
3
它不会真实渲染,但会存在于DOM上,表现为:
<template id="foo">
#document-fragment
<p>I am a p tag</p>
</template>
2
3
4
可以这样获取到这个DocumentFragment:
const foo = document.querySelector('#foo')
console.log(foo.content) // #document-fragment
2
下面演示了将fragment添加到dom元素上的情形,注意fragment的dom操作不会重排布局。
<body>
<template id="foo">
<p>I am a p tag</p>
</template>
<div class="box"></div>
</body>
<script>
const foo = document.querySelector('#foo')
const box = document.querySelector('.box')
box.appendChild(foo.content)
</script>
2
3
4
5
6
7
8
9
10
11
再检查元素,发现结构发生了变化。
<body>
<template id="foo">
#document-fragment
</template>
<div class="box">
<p>I am a p tag</p>
</div>
</body>3
2
3
4
5
6
7
8
# 影子DOM
影子DOM与HTML模板类似,都是类document结构;区别在于影子DOM的内容会实际渲染到页面上,而HTML模板不会。
影子宿主:容纳影子DOM的元素
影子根:影子DOM的根节点
attachShadow(), 创建影子DOM,入参必须传mode属性,可能的值有open和closed。注意closed类型的影子
微信小程序的微信开发者工具,就应用了shadow-dom
<div class="host"></div>
<script>
const host = document.querySelector('.host')
const openShadow = host.attachShadow({ mode: 'open' })
openShadow.innerHTML = '<div>hello</div>'
console.log(openShadow)
console.log(openShadow === host.shadowRoot) // true
</script>
2
3
4
5
6
7
8
检查元素如下
<div class="host">
#shadow-root(open)
<div>hello</div>
</div>
2
3
4
影子DOM使用的初衷是限制css的作用范围,目的是指让css在影子DOM范围内生效。先看下没有影子DOM的场景:
<style>
.red {
color: red;
}
.blue {
color: blue;
}
.green {
color: green;
}
</style>
<div>
<p class="red">red</p>
</div>
<div>
<p class="blue">blue</p>
</div>
<div>
<p class="green">green</p>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可以看到,通过设置类名,然后对每个类设置样式,就设置不同的样式了。不过这些类名相当于全局样式,也会影响到其他样式。而影子DOM是这样:
for (let item of ['red', 'blue', 'green']) {
const host = document.createElement('div')
document.body.appendChild(host)
const openShadow = host.attachShadow({ mode: 'open' })
openShadow.innerHTML = `
<p>${item}</p>
<style>
p {
color: ${item};
}
</style>
`
}
2
3
4
5
6
7
8
9
10
11
12
13
影子DOM是为自定义Web组件设计的,和Vue类似,它拥有自己的插槽,可以将影子宿主中的HTML投射到影子DOM中。
for (let item of ['red', 'blue', 'green']) {
const host = document.createElement('div')
host.innerText = item
document.body.appendChild(host)
const openShadow = host.attachShadow({ mode: 'open' })
openShadow.innerHTML = `
<p><slot></slot></p>
<style>
p {
color: ${item};
}
</style>
`
}
2
3
4
5
6
7
8
9
10
11
12
13
14
除了默认slot,还有命名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">haha</slot>
<slot name="foo">heihei</slot>
`
2
3
4
5
6
7
8
9
10
11
用法和Vue语法类似。
影子DOM事件经过浏览器处理,会发生事件重定向。最终的结果是,宿主和影子DOM可以触发各自的事件,如下:
document.body.innerHTML = `
<div onclick="console.log('div:',event.target)">out</div>
`
document.querySelector('div').attachShadow({ mode: 'open' }).innerHTML = `
<button onclick="console.log('button:',event.target)">inner</button>
`
2
3
4
5
6
7
button: <button onclick="console.log('button:',event.target)">inner</button>
div: <div onclick="console.log('div:',event.target)">#shadow-root (open)"out"</div>
2
3
# 自定义元素
自定义元素为Web开发提供了更多可能:
- 可以在自定义标签出现时为它定义复杂的行为
- 可以在DOM中将其纳入元素生命周期管理
- 要使用全局属性customElements
1.创建自定义元素
如下创建了一个简单的自定义元素:
<x-foo>hi</x-foo>
<x-foo>hello</x-foo>
<x-foo>hey</x-foo>
<div is="y-foo">y-foo</div>
<script>
// 调用customElements.define()创建自定义元素 例x-foo
class FooElement extends HTMLElement {
constructor() {
super()
console.log('hello x-foo')
}
}
customElements.define('x-foo', FooElement)
console.log(document.querySelector('x-foo') instanceof FooElement)
// 如果自定义元素继承了一个元素类,则可以使用is和extends选项将标签指定为该自定义元素 例y-foo
class YFooElement extends HTMLDivElement {
constructor() {
super()
console.log('hi y-foo')
}
}
customElements.define('y-foo', YFooElement, { extends: 'div' })
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2.结合Web组件
接下来,将HTML模板、影子DOM和自定义元素结合使用,发挥Web组件的威力。
首先为自定义元素添加影子DOM:
<x-foo>hi</x-foo>
<script>
class FooElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<p>I am custom</p>
`
}
}
customElements.define('x-foo', FooElement)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
为避免使用innerHTML(导致维护困难),可以加入HTML模板。
<body>
<template id="x-foo-tpl">
<p>I am custom</p>
</template>
<x-foo></x-foo>
<script>
class FooElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
const xFooTpl = document.querySelector('#x-foo-tpl')
this.shadowRoot.appendChild(xFooTpl.content.cloneNode(true))
}
}
customElements.define('x-foo', FooElement)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如此,html和js的职能分开,可以实现可复用的组件。
3.自定义元素的生命周期
- constructor():在创建元素实例时调用。
- connectedCallback():在自定义元素挂载到DOM上时调用。
- disconnectedCallback():在自定义元素从DOM移除时调用。
- attributeChangedCallback():在每次可观察属性的值发生变化时调用。
- adoptedCallback():在通过
document.adoptNode()
将这个自定义元素实例移动到新文档对象时调动。
class FooElement extends HTMLElement {
// constructor...
connectedCallback() {
console.log('connected')
}
disconnectedCallback() {
console.log('disconnected')
}
}
2
3
4
5
6
7
8
9
4.反射自定义元素属性
DOM实体和js对象的属性应该同步变化。
- js对象反射到DOM,可以通过getter和setter函数进行同步
- DOM到js对象,需要使用observedAttributes()返回自定义元素属性的值,从而在attributeChangedCallback触发后进行修改。
class FooElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
const xFooTpl = document.querySelector('#x-foo-tpl')
this.shadowRoot.appendChild(xFooTpl.content.cloneNode(true))
this.bar = 1
}
get bar() {
return this.getAttribute('bar')
}
set bar(value) {
this.setAttribute('bar', value)
}
static get observedAttributes() {
return ['bar']
}
connectedCallback() {
console.log('connected')
}
disconnectedCallback() {
console.log('disconnected')
}
attributeChangedCallback(name, oldV, newV) {
console.log('attributeChanged')
console.log('name', name)
console.log('oldV', oldV)
console.log('newV', newV)
if (oldV !== newV) {
this[name] = newV
}
}
}
customElements.define('x-foo', FooElement)
const el = document.querySelector('x-foo')
el.setAttribute('bar', 2)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
5.升级自定义元素(略,目前还是草案)
# Web Cryptography API
# 生成随机数
通常人们使用Math.random()
来生成随机数,这种方式是伪随机,随机数顺序是确定的,如果知道其内部状态,就可以预测后续生成的伪随机值。所以不建议用来对数据加密。
在伪随机的基础上,额外增加一个熵作为输入,这样生成的值就很难预测了,可以用于加密。
function randomFloat() {
// 生成32位随机值
const array = new Uint32Array(1)
// 最大值是2^32-1
const maxUnit32 = Math.pow(2, 32) - 1
// 用最大可能的值来除
return crypto.getRandomValues(array)[0] / maxUnit32
}
console.log(randomFloat())
2
3
4
5
6
7
8
9
10
11
12
使用SubtleCrypto对象(略)