Worker() - Web API 接口参考

Worker()
========

**`Worker()`** 构造函数创建一个 `Worker` 对象，该对象执行指定的 URL 脚本。这个脚本必须遵守 同源策略 。

如果 此 URL 有一个无效的语句，或者违反同源策略，一个 `SECURITY_ERR` 类型的`DOMException`被抛出。

**备注：** 浏览器厂商对于 data URI 是否同源存在分歧。尽管 Gecko 10.0 和之后的版本接受 data URIs，但在所有其他浏览器中并非如此。

语法
--

```js
const myWorker = new Worker(aURL, options);
```

### 参数

* 如果文档不允许启动 worker，则会引发 SecurityError
* 如果脚本之一的 MIME 类型为 `text/csv`, `image/*`, `video/*`,或 `audio/*`, 则会引发 NetworkError。它应该始终是 text/javascript。
* 如果 aURL 无法解析，则引发 SyntaxError。
* *aURL*
    + : 是一个`DOMString` 表示 **worker** 将执行的脚本的 URL。它必须遵守同源策略。
* *options* 可选
    + : 包含可在创建对象实例时设置的选项属性的对象。可用属性如下：
        - `type`：用以指定 worker 类型的 `DOMString` 值。该值可以是 `classic` 或 `module`. 如果未指定，将使用默认值 `classic.`
        - `credentials`：用以指定 worker 凭证的 `DOMString` 值。该值可以是\* `omit`\*, `same-origin`，或 *`include`.。如果未指定，或者 type 是 `classic`，将使用默认值 `omit` (不要求凭证)。*
        - \*`name`：\*在 `DedicatedWorkerGlobalScope` 的情况下，用来表示 worker 的 scope 的一个 `DOMString` 值，主要用于调试目的。
### 返回值

创建的 worker。

### 异常

* 当 document 不被允许启动 worker 的时候，将抛出一个 SecurityError 异常。例如：如果提供的 aURL 有语法错误，或者与同源策略相冲突（跨域访问）。
* 如果 worker 的 MIME 类型不正确，将抛出一个 NetworkError 异常。worker 的 MIME 类型必须是 text/javascript。
* 如果 aURL 无法被解析（格式错误），将抛出一个 SyntaxError 异常。
例子
--

下面的代码片段展示了通过 Worker() 创建 `Worker` 对象的过程，以及随后的使用方法：

```js
let myWorker = new Worker("worker.js");

first.onchange = function () {
  myWorker.postMessage([first.value, second.value]);
  console.log("Message posted to worker");
};
```

完整的例子请看 Basic dedicated worker example (run dedicated worker).

规范
--

| Specification |
| --- |
| HTML Standard # dom-worker-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

**备注：** 浏览器可以被标记为对`Worker()`的完全支持尽管他并不支持一个以 modules 类型编写的脚本。截至 2019 年 8 月 1 日，暂无浏览器支持以模块类型编写的脚本。如果没有这种支持，moduleds 类型的脚本必须使用编译器翻译成无 module 代码才能在浏览器上运行。

参见
--

* `Worker` 它所属的接口。
* Web Workers API


Worker：error 事件 - Web API 接口参考

Worker：error 事件
===============

`Worker` 接口的 **`error`** 事件会在 worker 发生错误时触发。

语法
--

在如 `addEventListener()` 等方法中使用事件名称，或者使用事件处理器属性。

```js
addEventListener("error", (event) => {});

onerror = (event) => {};
```

事件类型
----

通用的 `Event`。

示例
--

下面的代码片段使用 `Worker()` 构造函数创建了一个 `Worker` 对象，并在生成的对象上设置了一个 `onerror` 事件处理器：

```js
const myWorker = new Worker("worker.js");

myWorker.onerror = (event) => {
  console.log("There is an error with your worker!");
};
```

规范
--

| Specification |
| --- |
| HTML Standard # handler-abstractworker-onerror |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


ShadowRoot.innerHTML - Web API 接口参考

ShadowRoot.innerHTML
====================

**实验性:** **这是一项实验性技术**
在将其用于生产之前，请仔细检查浏览器兼容性表格。

`ShadowRoot` 接口的 **`innerHTML`** 属性设置或返回 ShadowRoot 内的 DOM 树。

句法
--
```

let domString = shadowRoot.innerHTML
// 返回

shadowRoot.innerHTML = domString;

// 设置
```

### 值

一个 `DOMString`.

规范
--

| Specification |
| --- |
| DOM Parsing and Serialization # ref-for-dom-innerhtml-innerhtml-2 |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


ShadowRoot.mode - Web API 接口参考

ShadowRoot.mode
===============

Baseline Widely available
-------------------------

This feature is well established and works across many devices and browser versions. It’s been available across browsers since January 2020.

* Learn more
* See full compatibility
* Report feedback
**实验性:** **这是一项实验性技术**
在将其用于生产之前，请仔细检查浏览器兼容性表格。

**`mode`** 是 `ShadowRoot` 的只读属性，它返回 `ShadowRoot` 创建时的模式 ("`open`" 或者 "`closed`") 。

当 `ShadowRoot` 的 `mode` 是 "`closed`" 时， `ShadowRoot` 的内部实现无法被 JavaScript 访问及修改 — 也就是说将该实现不公开，例如，`<video>` 标签内部实现无法被 JavaScript 访问及修改。

Syntax
------
```

var mode = shadowRoot.mode
```

### Value

值为 "`open`" 或者 "`closed`".

Specifications
--------------

| Specification |
| --- |
| DOM Standard # dom-shadowroot-mode |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


ShadowRoot.delegatesFocus - Web API 接口参考

ShadowRoot.delegatesFocus
=========================

`ShadowRoot` 接口中的只读属性 **`delegatesFocus`** 返回一个布尔值表明 delegatesFocus 是否在 shadow 被附加的时候设置了。

这目前是一个实验的非标准特性，仅在 Chrome 中可用。

语法
--
```

var df = shadowRoot.delegatesFocus
```

### 值

一个布尔值 — `true` 值表明 shadow root 已经 delegate focus, `false` 反之。

示例
--

```js
let customElem = document.querySelector('my-shadow-dom-element');
let shadow = customElem.shadowRoot;

  ...

// Does it delegate focus?
let hostElem = shadow.delegatesFocus;
```

标准
--

这目前是个非标准特性。

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


电池管理器 - Web API 接口参考

电池管理器
=====

指示设备电池当前是否正在充电的 Boolean 值

语法
--
```

var charging = battery.charging
```

返回当前是否正在对 `BatteryManager` 对象的设备充电，如果正在充电则返回 `true`，否则返回 `false`.

示例
--

### HTML

```html
<div id="charging">(charging state unknown)</div>
```

### JavaScript

```js
navigator.getBattery().then(function (battery) {
  var charging = battery.charging;

  document.querySelector("#charging").textContent = charging;
});
```

规范
--

| Specification |
| --- |
| Battery Status API # dom-batterymanager-charging |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `BatteryManager`
* `Navigator.getBattery`


CustomEvent.initCustomEvent() - Web API 接口参考

CustomEvent.initCustomEvent()
=============================

**已弃用:** 不再推荐使用该特性。虽然一些浏览器仍然支持它，但也许已从相关的 web 标准中移除，也许正准备移除或出于兼容性而保留。请尽量不要使用该特性，并更新现有的代码；参见本页面底部的兼容性表格以指导你作出决定。请注意，该特性随时可能无法正常工作。

**`CustomEvent.initCustomEvent()`** 方法初始化了一个 `CustomEvent` object. 如果该事件已经被分发出去，则不会在初始化过程中重复触发．

这类对象一定是由 `Document.createEvent()` 方法创建的。该方法被分发之前必须通过`EventTarget.dispatchEvent()`方法设置．一旦被分发则，则无法被重新设置．

**备注：** **该方法已经作废，不要在新项目中继续使用该方法。**

Instead use specific event constructors, like `CustomEvent()`. The page on Creating and triggering events (en-US) gives more information about the way to use these.

Syntax
------
```

event.initCustomEvent(type, canBubble, cancelable, detail);
```

### Parameters

`type`

类型`DOMString`，事件名称。

*`canBubble`*

类型`Boolean`，事件是否沿着 dom 树向上冒泡。

`cancelable`

类型`Boolean`，事件是否可取消。

*`detail`*

事件初始化时传入的数据。

Specifications
--------------

| Specification |
| --- |
| DOM Standard # dom-customevent-initcustomevent |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

See also
--------

* `CustomEvent`
* The constructor to use instead of this deprecated method: `CustomEvent()`.


CustomEvent：CustomEvent() 构造函数 - Web API 接口参考

CustomEvent：CustomEvent() 构造函数
==============================

**`CustomerEvent()`** 构造函数创建一个新的 `CustomEvent` 对象。

语法
--

```js
new CustomEvent(type)
new CustomEvent(type, options)
```

### 参数

`type`

提供事件名称的字符串。事件名称区分大小写。

`options` 可选

一个对象，*除 `Event()` 中定义的属性外*，该对象还可以具有以下属性：

`detail` 可选

与事件相关联的事件相关值。处理器可使用 `CustomEvent.detail` 属性获取该值。默认为 `null`。

### 返回值

一个新的 `CustomEvent` 对象。

示例
--

```js
// 创建自定义事件
const catFound = new CustomEvent("animalfound", {
  detail: {
    name: "猫",
  },
});
const dogFound = new CustomEvent("animalfound", {
  detail: {
    name: "狗",
  },
});

// 添加合适的事件监听器
obj.addEventListener("animalfound", (e) => console.log(e.detail.name));

// 触发事件
obj.dispatchEvent(catFound);
obj.dispatchEvent(dogFound);

// 控制台中输出“猫”和“狗”
```

额外的示例可以在创建和触发事件中找到。

规范
--

| Specification |
| --- |
| DOM Standard # ref-for-dom-customevent-customevent |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `CustomEvent`
* 创建和触发事件


CustomEvent.detail - Web API 接口参考

CustomEvent.detail
==================

接口 `CustomEvent` 的只读属性 **`detail`** （详情）返回在初始化事件对象时传递过来的任何类型数据。

**备注：** 此特性在 Web Worker 中可用

Syntax
------
```

 let myDetail = customEventInstance.detail;
```

### Return value

事件对象初始化时传递的任何类型数据。

Example
-------

```js
// add an appropriate event listener
obj.addEventListener("cat", function (e) {
  process(e.detail);
});

// create and dispatch the event
let event = new CustomEvent("cat", {
  detail: {
    hazcheeseburger: true,
  },
});
obj.dispatchEvent(event);

// Will return an object contaning the hazcheeseburger property
let myDetail = event.detail;
```

Specifications
--------------

| Specification |
| --- |
| DOM Standard # ref-for-dom-customevent-detail② |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

See also
--------

* `CustomEvent`


Crypto.randomUUID() - Web API 接口参考

Crypto.randomUUID()
===================

**安全上下文:** 此项功能仅在一些支持的浏览器的安全上下文（HTTPS）中可用。

`Crypto` 接口的 **`randomUUID()`** 方法用于通过密码学安全的随机数生成器生成第四版 UUID (en-US)。

语法
--

```js
randomUUID()
```

### 参数

无。

### 返回值

包含随机生成的、长度为 36 字符的第四版 UUID 字符串。

示例
--

通过全局的 `crypto` 属性访问该方法。

```js
/\* 假定 self.crypto.randomUUID() 可用 \*/

let uuid = self.crypto.randomUUID();
console.log(uuid); // 例如 "36b8f84d-df4e-4d49-b662-bcde71a8764f"
```

规范
--

| Specification |
| --- |
| Web Cryptography API # Crypto-method-randomUUID |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `Web Crypto API`
* `crypto` 用于获取 `Crypto` 对象。
* `Crypto.getRandomValues`，用于获取任意个安全的随机字节的来源。


Crypto.getRandomValues() - Web API 接口参考

Crypto.getRandomValues()
========================

**`Crypto.getRandomValues()`** 方法让你可以获取符合密码学要求的安全的随机值。传入参数的数组被随机值填充（在加密意义上的随机）。

为了确保足够的性能，不使用真正的随机数生成器，但是它们正在使用具有足够熵值伪随机数生成器。它所使用的 PRNG 的实现与其他不同，但适用于加密的用途。该实现还需要使用具有足够熵的种子，如系统级熵源。

语法
--
```

cryptoObj.getRandomValues(typedArray);
```

### 参数

`typedArray`

是一个基于整数的 `TypedArray`，它可以是 `Int8Array`、`Uint8Array`、`Int16Array`、 `Uint16Array`、 `Int32Array` 或者 `Uint32Array`（**不可**使用 `Float32Array` 或者 `Float64Array`）。在数组中的所有的元素会被随机数重写。（注释：生成的随机数储存在 `typedArray` 数组上。）

### 异常事件

`QuotaExceededError` `DOMException`

如果请求的长度超过 65536 字节，则抛出该异常。

例子
--

```js
/\* 假设 window.crypto.getRandomValues 可用 \*/

var array = new Uint32Array(10);
window.crypto.getRandomValues(array);

console.log("Your lucky numbers:");
for (var i = 0; i < array.length; i++) {
  console.log(array[i]);
}
```

标准
--

| Specification |
| --- |
| Web Cryptography API # Crypto-method-getRandomValues |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* 通过 `Window.crypto` 获取 `Crypto` 对象。
* `Math.random`，一个非密码学安全的随机数来源。


Crypto.subtle - Web API 接口参考

Crypto.subtle
=============

**安全上下文:** 此项功能仅在一些支持的浏览器的安全上下文（HTTPS）中可用。

**`Crypto.subtle`** 只读属性返回一个 `SubtleCrypto` 对象，可以用来做低级加密运算。

值
-

一个可用于与 Web Crypto API 的低级加密功能交互的 `SubtleCrypto` 对象。

规范
--

| Specification |
| --- |
| Web Cryptography API # Crypto-attribute-subtle |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `Crypto`
* `SubtleCrypto`
* 兼容性测试页面。
* 用于 IE11 和 Safari 的垫片。


WaveShaperNode.curve - Web API 接口参考

WaveShaperNode.curve
====================

`WaveShaperNode` 接口的 `curve` 属性是一个描述要被应用的畸变的`Float32Array` (en-US) 数组。

数组的中间元素被应用于每个信号数值 `0`，第一个应用于信号数值 `-1`，最后一个应用于信号数值 `1`；小于 `-1` 或者大于 `1` 的数值分别按照 `-1` 和 `1` 来处理。

如有必要，使用线性插值计算畸变曲线的中间值。

**备注：** 数组的值可以是 `null` : 在这个情况下，不会有畸变被应用到输入的信号上。

语法
--

```js
var audioCtx = new AudioContext();
var distortion = audioCtx.createWaveShaper();
distortion.curve = myCurveDataArray; // myCurveDataArray is a Float32Array
```

### Value

一个 `Float32Array` (en-US).

示例
--

参见 `BaseAudioContext.createWaveShaper()` 以获取示例代码。

规范
--

| Specification |
| --- |
| Web Audio API # dom-waveshapernode-curve |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* 使用 Web Audio API (en-US)


WaveShaperNode.oversample - Web API 接口参考

WaveShaperNode.oversample
=========================

`WaveShaperNode` 接口的 `oversample` 属性是一个指示过采样是否必须使用的枚举值。过采样是一个用于在将畸变应用到音频信号之前创建更多的采样（上采样）的技术。

一旦被应用，采样的数值会被还原为初始的数值。这将通过避免一些混淆现象从而导致更好的结果，代价则是在畸变曲线上会有较低的精确度。

可用的`oversample` 值有：

| Value | Effect |
| --- | --- |
| `'none'` | 不使用过采样。 |
| `'2x'` | 在应用畸变曲线前将采样的数量翻倍。 |
| `'4x'` | 在应用畸变曲线前将采样的数量翻 4 倍。 |

语法
--

```js
distortion.oversample = enumeratedValue;
```

### 值

* 畸变是一个`WaveShaperNode`.
* 枚举值`'none'`， `'2x'`，或 `'4x'`。
示例
--

参见 `BaseAudioContext.createWaveShaper()` 以获取示例代码。

规范
--

| Specification |
| --- |
| Web Audio API # dom-waveshapernode-oversample |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* 使用 Web Audio API (en-US)


WaveShaperNode.WaveShaperNode() - Web API 接口参考

WaveShaperNode.WaveShaperNode()
===============================

Web Audio API 的 **`WaveShaperNode()`** 构造方法创建一个新的 `WaveShaperNode`对象，是一个可以用来表示非线性畸变的`AudioNode` 。

语法
--
```

var waveShaperNode = new WaveShaperNode(context, options)
```

### 参数

*继承* *`AudioNodeOptions` 字典的参数。*

*context*

`AudioContext`的一个引用。

*options* 可选

Options 参数如下：

* `curve`: 用于波形形成效果的修正曲线。输入信号通常在 [-1;1] 的范围间。
* `oversample`: 指定在应用修正曲线时会被使用的过采样的类别（如果有的话）。有效的值有'`none`', '`2x`', 或者'`4x`'。缺省情况下是'`none`'。

### 返回值

一个新的`WaveShaperNode`对象的实例。

规范
--

| Specification |
| --- |
| Web Audio API # dom-waveshapernode-waveshapernode |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTML、CSS 和 DOM 中的空白符是如何处理的 - Web API 接口参考

HTML、CSS 和 DOM 中的空白符是如何处理的
==========================

在 DOM 中，空白符的存在会导致布局问题，并使内容树的操作变得困难，这取决于它的位置。这篇文章探讨了什么时候会出现困难，并研究了如何缓解由此产生的问题。

什么是空白符？
-------

空白符是指任何仅由空格、制表符或换行符（准确地说，是 CRLF 序列、回车或换行）组成的文本串。这些字符允许你以一种使你自己和其他人容易阅读的方式来格式化你的代码。事实上，我们的大部分源代码都充满了这些空白字符，我们只倾向于在生产构建步骤中去掉它，以减少代码下载量。

### HTML 会大幅度忽略空白符吗？

就 HTML 而言，空白符基本上是被忽略的——单词之间的空白被视为一个字符，而元素的开始和结束以及元素之外的空白则被忽略。以下面这个最小的示例为例：

```html
<!DOCTYPE html>

  <h1>      Hello      World!     </h1>
```

这段源代码在 `DOCTYPE` 后面有几个换行符，在 `<h1>` 元素前后和里面有很多空格字符，但浏览器似乎根本不在乎，只是显示“Hello World!”，好像这些字符根本不存在：

这是为了使空白字符不影响你的页面布局。在元素周围和内部创造空间是 CSS 的工作。

### 空白符发生了什么？

然而，它们不会就这样消失。

任何在原始文档中的 HTML 元素之外的空白字符都会在 DOM 中表现出来。这在内部是需要的，以便编辑器可以保留文档的格式。这意味着：

* 会有一些文本节点只包含空白，而且
* 有些文本节点会在开头或结尾有空白。

以下列文档为例：

```html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>My Document</title>
  </head>
  <body>
    <h1>Header</h1>
    <p>Paragraph</p>
  </body>
</html>
```

其 DOM 树类似于这样：

![代表单个 HTML 文档的 DOM 树](/zh-CN/docs/Web/API/Document_Object_Model/Whitespace/dom-string.png)

在 DOM 中保留空白字符在很多方面都是有用的，但在某些地方，这使得某些布局更难实现，并给那些想在 DOM 中迭代节点的开发者带来了问题。我们将在后面讨论这些问题，以及一些解决方案。

### CSS 如何处理空白符？

大多数空白字符会被忽略，但不是所有的都会被忽略。在前面的示例中，“Hello”和“World!”之间的一个空格在浏览器中渲染时仍然存在。浏览器引擎中有一些规则来决定哪些空白字符是有用的，哪些是没用的——这些规则至少在 CSS 文本模块第 3 级中作了部分说明，特别是关于 CSS `white-space` 属性和空白符处理细节的部分，但我们也在下面提供一个更简单的解释。

#### 示例

让我们看看其他示例，为了使它看起来更简单，我们添加了一个注释，将所有的空格显示为“◦”，所有的制表符显示为“⇥”，所有的换行符显示为“⏎”：

这个示例

```html
<h1>   Hello
        <span> World!</span>   </h1>

<!--
<h1>◦◦◦Hello◦⏎
⇥⇥⇥⇥<span>◦World!</span>⇥◦◦</h1>
-->
```

会在浏览器中像这样渲染：

#### 解释

`<h1>` 元素只包含行内元素，实际上包含：

* 一个文本节点（包含一些空格，单词“Hello”和一些制表符）。
* 一个行内元素（`<span>`，包含一个空格和一个单词“World!”）。
* 另外一个文本节点（只包含制表符和空格）。

正因为如此，它建立了所谓的行内格式化上下文。这是浏览器引擎可能使用的布局渲染上下文之一。

在这个上下文中，空白符的处理可以总结如下：

1. 首先，换行前后的所有空格和制表符都会被忽略，所以，如果我们拿之前的示例标记：

```html
<h1>◦◦◦Hello◦⏎
⇥⇥⇥⇥<span>◦World!</span>⇥◦◦</h1>
```

然后应用这条规则，会得到：

```html
<h1>◦◦◦Hello⏎
<span>◦World!</span>⇥◦◦</h1>
```

2. 然后，所有制表符视作空格字符，所以这个示例会变为：

```html
<h1>◦◦◦Hello⏎
<span>◦World!</span>◦◦◦</h1>
```

3. 然后，换行符被转化为空格符：

```html
<h1>◦◦◦Hello◦<span>◦World!</span>◦◦◦</h1>
```

4. 之后，任何紧跟在另一个空格之后的空格（甚至跨越两个独立的行内元素）都会被忽略，所以我们会得到：

```html
<h1>◦Hello◦<span>World!</span></h1>
```

5. 最后，元素开头和结尾的空格序列被删除，所以我们最终得到了：

```html
<h1>Hello◦<span>World!</span></h1>
```

这就是为什么人们在访问网页时，会看到“Hello World!”这句话很好地写在页面的顶部，而不是一个奇怪的缩进的“Hello”，但在下面一行有一个更奇怪的缩进的“World!”。

**备注：** 从版本 52 开始，Firefox 开发者工具支持高亮文本节点，使得观察包含节点中包含的空白字符变得更加容易。纯的空白符节点会有一个“whitespace”标记。

### 在块格式化上下文的空白符

上面我们只是看了包含行内元素的元素，以及行内格式化上下文。如果一个元素至少包含一个块元素，那么它就会建立所谓的块格式化上下文。

这种情况下，空白符的处理方式非常不同。

#### 示例

让我们看看一个示例来解释发生了什么。我们已经像以前一样标记了空白字符。

我们有三个文本节点，一个在第一个 `<div>` 之前，一个在 2 个 `<div>` 之间，还有一个在第二个 `<div>` 之后。

```html
<body>
  <div>  Hello  </div>

   <div>  World!   </div>
</body>

<!--
<body>⏎
⇥<div>◦◦Hello◦◦</div>⏎
⏎
◦◦◦<div>◦◦World!◦◦</div>◦◦⏎
</body>
-->
```

它会像这样渲染：

#### 解释

我们可以将这里的空白符处理方式总结如下（不同的浏览器在具体行为上可能有一些细微的差别，但这基本上是可行的）：

1. 因为我们在块格式化上下文内，所有部分必须是一个块，所以我们的 3 个文本节点也会组成各自的“块”，就像这 2 个 `<div>` 一样。块占据了可用的全部宽度，并且相互堆叠，这意味着，从上面的示例开始：

```html
<body>⏎
⇥<div>◦◦Hello◦◦</div>⏎
⏎
◦◦◦<div>◦◦World!◦◦</div>◦◦⏎
</body>
```

我们会得到一个由多个“块”组成的布局：

```html
<block>⏎⇥</block>
<block>◦◦Hello◦◦</block>
<block>⏎◦◦◦</block>
<block>◦◦World!◦◦</block>
<block>◦◦⏎</block>
```

2. 然后通过对这些块应用行内格式化背景下的空白处理规则来进一步简化：

```html
<block></block>
<block>Hello</block>
<block></block>
<block>World!</block>
<block></block>
```

3. 我们现在拥有的 3 个空块不会在最终的布局中占据任何空间，因为它们不包含任何东西，所以我们最终将只有 2 个块在页面中占据空间。人们在浏览网页时，会看到“Hello”和“World!”这两个词分别出现在两行上，就像你所期望的那样，两个 `<div>` 被排列出来。浏览器引擎基本上忽略了在源代码中添加的所有空白符处。
在行内和行内块元素之间的空格
--------------

让我们继续看看由于空白符可能产生的几个问题，以及如何解决这些问题。首先，我们来看看在行内和行内块元素之间的空格会发生什么。事实上，我们在第一个示例中已经看到了这一点，当时我们描述了在行内格式化背景下是如何处理空白的。

我们说过，存在可以忽略大多数字符，但分词的字符仍然存在的规则。当你只是处理像 `<p>` 这样只包含 `<em>`、`<strong>`、`<span>` 等行级元素的块级元素时，通常没有必要关心这个问题，因为布局中的额外空白符有助于分隔句子中的单词。

当你开始使用行内块元素时，情况会变得更加有趣。这些元素的行为就像外面的行内元素和里面的块，通常用于显示更复杂的 UI，而不仅仅是文本，并排在同一行，例如导航菜单项。

因为它们是块，许多人期望它们会有这样的行为，但实际上它们并没有。如果相邻的行内元素之间有格式化的空白，这将导致布局中的空白，就像文本中的单词之间的空白一样。

### 示例

考虑这个示例，当然，我们像之前的示例一样包含了显示出所有空白字符的 HTML 注释。

```css
.people-list {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.people-list li {
  display: inline-block;
  width: 2em;
  height: 2em;
  background: #f06;
  border: 1px solid;
}
```

```html
<ul class="people-list">
  <li></li>

  <li></li>

  <li></li>

  <li></li>

  <li></li>
</ul>

<!--
<ul class="people-list">⏎
◦◦<li></li>⏎
⏎
◦◦<li></li>⏎
⏎
◦◦<li></li>⏎
⏎
◦◦<li></li>⏎
⏎
◦◦<li></li>⏎
</ul>
-->
```

这个示例将渲染为这样：

根据使用情况（是头像列表，还是水平导航按钮？），你可能不希望块之间包含空隙。你可能希望元素的两边彼此平齐，并且能够自己控制任何间距。

Firefox 开发者工具 HTML 检查器将突出显示文本节点，并准确显示这些元素所占用的面积。如果你想知道是什么原因造成的问题，并可能认为你在那里有一些额外的内边距或其他东西，那就很有用了！

![使用 Firefox 开发者工具 HTML 检查器显示块之间的空白符的示例](/zh-CN/docs/Web/API/Document_Object_Model/Whitespace/whitespace-devtools.png)

### 解决方案

有一些方法来解决这个问题：

使用弹性盒子来创建项目的水平列表，而不是尝试 `inline-block` 解决方案。这可以为你处理一切，而且绝对是首选的解决方案：

```css
ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  display: flex;
}
```

如果你确实需要依赖于 `inline-block`，你可以将列表的 `font-size` 设置为 0。这只有在你的区块不是用 em 确定大小（em 基于 `font-size`，所以区块大小最终也会是 0）的情况下才有效。rem 在这里是更好的选择：

```css
ul {
  font-size: 0;
  /\* … \*/
}

li {
  display: inline-block;
  width: 2rem;
  height: 2rem;
  /\* … \*/
}
```

或者，你可以在列表项中设置负内边距：

```css
li {
  display: inline-block;
  width: 2rem;
  height: 2rem;
  margin-right: -0.25rem;
}
```

你也可以通过把你的列表项放在源文件的同一行来解决这个问题，这将导致首先不创建空白节点：

```html
<li></li><li></li><li></li><li></li><li></li>
```

DOM 遍历与空白符
----------

当试图在 JavaScript 中进行 DOM 操作时，你也可能因为空白节点而遇到问题。例如，如果你有一个对父节点的引用，并想用 `Node.firstChild` 来影响它的第一个元素子节点，如果在打开的父标签之后有一个意外的空白节点，你将不会得到你所期望的结果。文本节点会被选中，而不是你想影响的元素。

再比如，如果你有一个特定的元素子集，你想根据它们是否为空（没有子节点）来做一些事情，你可以使用类似 `Node.hasChildNodes()` 的东西来检查每个元素是否为空，但同样，如果任何目标元素包含文本节点，你可能会得到错误的结果。

空白符辅助函数
-------

以下的 JavaScript 代码定义了许多函数，能够让你在处理 DOM 中的空白符时轻松点：

```js
/\*\*
 \* 以下所谓的“空白符”代表：
 \* "\t" TAB \u0009（制表符）
 \* "\n" LF \u000A（换行符）
 \* "\r" CR \u000D（回车符）
 \* " " SPC \u0020（真正的空格符）
 \*
 \* 不使用 JavaScript 的“\s”，因为它包含不断行空白字符等其他字符。
 \*/

/\*\*
 \* 测知某节点的文字内容是否全为空白。
 \*
 \* @param nod |CharacterData| 类的节点（如 |Text|、|Comment| 或 |CDATASection|）。
 \* @return 若 |nod| 的文字内容全为空白则返回 true，否则返回 false。
 \*/
function is\_all\_ws(nod) {
  return !/[^\t\n\r ]/.test(nod.textContent);
}

/\*\*
 \* 测知是否应在遍历时略过某节点。
 \*
 \* @param nod DOM1 |Node| 对象
 \* @return 若 |Text| 节点内仅有空白符或为 |Comment| 节点时，传回 true，
 \* 否则返回 false。
 \*/
function is\_ignorable(nod) {
  return (
    nod.nodeType === 8 || // 注释节点
    (nod.nodeType === 3 && is\_all\_ws(nod))
  ); // 所有字符都是空白字符的文本节点
}

/\*\*
 \* 此为会跳过空白符节点及注释节点的 |previousSibling| 函数
 \*（|previousSibling| 是 DOM 节点的特性值，为该节点的前一个节点。）
 \*
 \* @param sib 节点引用。
 \* @return 有两种可能：
 \* 1) |sib| 的最近前一个“不可忽略节点”，由 |is\_ignorable| 决定。或
 \* 2) 若该节点前无任何此类节点，则返回 null。
 \*/
function node\_before(sib) {
  while ((sib = sib.previousSibling)) {
    if (!is\_ignorable(sib)) return sib;
  }
  return null;
}

/\*\*
 \* 此为会跳过空白符节点及注释节点的 |nextSibling| 函数
 \*
 \* @param sib 节点引用。
 \* @return 有两种可能：
 \* 1) |sib| 的最近后一个“不可忽略节点”，由 |is\_ignorable| 决定。或
 \* 2) 若该节点后无任何此类节点，则返回 null。
 \*/
function node\_after(sib) {
  while ((sib = sib.nextSibling)) {
    if (!is\_ignorable(sib)) {
      return sib;
    }
  }
  return null;
}

/\*\*
 \* 此为会跳过空白符节点及注释节点的 |lastChild| 函数
 \*（|lastChild| 是 DOM 节点的特性值，为该节点之中最后一个子节点。）
 \*
 \* @param par 节点引用。
 \* @return 有两种可能：
 \* 1) |par| 中最后一个“不可忽略的子节点”，由 |is\_ignorable| 决定。或
 \* 2) 若该节点中无任何此类子节点，则返回 null。
 \*/
function last\_child(par) {
  let res = par.lastChild;
  while (res) {
    if (!is\_ignorable(res)) {
      return res;
    }
    res = res.previousSibling;
  }
  return null;
}

/\*\*
 \* 此为会跳过空白符节点及注释节点的 |firstChild| 函数
 \*
 \* @param par 节点引用。
 \* @传回值 有两种可能：
 \* 1) |par| 中第一个“不可忽略的子节点”，由 |is\_ignorable| 决定。或
 \* 2) 若该节点中无任何此类子节点，则返回 null。
 \*/
function first\_child(par) {
  let res = par.firstChild;
  while (res) {
    if (!is\_ignorable(res)) {
      return res;
    }
    res = res.nextSibling;
  }
  return null;
}

/\*\*
 \* 此为返回不包含文字节点的首尾所有空白符、
 \* 并将两个以上的空白符缩减为一个的 |data| 函数。
 \*（|data| 是 DOM 文字节点的特性值，为该文字节点中的文本。）
 \*
 \* @param txt 欲返回其中数据的文字节点
 \* @return 文字节点的内容，其中空白符已依前述方式处理。
 \*/
function data\_of(txt) {
  let data = txt.textContent;
  data = data.replace(/[\t\n\r ]+/g, " ");
  if (data[0] === " ") {
    data = data.substring(1, data.length);
  }
  if (data[data.length - 1] === " ") {
    data = data.substring(0, data.length - 1);
  }
  return data;
}
```

示例
--

下面的代码演示了上面这些工具函数的使用方法。具体操作是，遍历一个子节点全部为元素节点的元素，找到所包含的第一个节点为一个文本内容为“`This is the third paragraph`”的文本节点的那个子元素，并修改该子元素的 class 属性及其第一个文本节点的文字内容。

```js
let cur = first\_child(document.getElementById("test"));
while (cur) {
  if (data\_of(cur.firstChild) === "This is the third paragraph.") {
    cur.className = "magic";
    cur.firstChild.textContent = "This is the magic paragraph.";
  }
  cur = node\_after(cur);
}
```


使用 DOM 进行 web 和 XML 开发的示例 - Web API 接口参考

使用 DOM 进行 web 和 XML 开发的示例
=========================

本章提供了一些介绍如何使用 DOM 进行 Web 以及 XML 开发的较长示例。在可能的情况下，示例只使用 JavaScript 中较常见的 API、技巧以及模式来操作文档对象。

示例 1：高度和宽度
----------

下面的示例展示了不同尺寸的图片对 `height` 和 `width` 属性的使用：

```html
<!doctype html>
<html lang="zh-CN">
  <head>
    <title>宽度/高度示例</title>
    <script>
 function init() {
 const arrImages = new Array(3);

 arrImages[0] = document.getElementById("image1");
 arrImages[1] = document.getElementById("image2");
 arrImages[2] = document.getElementById("image3");

 const objOutput = document.getElementById("output");
 let strHtml = "<ul>";

 for (let i = 0; i < arrImages.length; i++) {
 strHtml +=
 "<li>图片" +
 (i + 1) +
 ": height=" +
 arrImages[i].height +
 ", width=" +
 arrImages[i].width +
 ", style.height=" +
 arrImages[i].style.height +
 ", style.width=" +
 arrImages[i].style.width +
 "<\/li>";
 }

 strHtml += "<\/ul>";

 objOutput.innerHTML = strHtml;
 }
 </script>
  </head>
  <body onload="init();">
    <p>
      图片 1：无高度、宽度及样式
      <img id="image1" src="http://www.mozilla.org/images/mozilla-banner.gif" />
    </p>

    <p>
      图片 2：height="50"，width="500"，无样式
      <img
 id="image2"
 src="http://www.mozilla.org/images/mozilla-banner.gif"
 height="50"
 width="500" />
    </p>

    <p>
      图片 3：无高度、宽度，样式中设置了“height: 50px; width: 500px;”
      <img
 id="image3"
 src="http://www.mozilla.org/images/mozilla-banner.gif"
 style="height: 50px; width: 500px;" />
    </p>

    <div id="output"></div>
  </body>
</html>
```

示例 2：图片属性
---------

```html
<!doctype html>
<html lang="zh-CN">
  <head>
    <title>修改图片的边框</title>

    <script>
 function setBorderWidth(width) {
 document.getElementById("img1").style.borderWidth = width + "px";
 }
 </script>
  </head>

  <body>
    <p>
      <img
 id="img1"
 src="image1.gif"
 style="border: 5px solid green;"
 width="100"
 height="100"
 alt="border test" />
    </p>

    <form name="FormName">
      <input
 type="button"
 value="设置边框为 20px 宽"
 onclick="setBorderWidth(20);" />
      <input
 type="button"
 value="设置边框为 5px 宽"
 onclick="setBorderWidth(5);" />
    </form>
  </body>
</html>
```

示例 3：修改样式
---------

在这个简单的例子中，HTML 段落元素的一些基本样式属性是使用该元素上的样式对象和该对象的 CSS 样式属性来访问的，这些对象可以从 DOM 中检索和设置。在这个示例中，你是直接操作各个样式的。在下一个例子中（见示例 4），你可以使用样式表及其规则来改变整个文档的样式。

```html
<!doctype html>
<html lang="zh-CN">
  <head>
    <title>改变颜色和字体大小的示例</title>

    <script>
 function changeText() {
 const p = document.getElementById("pid");

 p.style.color = "blue";
 p.style.fontSize = "18pt";
 }
 </script>
  </head>
  <body>
    <p id="pid" onclick="window.location.href = 'http://www.cnn.com/';">
      链接文字
    </p>

    <form>
      <p><input value="rec" type="button" onclick="changeText();" /></p>
    </form>
  </body>
</html>
```

示例 4：使用样式表
----------

`document` 对象的 `styleSheets` 属性返回该文档中已加载的样式表列表。你可以使用 styleheet、style 和 `CSSRule` 对象单独访问这些样式表及其规则，如本例所示，它将所有的样式规则选择器打印到控制台。

```js
const ss = document.styleSheets;

for (let i = 0; i < ss.length; i++) {
  for (let j = 0; j < ss[i].cssRules.length; j++) {
    dump(`${ss[i].cssRules[j].selectorText}\n`);
  }
}
```

下面的是一个只定义了三条样式规则的单个样式表的文档：

```css
body {
  background-color: darkblue;
}
p {
  font-family: Arial;
  font-size: 10pt;
  margin-left: 0.125in;
}
#lumpy {
  display: none;
}
```

该脚本的输出是这样的：
```

BODY
P
#LUMPY
```

示例 5：事件传播
---------

本实例以一种非常简单的方式演示了事件是如何在 DOM 中启动和处理的。当这个 HTML 文档的 BODY 加载时，一个事件监听器被注册在 TABLE 的顶行。事件监听器通过执行函数 stopEvent 来处理事件，它改变了表格底部单元格的值。

然而，stopEvent 也调用了事件对象方法 `event.stopPropagation`，它使事件不再向 DOM 中冒泡。请注意，表格本身有一个 `onclick` 事件处理器，当表格被点击时应该显示一条信息。但是 stopEvent 方法已经停止了传播，所以在表格中的数据被更新后，事件阶段实际上已经结束，并且显示了一个警报框来确认这一点。

```html
<!doctype html>
<html lang="zh-CN">
  <head>
    <title>事件传播</title>

    <style>
 #t-daddy {
 border: 1px solid red;
 }
 #c1 {
 background-color: pink;
 }
 </style>

    <script>
 function stopEvent(event) {
 const c2 = document.getElementById("c2");
 c2.textContent = "hello";

 // 这应该阻止 t-daddy 接收点击事件
 event.stopPropagation();
 alert("事件传递终止");
 }

 function load() {
 const elem = document.getElementById("tbl1");
 elem.addEventListener("click", stopEvent, false);
 }
 </script>
  </head>

  <body onload="load();">
    <table id="t-daddy" onclick="alert('hi');">
      <tr id="tbl1">
        <td id="c1">一</td>
      </tr>
      <tr>
        <td id="c2">二</td>
      </tr>
    </table>
  </body>
</html>
```

示例 6：getComputedStyle
---------------------

这个例子演示了如何使用 `window.getComputedStyle` 方法来获取一个元素的样式，这些样式不是用 `style` 属性或用 JavaScript 设置的（例如 `elt.style.backgroundColor="rgb(173, 216, 230)"`）。后面这些类型的样式可以用更直接的 `elt.style` 属性来检索，其属性在 DOM CSS 属性列表中列出。

`getComputedStyle()` 返回一个 `CSSStyleDeclaration` 对象，其各个样式属性可以用这个对象的 `getPropertyValue()` 方法进行引用，正如下面的示例文档所示。

```html
<!doctype html>
<html lang="zh-CN">
  <head>
    <title>getComputedStyle 示例</title>

    <script>
 function cStyles() {
 const RefDiv = document.getElementById("d1");
 const txtHeight = document.getElementById("t1");
 const h\_style = document.defaultView
 .getComputedStyle(RefDiv, null)
 .getPropertyValue("height");

 txtHeight.value = h\_style;

 const txtWidth = document.getElementById("t2");
 const w\_style = document.defaultView
 .getComputedStyle(RefDiv, null)
 .getPropertyValue("width");

 txtWidth.value = w\_style;

 const txtBackgroundColor = document.getElementById("t3");
 const b\_style = document.defaultView
 .getComputedStyle(RefDiv, null)
 .getPropertyValue("background-color");

 txtBackgroundColor.value = b\_style;
 }
 </script>

    <style>
 #d1 {
 margin-left: 10px;
 background-color: rgb(173, 216, 230);
 height: 20px;
 max-width: 20px;
 }
 </style>
  </head>

  <body>
    <div id="d1"> </div>

    <form action="">
      <p>
        <button type="button" onclick="cStyles();">getComputedStyle</button>
        height<input id="t1" type="text" value="1" /> max-width<input
 id="t2"
 type="text"
 value="2" />
        bg-color<input id="t3" type="text" value="3" />
      </p>
    </form>
  </body>
</html>
```

示例 7：显示事件对象的属性
--------------

本示例使用 DOM 方法来显示 `onload` `event` 对象的所有属性及其在表格中的值。它还展示了一种有用的技术，即使用 `for...in` 循环来迭代对象的属性以获取其值。

事件对象的属性在不同的浏览器之间有很大的不同，WHATWG DOM 标准列出了标准的属性，然而许多浏览器对这些属性做了很大的扩展。

把下面的代码放到一个空白的文本文件中，并把它加载到各种浏览器中，你会对不同数量和名称的属性感到惊讶。你可能还想在页面中添加一些元素，从不同的事件处理程序中调用这个函数。

```html
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>显示事件的属性</title>

    <style>
 table {
 border-collapse: collapse;
 }
 thead {
 font-weight: bold;
 }
 td {
 padding: 2px 10px 2px 10px;
 }

 .odd {
 background-color: #efdfef;
 }
 .even {
 background-color: #ffffff;
 }
 </style>

    <script>
 function showEventProperties(e) {
 function addCell(row, text) {
 const cell = row.insertCell(-1);
 cell.appendChild(document.createTextNode(text));
 }

 const event = e || window.event;
 document.getElementById("eventType").innerHTML = event.type;

 const table = document.createElement("table");
 const thead = table.createTHead();
 let row = thead.insertRow(-1);
 const labelList = ["#", "Property", "Value"];
 const len = labelList.length;

 for (let i = 0; i < len; i++) {
 addCell(row, labelList[i]);
 }

 const tbody = document.createElement("tbody");
 table.appendChild(tbody);

 for (const p in event) {
 row = tbody.insertRow(-1);
 row.className = row.rowIndex % 2 ? "odd" : "even";
 addCell(row, row.rowIndex);
 addCell(row, p);
 addCell(row, event[p]);
 }

 document.body.appendChild(table);
 }

 window.onload = (event) => {
 showEventProperties(event);
 };
 </script>
  </head>

  <body>
    <h1>DOM <span id="eventType"></span> 事件对象的属性列表</h1>
  </body>
</html>
```

示例 8：使用 DOM Table 接口
--------------------

DOM `HTMLTableElement` 接口提供了一些用于创建和操作表格的便利方法。两个经常使用的方法是 `HTMLTableElement.insertRow` (en-US) 和 `HTMLTableRowElement.insertCell` (en-US)。

要增加一行和一些单元格到现有的表：

```html
<table id="table0">
  <tr>
    <td>Row 0 Cell 0</td>
    <td>Row 0 Cell 1</td>
  </tr>
</table>

<script>
 const table = document.getElementById("table0");
 const row = table.insertRow(-1);
 let cell;
 let text;

 for (let i = 0; i < 2; i++) {
 cell = row.insertCell(-1);
 text = "Row " + row.rowIndex + " Cell " + i;
 cell.appendChild(document.createTextNode(text));
 }
</script>
```

### 备注

* 表格的 `innerHTML` 属性不应该被用来修改表格，即使你可以用它来写一个完整的表格或单元格的内容。
* 如果使用 DOM 核心方法 `document.createElement` 和 `Node.appendChild` 来创建行和单元格，IE 要求它们被追加到 `<tbody>` 元素，而其他浏览器将允许追加到 `<table>` 元素（行将被添加到最后的 `<tbody>` 元素）。
* 还有一些属于 `HTMLTableElement` 接口的便利方法，可以用来创建和修改表格。


DOM 概述 - Web API 接口参考

DOM 概述
======

**文档对象模型**（*DOM*）是 web 上构成文档结构和内容的对象的数据表示。本指南将介绍 DOM，研究 DOM 如何在内存中表示 HTML 文档以及如何使用 API 来创建 web 内容和应用程序。

什么是 DOM？
--------

文档对象模型（DOM）是一个网络文档的编程接口。它代表页面，以便程序可以改变文档的结构、风格和内容。DOM 将文档表示为节点和对象；这样，编程语言就可以与页面交互。

网页是一个既可以在浏览器窗口中显示，也可以作为 HTML 源代码的文档。在这两种情况下，它都是同一个文档，但文档对象模型（DOM）的表示方式使它可以被操作。作为一个面向对象的网页表示，它可以用脚本语言（如 JavaScript）进行修改。

例如，DOM 中指定下面代码中的 `querySelectorAll` 方法必须要返回文档中所有 `<p>` 元素组成的列表：

```js
const paragraphs = document.querySelectorAll("p");
// paragraphs[0] 是第一个 <p> 元素
// paragraphs[1] 是第二个 <p> 元素，依此类推
alert(paragraphs[0].nodeName);
```

所有可用于操作和创建网页的属性、方法和事件都被组织到对象中。例如，代表文档本身的 `document` 对象，任何实现 `HTMLTableElement` 访问 HTML 表格的 `table` 对象等，都是对象。

DOM 是使用多个 API 构建的，它们一起工作。核心 DOM 定义了描述任何文档和其中对象的实体。其他 API 会根据需要对其进行扩展，为 DOM 增加新的特性和功能。例如，HTML DOM API 为核心 DOM 增加了对表示 HTML 文档的支持，而 SVG API 增加了对表示 SVG 文档的支持。

DOM 和 JavaScript
----------------

上一个简短的例子，和几乎所有的例子一样，是 JavaScript。也就是说，它是用 JavaScript *写*的，但却*使用* DOM 来访问文档及其元素。DOM 不是一种编程语言，但如果没有它，JavaScript 语言就不会有任何关于网页、HTML 文档、SVG 文档及其组成部分的模型或概念。文档作为一个整体，标题、文档中的表格、表头、表格单元中的文本以及文档中的所有其他元素都是该文档对象模型的一部分。它们都可以使用 DOM 和像 JavaScript 这样的脚本语言进行访问和操作。

DOM 不是 JavaScript 语言的一部分，而是用于建立网站的 Web API。JavaScript 也可以在其他情况下使用。例如，Node.js 在计算机上运行 JavaScript 程序，但提供了一套不同的 API，而且 DOM API 不是 Node.js 运行时的核心部分。

DOM 被设计成与特定编程语言相独立，使文档的结构化表述可以通过单一，一致的 API 获得。尽管我们在本参考文档中会专注于使用 JavaScript，但 DOM 也可以使用其他的语言来实现，以 Python 为例，代码如下：

```python
# Python DOM 示例
import xml.dom.minidom as m
doc = m.parse(r"C:\Projects\Py\chap1.xml")
doc.nodeName # 文档对象的 DOM 属性
p_list = doc.getElementsByTagName("para")
```

要获取更多在网页上使用 JavaScript 的信息，请参阅 JavaScript 技术概述。

访问 DOM
------

在使用 DOM 时，不需要做任何其他特殊的操作。你可以在*脚本*中直接使用 JavaScript 的 API，这是一个由浏览器运行的程序。

当你创建一个脚本时，无论是内联到 `<script>` 元素中还是包含在网页中，你都可以立即开始使用 `document` 或 `window` 对象的 API 来操作文档本身，或网页中的任何元素（文档的子元素）。你的 DOM 编程可能就像下面这个示例一样简单，通过使用 `console.log()` 函数在控制台显示一条信息：

```html
<body onload="console.log('欢迎来到我的主页！');">
  …
</body>
```

由于一般不建议将页面的结构（用 HTML 编写）和对 DOM 的操作（用 JavaScript 编写）混在一起，所以 JavaScript 部分将在这里分组，并与 HTML 分开。

例如，以下函数创建了一个新的 h1 元素，向该元素添加文本，然后将其添加到文档的树中：

```html
<html lang="zh-CN">
  <head>
    <script>
 // 当文档加载时，运行这段函数
 window.onload = () => {
 // 在空 HTML 页面中创建一系列元素
 const heading = document.createElement("h1");
 const headingText = document.createTextNode("Big Head!");
 heading.appendChild(headingText);
 document.body.appendChild(heading);
 };
 </script>
  </head>
  <body></body>
</html>
```

基本的数据类型
-------

本页试图用简单的语言描述各种对象和类型。但是，有一些不同的数据类型在 API 中传递，你应该注意到。

**备注：** 因为绝大多数使用 DOM 的代码都是围绕着操作 HTML 文档进行的，所以我们通常把 DOM 中的节点称为**元素**（element），尽管严格来说并不是每个节点都是一个元素。

下面的表格大致描述了这些数据类型。

| 数据类型（接口） | 描述 |
| --- | --- |
| `Document` |
 当一个成员返回 `document` 对象（例如，元素的
 `ownerDocument` 属性返回它所属的
 `document`），这个对象就是 root
 `document` 对象本身。DOM `document` 参考一章对 `document` 对象进行了描述。
  |
| `Node` | 位于文档中的每个对象都是某种类型的节点。在一个 HTML 文档中，一个对象可以是一个元素节点，也可以是一个文本节点或属性节点。 |
| `Element` | `element` 类型是基于 `node` 的。它指的是一个元素或一个由 DOM API 的成员返回的 `element` 类型的节点。例如，我们不说 `document.createElement()` 方法返回一个 `node` 的对象引用，而只是说这个方法返回刚刚在 DOM 中创建的 `element`。`element` 对象实现了 DOM 的 `Element` 接口和更基本的 `Node` 接口，这两个接口都包含在本参考中。在 HTML 文档中，元素通过 HTML DOM API 的 `HTMLElement` 接口以及其他描述特定种类元素能力的接口（例如用于 `<table>` 元素的 `HTMLTableElement` 接口）进一步强化。 |
| `NodeList` | `nodeList` 是由元素组成的数组，如同
 `document.querySelectorAll()`
 等方法返回的类型。`nodeList` 中的条目通过索引有两种方式进行访问：
 * list.item(1)
* list[1]

两种方式是等价的，第一种方式中 `item()` 是
 `nodeList` 对象中的单独方法。后面的方式则使用了经典的数组语法来获取列表中的第二个条目。
  |
| `Attr` |
 当 `attribute` 通过成员函数（例如通过 `createAttribute()`
 方法）返回时，它是一个为属性暴露出专门接口的对象引用。DOM
 中的属性也是节点，就像元素一样，只不过你可能会很少使用它。
  |
| `NamedNodeMap` | `namedNodeMap` 和数组类似，但是条目是由名称或索引访问的，虽然后一种方式仅仅是为了枚举方便，因为在 list
 中本来就没有特定的顺序。出于这个目的， `namedNodeMap` 有一个 item() 方法，你也可以从 `namedNodeMap` 添加或移除条目。
  |

还有一些常见的术语需要记住。例如，通常把任何 `Attr` 节点称为 `attribute`，把 DOM 节点组成的数组称为 `nodeList`。你会发现这些术语和其他术语将在整个文档中被介绍和使用。

DOM 接口
------

本指南是关于对象和你可以用来操作 DOM 层次结构的实际*事物*。在很多地方，理解这些东西的工作方式会让人感到困惑。例如，代表 HTML `form` 元素的对象从 `HTMLFormElement` 接口获得其 `name` 属性，但其 `className` 属性来自 `HTMLElement` 接口。在这两种情况下，你想要的属性都在那个表单对象中。

但是对象和它们在 DOM 中实现的接口之间的关系可能会令人困惑，因此本节试图讲述一些关于 DOM 规范中的实际接口以及它们如何被提供的内容。

### 接口及对象

许多对象实现了几个不同的接口。例如，table 对象实现了一个专门的 `HTMLTableElement` 接口，其中包括诸如 `createCaption` 和 `insertRow` 等方法。但由于它也是一个 HTML 元素，所以 `table` 实现了 DOM `Element` 参考章节中描述的 `Element` 接口。最后，因为就 DOM 而言，HTML 元素也是构成 HTML 或 XML 页面对象模型的节点树中的一个节点，所以表格对象也实现了更基本的 `Node` 接口，`Element` 就是从这个接口衍生出来的。

正如下面的示例，当你得到一个 `table` 对象的引用时，你经常会轮流使用对象实现的三个不同接口的方法，但并不知其所以然。

```js
const table = document.getElementById("table");
const tableAttrs = table.attributes; // Node/Element 接口
for (let i = 0; i < tableAttrs.length; i++) {
  // HTMLTableElement 接口：border 属性
  if (tableAttrs[i].nodeName.toLowerCase() === "border") {
    table.border = "1";
  }
}
// HTMLTableElement 接口：summary 属性
table.summary = "note: increased border";
```

### DOM 中的核心接口

本节列出了 DOM 中一些最常用的接口。我们的想法不是在这里描述这些 API 的作用，而是让你了解在你使用 DOM 时经常会看到的各种方法和属性。这些常用的 API 在本节最后的 DOM 实例一章中的较长的示例中使用。

`document` 和 `window` 对象是你在 DOM 编程中最常使用的接口对象。简单来说，`window` 对象代表类似浏览器的东西，而 `document` 对象是文档本身的根。`Element` 继承自通用的 `Node` 接口，这两个接口一起提供了许多你在单个元素上使用的方法和属性。这些元素也可以有特定的接口来处理这些元素持有的数据种类，如上一节中的 `table` 对象的示例。

下面是在 web 和 XML 页面脚本中使用 DOM 时，一些常用的 API 简要列表。

* `document.querySelector()`
* `document.querySelectorAll()`
* `document.createElement()`
* `Element.innerHTML`
* `Element.setAttribute()`
* `Element.getAttribute()`
* `EventTarget.addEventListener()`
* `HTMLElement.style`
* `Node.appendChild()`
* `window.onload`
* `window.scrollTo()`
示例
--

### 设置文本内容

本实例使用了包含了一个 `<textarea>` 和两个 `<button>` 元素的 `<div>` 元素。当用户点击第一个按钮时，我们在 `<textarea>` 中放置一些文本。当用户点击第二个按钮时，我们清除文本内容。我们使用：

* `Document.querySelector()` 来访问 `<textarea>` 和按钮
* `EventTarget.addEventListener()` 来监听按钮的点击事件
* `Node.textContent` 来设置和清除文本

#### HTML

```html
<div class="container">
  <textarea class="story"></textarea>
  <button id="set-text" type="button">设置文本内容</button>
  <button id="clear-text" type="button">清除文本内容</button>
</div>
```

#### CSS

```css
.container {
  display: flex;
  gap: 0.5rem;
  flex-direction: column;
}

button {
  width: 200px;
}
```

#### JavaScript

```js
const story = document.body.querySelector(".story");

const setText = document.body.querySelector("#set-text");
setText.addEventListener("click", () => {
  story.textContent = "It was a dark and stormy night...";
});

const clearText = document.body.querySelector("#clear-text");
clearText.addEventListener("click", () => {
  story.textContent = "";
});
```

#### 结果

### 添加子元素

本实例使用了包含了一个 `<div>` 和两个 `<button>` 元素的 `<div>` 元素。当用户点击第一个按钮时，我们创建一个新的元素，并添加到 `<div>` 的子元素。当用户点击第二个按钮时，我们移除那个子元素。我们使用：

* `Document.querySelector()` 来访问 `<div>` 和按钮
* `EventTarget.addEventListener()` 来监听按钮点击事件
* `Document.createElement` 来创建元素
* `Node.appendChild()` 来添加子元素
* `Node.removeChild()` 来移除子元素

#### HTML

```html
<div class="container">
  <div class="parent">父元素</div>
  <button id="add-child" type="button">添加子元素</button>
  <button id="remove-child" type="button">移除子元素</button>
</div>
```

#### CSS

```css
.container {
  display: flex;
  gap: 0.5rem;
  flex-direction: column;
}

button {
  width: 100px;
}

div.parent {
  border: 1px solid black;
  padding: 5px;
  width: 100px;
  height: 100px;
}

div.child {
  border: 1px solid red;
  margin: 10px;
  padding: 5px;
  width: 80px;
  height: 60px;
  box-sizing: border-box;
}
```

#### JavaScript

```js
const parent = document.body.querySelector(".parent");

const addChild = document.body.querySelector("#add-child");
addChild.addEventListener("click", () => {
  // 只在文本节点“父节点”没有子节点时添加一个子节点
  if (parent.childNodes.length > 1) {
    return;
  }
  const child = document.createElement("div");
  child.classList.add("child");
  child.textContent = "子节点";
  parent.appendChild(child);
});

const removeChild = document.body.querySelector("#remove-child");
removeChild.addEventListener("click", () => {
  const child = document.body.querySelector(".child");
  parent.removeChild(child);
});
```

#### 结果

规范
--

| Specification |
| --- |
| DOM Standard  |


使用 JavaScript 和 DOM 接口遍历 HTML 表格 - Web API 接口参考

使用 JavaScript 和 DOM 接口遍历 HTML 表格
================================

本文概述了一些强大的，基本的 DOM 1 级别中的方法以及如何在 JavaScript 中使用它们。你将会如何动态地创建、访问、控制以及移除 HTML 元素。这里提到的 DOM 方法，并非是 HTML 专有的；它们在 XML 中同样适用。这里提供的演示在任何现代浏览器中都能正常工作。

**备注：** 这里介绍的 DOM 方法是文档对象模型 1 级规范核心的一部分。DOM 1 级既包括通用的文档访问和操作的方法（DOM 1 核心），也包括专门针对 HTML 文档的方法（DOM 1 HTML）。

动态创建 HTML 表格
------------

### 示例

在本示例中，当按钮被点击时，会向页面中添加一个表格。

#### HTML

```html
<input type="button" value="生成表格" onclick="generateTable()" />
```

#### JavaScript

```js
function generateTable() {
  // 创建一个 <table> 元素和一个 <tbody> 元素
  const tbl = document.createElement("table");
  const tblBody = document.createElement("tbody");

  // 创建单元格
  for (let i = 0; i < 2; i++) {
    // 创建一行
    const row = document.createElement("tr");

    for (let j = 0; j < 2; j++) {
      // 创建 <td> 元素和文本节点，文本节点是 <td> 的内容，并将 <td>
      // 放在表格最后一行
      const cell = document.createElement("td");
      const cellText = document.createTextNode(`cell in row ${i}, column ${j}`);
      cell.appendChild(cellText);
      row.appendChild(cell);
    }

    // 将该行添加到表格的末尾
    tblBody.appendChild(row);
  }
  // 将 <tbody> 放置在 <table> 内
  tbl.appendChild(tblBody);
  // 将 <table> 放置在 <body> 内
  document.body.appendChild(tbl);
  // 将 tbl 的 border 属性设置为 '2'
  tbl.setAttribute("border", "2");
}
```

```
table {
  margin: 1rem auto;
}

td {
  padding: 0.5rem;
}
```

#### 结果

### 解释

注意我们创建元素和文本节点的顺序：

1. 首先我们创建了 `<table>` 元素。
2. 然后，我们创建了 `<table>` 的子元素 `<tbody>`。
3. 然后，我们使用循环语句创建了 `<tbody>` 的子元素，`<tr>`。
4. 对于每一个 `<tr>` 元素，我们使用一个循环语句创建它的子元素 `<td>`。
5. 对于每一个 `<td>` 元素，我们创建单元格内的文本节点。

现在，我们创建了 `<table>`、`<tbody>`、`<tr>` 和 `<td>` 等元素，然后创建了文本节点；接下来，我们将每一个对象逆序地接在各自的父节点上：

1. 首先，我们使用这段代码将每一个文本节点接在 `<td>` 元素上。

```js
cell.appendChild(cellText);
```

2. 然后，我们将每一个 `<td>` 元素接在它的父元素 `<tr>` 上。

```js
row.appendChild(cell);
```

3. 然后，我们将每一个 `<tr>` 元素接在它的父元素 `<tbody>` 上。

```js
tblBody.appendChild(row);
```

4. 下一步，我们将 `<tbody>` 元素接在它的父元素 `<table>` 上。

```js
tbl.appendChild(tblBody);
```

5. 最后，我们将 `<table>` 元素接在它的父元素 `<body>` 上。

```js
document.body.appendChild(tbl);
```

请记住这个机制。你将会在 W3C DOM 编程中经常使用它。首先，你从上到下的创建元素；然后你从下向上的将子元素接在他们的父元素上。

下面是由 JavaScript 代码生成的 HTML 代码：

```html
<table border="2">
  <tbody>
    <tr>
      <td>cell is row 0 column 0</td>
      <td>cell is row 0 column 1</td>
    </tr>
    <tr>
      <td>cell is row 1 column 0</td>
      <td>cell is row 1 column 1</td>
    </tr>
  </tbody>
</table>
```

下面是由代码生成的 `<table>` 及其子元素的 DOM 对象树：

![一个 DOM 对象树是如何从主元素和它的子元素生成的](/zh-CN/docs/Web/API/Document_Object_Model/Traversing_an_HTML_table_with_JavaScript_and_DOM_Interfaces/sample1-tabledom.jpg)

你只需使用一些 DOM 方法就可以建立这个表及其内部子元素。记住要牢记你打算创建的结构的树形模型；这将使你更容易写出必要的代码。在图 1 的 `<table>` 树中，元素 `<table>` 有一个子节点：元素 `<tbody>`。`<tbody>` 有两个子节点。每个 `<tbody>` 的子节点（`<tr>`）有两个子节点（`<td>`）。最后，每个 `<td>` 有一个子节点：一个文本节点。

设置段落的背景颜色
---------

### 示例

在本示例中，当按钮被点击时，段落的背景颜色将会改变。

#### HTML

```html
<body>
  <input type="button" value="设置段落背景颜色" onclick="setBackground()" />
  <p>hi</p>
  <p>hello</p>
</body>
```

#### JavaScript

```js
function setBackground() {
  // 获取文档中所有的 p 元素
  const paragraphs = document.getElementsByTagName("p");

  // 从列表中获取第二个元素
  const secondParagraph = paragraphs[1];

  // 设置内联样式
  secondParagraph.style.background = "red";
}
```

#### 结果

### 解释

`getElementsByTagName(tagNameValue)` 是任何 DOM `Element` 或根 `Document` 元素中的一个方法。当被调用时，它返回一个数组，其中包含所有与标签名称相匹配的元素的后代。列表中的第一个元素位于数组中的 `[0]` 位置。

我们进行了以下步骤：

1. 首先，我们获取了文档中所有的 `p` 元素：

```js
const paragraphs = document.getElementsByTagName("p");
```

2. 然后，我们从 `p` 元素的列表中获取了第二个段落元素：

```js
const secondParagraph = paragraphs[1];
```

![在 DOM 树中，一个段落元素作为一个新的子节点被添加到一个现有的段落中。](/zh-CN/docs/Web/API/Document_Object_Model/Traversing_an_HTML_table_with_JavaScript_and_DOM_Interfaces/sample2a2.jpg)
3. 最后，我们使用 `paragraph` 对象的 `style` 属性，将背景色设置为红色：

```js
secondParagraph.style.background = "red";
```

### 使用 document.createTextNode("..") 创建文本节点

使用文档对象来调用 `createTextNode` 方法并创建你自己的文本节点。你只需要传递文字内容给这个函数。返回的值就是一个代表那个文本节点信息的对象。

```js
myTextNode = document.createTextNode("world");
```

这表示你已经创建了一个 `TEXT_NODE`（一个文字片断）类型的节点，并且它的内容是 `"world"`，任何你对 `myTextNode` 的引用都指向这个节点对象。如果想将这个文本插入到 HTML 页面中，你还需要将它作为其他节点元素的子元素。

### 使用 appendChild(..) 插入元素

那么，通过调用 `secondParagraph.appendChild(node_element)` 你可以将这个元素设置成为第二个 `<p>` 元素的一个新的子元素。

```js
secondParagraph.appendChild(myTextNode);
```

在测试了这个例子之后，我们注意到，hello 和 world 单词被组合在了一起：helloworld。事实上，当你看到 HTML 页面时，hello 和 world 两个文字节点看起来更像是一个节点。但是请记住它们在文档模型中的形式是两个节点。第二个节点是一个 `TEXT_NODE` 类型的新节点，也是第二个 `<p>` 标签的第二个子元素。下面的示意图将在文档树中展示最近创建的文本节点对象。

![段落元素中的文本节点作为 DOM 树中的单独子元素](/zh-CN/docs/Web/API/Document_Object_Model/Traversing_an_HTML_table_with_JavaScript_and_DOM_Interfaces/sample2b2.jpg)

**备注：** `createTextNode()` 和 `appendChild()` 是在 *hello* 和 *world* 这两个词之间加入空白的简单方法。另一个重要的注意点是，`appendChild` 方法将在最后一个子节点之后追加子节点，就像 *world* 这个词已经被添加到 *hello* 这个词之后一样。因此，如果你想在 *hello* 和 *world* 之间追加一个文本节点，你需要使用 `insertBefore` 而不是 `appendChild`。

### 使用文档对象和 createElement(..) 方法创建新的元素

你可以使用 `createElement` 来创建新的 HTML 元素或者任何其他你想要的元素。比如，如果你想要创建一个新的 `<p>` 元素作为 `<body>` 的子元素，你可以使用前面例子的 `myBody` 并给它附加一个新的元素节点。使用 `document.createElement("tagname")` 可以方便的创建一个节点。如下：

```js
myNewPTagNode = document.createElement("p");
myBody.appendChild(myNewPTagNode);
```

![如何将一个新的节点元素附加到文档树内的文本节点对象上](/zh-CN/docs/Web/API/Document_Object_Model/Traversing_an_HTML_table_with_JavaScript_and_DOM_Interfaces/sample2c.jpg)

### 使用 removeChild(..) 方法移除节点

每一个节点都可以被移除。下面的代码从第二个 `<p>` 元素 `secondParagraph` 中移除文本节点 `myTextNode`（其中包含单词“world”）。

```js
secondParagraph.removeChild(myTextNode);
```

文本节点 `myTextNode`（包含单词“world”）仍然存在。下面的代码将 `myTextNode` 附加到最近创建的 `<p>` 元素，`myNewPTagNode`。

```js
myNewPTagNode.appendChild(myTextNode);
```

被修改的对象树的最后的状态如下：

![创建并向对象树文本结构追加一个新的节点元素](/zh-CN/docs/Web/API/Document_Object_Model/Traversing_an_HTML_table_with_JavaScript_and_DOM_Interfaces/sample2d.jpg)

动态创建一个表格（回到 Sample1.html）
-------------------------

这一段落的剩余部分我们将继续修改我们的 sample1.html。下面的图展示了我们在示例中创建的表格的对象树的结构。

### 回顾 HTML 表格结构

![添加新节点元素后的 HTML 表格对象树结构](/zh-CN/docs/Web/API/Document_Object_Model/Traversing_an_HTML_table_with_JavaScript_and_DOM_Interfaces/sample1-tabledom.jpg)

### 创建元素节点并将它们插入到文档树中

sample1.html 中创建表格的基本步骤是：

* 获得 body 对象（文档对象的第一个元素）
* 创建所有元素。
* 最后，根据表格结构（上面图中所示）将每一个孩子节点拼接起来。下面的一段源码是经过修改的 sample1.html

**备注：** 在 `start` 函数的最后，有一行新的代码。该表的 `border` 属性是用另一个 DOM 方法 `setAttribute()` 设置的。`setAttribute()` 有两个参数：属性名和属性值。你可以使用 `setAttribute` 方法设置任何元素的任何属性。

```html
<html lang="zh-CN">
  <head>
    <title>示例代码——使用 JavaScript 和 DOM 接口遍历 HTML 表格</title>
    <script>
 function start() {
 // 获取 body 的引用
 const myBody = document.getElementsByTagName("body")[0];

 // 创建 <table> 和 <tbody> 元素
 const myTable = document.createElement("table");
 const myTableBody = document.createElement("tbody");

 // 创建单元格
 for (let j = 0; j < 3; j++) {
 // 创建一个 <tr> 元素
 const myCurrentRow = document.createElement("tr");

 for (let i = 0; i < 4; i++) {
 // 创建一个 <td> 元素
 const myCurrentCell = document.createElement("td");
 // 创建文本节点
 const currentText = document.createTextNode(
 `cell is row ${j}, column ${i}`,
 );
 // 将文本节点附加至 <td> 中
 myCurrentCell.appendChild(currentText);
 // 将单元格 <td> 附加至行 <tr> 中
 myCurrentRow.appendChild(myCurrentCell);
 }
 // 将行 <tr> 附加至 <tbody> 中
 myTableBody.appendChild(myCurrentRow);
 }

 // 将 <tbody> 附加至 <table> 中
 myTable.appendChild(myTableBody);
 // 将 <table> 附加至 <body> 中
 myBody.appendChild(myTable);
 // 将 myTable 的 border 属性设为 2
 myTable.setAttribute("border", "2");
 }
 </script>
  </head>
  <body onload="start()"></body>
</html>
```

使用 CSS 和 DOM 来操作表格
------------------

### 从表格中获得一个文字节点

示例介绍了两个新的 DOM 属性。首先，使用 `childNodes` 属性来获得 myCell 的孩子节点列表。`childNodes` 列表包括所有的孩子节点，无论它们的名称或类型是什么。像 `getElementsByTagName()` 一样，它返回了一个节点列表。

不同的是，`getElementsByTagName()` 只返回指定标签名称的元素，且它会返回任何级别的后代，而不仅仅是直接子元素。

一旦你获得了返回的列表，你可以使用 `[x]` 方法来使用指定的元素。这个例子在表格的第二行第二个单元格中的 `myCellText` 中保存了一个文字节点。

然后，为了显示本例中的结果，它创建了一个新的文本节点，其内容是 `myCellText` 的数据，并将其作为 `<body>` 元素的一个子节点进行附加。

**备注：** 如果你的对象是一个文字节点，你可以使用 data 属性来获取节点的文字内容。

```js
myBody = document.getElementsByTagName("body")[0];
myTable = myBody.getElementsByTagName("table")[0];
myTableBody = myTable.getElementsByTagName("tbody")[0];
myRow = myTableBody.getElementsByTagName("tr")[1];
myCell = myRow.getElementsByTagName("td")[1];

// myCell 子节点列表中的第一个元素
myCellText = myCell.childNodes[0];

// currentText 的内容是 myCellText 的数据内容
currentText = document.createTextNode(myCellText.data);
myBody.appendChild(currentText);
```

### 获得属性的值

在 sample1 的最后我们在 `myTable` 对象上调用了 `setAttribute`。这个调用是用来设置表格的边框属性的。为了获取属性的值，需要使用 `getAttribute` 方法：

```js
myTable.getAttribute("border");
```

#### 通过改变样式属性来隐藏一列

一旦你在你的 JavaScript 变量中保存了一个对象，你就可以直接为它设置 `style` 属性。下面的代码是修改后的 sample1.html，在这里，第二列的每一个单元格都被隐藏了。而且第一列中的每一个单元格改为使用红色背景。注意，`style` 属性是被直接设置的。

```html
<html lang="zh-CN">
  <body onload="start()"></body>
  <script>
 function start() {
 const myBody = document.getElementsByTagName("body")[0];
 const myTable = document.createElement("table");
 const myTableBody = document.createElement("tbody");

 for (let row = 0; row < 2; row++) {
 const myCurrentRow = document.createElement("tr");
 for (let col = 0; col < 2; col++) {
 const myCurrentCell = document.createElement("td");
 const currentText = document.createTextNode(`cell is: ${row}${col}`);
 myCurrentCell.appendChild(currentText);
 myCurrentRow.appendChild(myCurrentCell);
 // 如果列值为 0，设置单元格背景颜色
 // 如果列值为 1，隐藏单元格
 if (col === 0) {
 myCurrentCell.style.background = "rgb(255, 0, 0)";
 } else {
 myCurrentCell.style.display = "none";
 }
 }
 myTableBody.appendChild(myCurrentRow);
 }
 myTable.appendChild(myTableBody);
 myBody.appendChild(myTable);
 }
 </script>
</html>
```


Audio() - Web API 接口参考

Audio()
=======

**`Audio()`** 构造器创建并返回一个 `HTMLAudioElement`，可以将它附加到文档中以供用户交互，也可以用于管理和播放背景音乐。

语法
--
```

audioObj = new Audio(url);
```

### 参数

`url` 可选

一个可选的、包含音频文件 URL 的 `DOMString`。

### 返回值

新创建的 `HTMLAudioElement` 对象，被设置为播放指定 `url`的音频文件。新对象的 `preload` 属性被设置为 `auto` 且它的 `src` 属性被设置为具体的 URL 或 `null` （当没有提供 URL 时）。如果提供了 URL，浏览器在返回新对象之前开始异步加载媒体资源。

使用说明
----

你也可以使用其他元素创建方法，例如 `document` 对象的 `createElement()` 方法，去构建一个新的 `HTMLAudioElement`.

### 检测回放时机

提供三种方法给开发者，判断音频文件是否已经加载，允许开始回放：

* 检测 `readyState` 属性的值。如果值是 `HTMLMediaElement.HAVE_FUTURE_DATA`，说明有足够的数据开始回放并能至少播放一小段时间。如果值是 `HTMLMediaElement.HAVE_ENOUGH_DATA`，根据当前的下载速率，说明有足够的数据，你可以无干扰地播放至结束。
* 监听 `canplay` 事件。当足够的音频数据被发送至 `<audio>` 元素，能开始回放，即使可能出现中断时，会触发该事件。
* 监听 `canplaythrough` 事件。当音频应该没有干扰能够顺利播放时将会触发该事件。

基于事件的方法是最优的：

```js
myAudioElement.addEventListener("canplaythrough", (event) => {
  /\* 音频可以播放；如果权限允许则播放 \*/
  myAudioElement.play();
});
```

### 内存使用与管理

如果所有使用 `Audio()` 构造函数创建的 audio 元素被删除，根据 JavaScript 垃圾回收机制，如果播放正在进行，内存中的 audio 元素不会被移除。相反，音频将会继续播放并且它的对象会保留在内存中，直到播放结束或是被暂停（例如调用 `pause()`）。在那个时候，这个对象才会成为垃圾回收的目标。

规范
--

| Specification |
| --- |
| HTML Standard # dom-audio-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

The compatibility table on this page is generated from structured data. If you'd like to contribute to the data, please check out https://github.com/mdn/browser-compat-data and send us a pull request.

参见
--

* Web media technologies
* 实现了此接口的 HTML 元素：`<audio>`。


渐进式 Web 应用介绍 - 渐进式 Web 应用（PWA）

渐进式 Web 应用介绍
============

* Overview: js13kGames
* 下一页

这篇文章将会为你介绍渐进式 Web 应用（PWA），讨论一下它们到底是什么；相比于常规的 Web 应用，它又带来了哪些优势。

什么是渐进式 Web 应用？
--------------

PWA 指的是使用指定技术和标准模式来开发的 Web 应用，这同时赋予它们 Web 应用和原生应用的特性。

例如一方面，Web 应用更加易于发现：相比于安装应用，访问一个网站显然更加容易和迅速。你还可以通过链接来分享 Web 应用。

另一方面，原生应用与操作系统可以更加完美的整合，也因此为用户提供了无缝的用户体验。你可以通过安装应用使得它在离线的状态下也可以运行；相较于使用浏览器访问，用户也更喜欢通过点击主页上的图标来访问它们喜爱的应用。

PWA 赋予了我们创建同时拥有以上两种优势的应用的能力。

这并不是一个新概念；类似的想法已经在过去的 Web 平台上通过各种方法实现了多次。渐进式增强和响应式设计已经可以让我们构建对移动端友好的网站。在多年以前的 Firefox OS 的生态系统中，离线运行和安装 Web 应用就已经成为了可能。

除此之外，PWA 还提供了更多的特性，并且无需在 Web 已有的那些优秀特点上妥协。

什么使应用成为 PWA?
------------

正如前文所述，PWA 不是只使用一种技术创建的。它代表了构建 Web 应用程序的新理念，涉及一些特定的模式，API 和其他功能。一眼是看不出来一个 Web App 是不是 PWA 的。如果应用程序满足某些要求，或者实现了一组特定的功能，例如离线工作、可安装、易于同步、可以发送推送通知等，我们就可以将其视为 PWA。

此外，还有一些工具可以按百分比衡量应用的完整性。（Lighthouse目前是最受欢迎的工具）通过实施各种技术优势，我们可以使应用程序更加渐进式，从而最终获得更高的 Lighthouse 得分。但这只是一个粗略的指标。

辨别一个 Web 应用是否是 PWA 有一些关键原则。一个 PWA 应该具有以下特点：

* 可发现（Discoverable）, 可以通过搜索引擎发现。
* 可安装（Installable）, 可以出现在设备的主屏幕。
* 可链接（Linkable）, 可以简单地通过 URL 分享。
* 独立于网络（Network independent）, 可以在离线状态或者是在网速很差的情况下运行。
* 渐进式（Progressive）, 在老版本的浏览器仍旧可以使用，在新版本的浏览器上可以使用全部功能。
* 可重入（Re-engageable）, 无论何时有新的内容，都可以发送通知。
* 响应式（Responsive）, 在任何具有屏幕和浏览器的设备上可以正常使用——包括手机、平板电脑、笔记本、电视、冰箱等。
* 安全（Safe）, 在用户、应用和服务器之间的连接是安全的，第三方无法访问你的敏感数据。
### 这么做值得吗？

当然值得！只需要相对较小的代价就可以实现 PWA 的核心特性，而优势却是巨大的。例如：

* 减少应用安装后的加载时间，通过 Service Workers 来进行缓存，以此来节省带宽和时间。
* 当应用有可用的更新时，可以只更新发生改变的那部分内容。相比之下，对于一个原生应用而言，即便是最微小的改动也需要强制用户去再次下载整个应用。
* 外观和使用感受与原生平台更加融为一体——应用图标被放置在主屏幕上，应用可以全屏运行等。
* 凭借系统通知和推送消息与用户保持连接，对用户产生更多的吸引力，并且提高转换效率。

有许多知名的成功企业正在尝试 PWA 方案，选择增强的网站体验而不是原生应用。事实上它们也确实从中获得了显而易见的益处。PWA Stats 这个网站上分享了许多案例研究，可以证明以上提及的这些优势。

最著名的案例可能是印度最大的电子商务网站 Flipkart Lite，它在 2015 年重建为 PWA，转化率提高了 70%。AliExpress PWA 也得到了比 Web 应用或原生应用更好的效果，新用户的转换率提高了 104%。对比一下利润增长率和转换为 PWA 所需相对较少的工作量，PWA 的优势显而易见。

像 couponmoto 这样的早期新兴创业公司也开始使用 PWA 来推动更多的消费者参与，这表明 PWA 可以帮助小公司和大公司更有效地（重新）吸引用户。

你可以查看 pwa.rocks 上的列表以获取更多示例。特别值得一提的是 hnpwa.com，它列出了 Hacker News 网站的示例实现（而不是随处可见的 TodoMVC 应用程序），你可以在其中看到各种前端框架的使用。

你甚至可以使用 PWABuilder 网站在线生成 PWA。

对于 Service Worker 和消息推送，请务必查看 Service Worker 手册。这是一个在现代站点中使用 Service Worker 的方法集合。

PWA 非常值得一试，你可以自己查看它是否适用于你的应用程序。

浏览器支持
-----

如前所述，PWA 不依赖于单个 API，而是使用多种技术来实现提供最佳 Web 体验的目标。

PWA 所需的关键要素是 Service Worker 支持。值得庆幸的是，桌面和移动设备上的所有主流浏览器都支持 Service Worker。

至于其他功能，像是推送通知、通知功能和添加至主屏功能也得到了广泛的支持。目前，Safari 对 Web App Manifest 和添加至主屏的支持有限，并且不支持 Web 推送通知。但是，其他主流浏览器都支持这里的所有功能。

其中一些 API 是实验性的，文档仍在草稿中，但是 Flipkart 和 AliExpress 这样的成功案例应该也能说服你尝试在 Web 应用程序中实现一些 PWA 功能。

最重要的是，你应该遵循渐进增强理念：在客户端支持它们的情况下使用提供此类增强功能的技术，但如果客户端不支持，则仍然提供应用程序的基本功能。这样，应用对每个人都可用，但使用现代浏览器的人能更多地从 PWA 功能中受益。

一个示例应用程序
--------

在本系列文章中，我们将研究一个超级简单网站的源代码，该网站列出了 js13kGames 2017 竞赛中提交给 A-Frame category 的游戏的相关信息。你不必考虑网站上的实际内容，这里主要是学习如何在你自己的项目中使用 PWA 功能。

你可以在 mdn.github.io/pwa-examples/js13kpwa 找到在线版本（另请参阅源代码），我们将在接下来的几篇文章中对其进行详细解释。

现在，让我们转到本系列的第二部分，我们来看看这个示例应用程序的结构。

* Overview: js13kGames
* 下一页


经期跟踪器 - 渐进式 Web 应用（PWA）

经期跟踪器
=====

* 下一页

这个入门教程将带领你完成构建基础渐进式 Web 应用（PWA）的所有步骤。我们将使用 Web 技术——HTML、CSS 和 JavaScript——来构建一个名为“CycleTracker”（经期跟踪器）的月经周期跟踪 Web 应用。像所有的 Web 应用一样，经期跟踪器被设计为可以在所有设备的所有浏览器上工作。

我们将逐步构建一个功能完整的 Web 应用，然后逐步增强经期跟踪器，使其可安装并在用户离线时仍可工作。

默认情况下，PWA 是常规的网站，两者使用相同的技术构建。和常规网站一样，PWA 可被链接，可通过搜索引擎发现，并在浏览器中可见。通过包含一个清单文件和 service worker，并通过 SSL 提供网站服务，任何网站都可以成为 PWA。

PWA 的优势
-------

我们将使用 Web 语言创建一个功能完整的应用，该应用不论有无网络，在浏览器和用户的操作系统上都能运行。像任何常规网站一样，经期跟踪器托管在 Web 服务器上并可从 Web 服务器下载。我们需要的只是一个文本编辑器，就能让经期跟踪器像所有的 PWA 一样，不需要任何额外的编程语言知识，不需要打包也不需要专有的 SDK，无需应用商店（也无需应用商店的审核和费用）就可以无缝地安装在任何操作系统上。

使用标准和开放的 Web 技术

过去，为了使应用程序可在操作系统（如 Windows、iOS、MacOS、Linux 和 Android）上安装，应用程序需要用操作系统支持的编程语言（如 C#、.Net、Objective C、Swift、Kotlin、Java 或 Python）来开发。PWA 则基于一个不同的模型：它们使用单一的代码库，使用跨操作系统的标准的开放 Web 技术（HTML、CSS 和 JavaScript）编写。

无需编译

对于大多数编程语言——如常用于构建 Android 应用的 Java、C/C++ 和 Kotlin，以及用于构建 iOS 应用的 Objective-C 和 Swift——都需要将代码编译并打包成可安装的格式，如 exe、.dmg、.elf 和 .apk 或其他依赖于操作系统的可安装文件类型。根据语言，编译和打包可能需要操作系统的 SDK (en-US) 。PWA 使用每个操作系统都支持的 Web 技术，不需要打包或编译。是的，开发团队可以有复杂的构建系统，但是，正如我们将在构建经期跟踪器时展示的那样，PWA 可以仅通过 HTML 和 JavaScript 构建（以及 CSS，尽管样式不是 PWA 所必需的）。

随时随地可用

单一操作系统的应用程序通常是在专有的应用商店中通过下载向用户分发。他们通过像苹果 App Store、Google Play、Microsoft Store 或类似的供应商提供。PWA 没有这些要求。如果你想分发你的经期跟踪器应用，你不需要中间商，用户可以通过访问你的应用的线上版本来访问它。但如果你想要的话，在 Play Store 和 App Store 上分发你的 PWA 也是可行的，与其他 iOS 或 Android 应用无任何区别。

易于用户安装

过去，下载的单一操作系统应用程序必须由用户有意识地安装。根据操作系统、安装格式和下载源，这可能是一个多步骤的安装过程。PWA 的安装过程更为简便，任何人只要有支持 PWA 的浏览器就可以使用 PWA，并且只通过几次点击就能将其安装。

### 与操作系统安装的软件应用比较

一经安装，PWA 便可以做到与用户设备上安装的其他应用程序类似的显示和行为：

应用程序窗口

通过一个清单设置，我们将使经期跟踪器在其自己的应用程序窗口中打开，这意味着安装的 PWA 与其他安装的应用程序类似。

应用程序图标

PWA 在操作系统的应用图标显示的同一位置显示其应用程序图标。这可以是主屏幕上的图标、工具栏中的图标、应用程序文件夹中的图标，或者任何设备显示应用程序图标的地方。我们将学习如何为经期跟踪器声明图标，所以，一经安装，我们的 PWA 可以像用户设备上的任何其他安装的应用程序一样出现。

离线工作

首次需要互联网访问来下载应用程序，当与服务器或其他用户同步数据时也需要互联网访问。这对所有应用程序都是必需的，不仅仅是 PWA。我们将添加一个 service worker 来创建离线体验，这意味着经期跟踪器即使在用户失去互联网访问时也能工作。我们将只触及 PWA 离线支持的能力。service worker 可以使 PWA 像任何其他已安装的应用程序一样离线工作。如果用户在离线时做出更改，service worker 使 PWA 能够在恢复连接后同步数据。使用 service worker，用户不需要积极地与 PWA 进行交互，事实上，PWA 甚至不需要打开，就能够发送和检索服务器数据。

经期跟踪器 PWA 教程
------------

这个 PWA 教程中的基础网页应用是一个经期跟踪器，用户可以追踪每个月经周期的开始和结束时间。我们将创建一个静态网页框架并为其设置样式，然后学习如何创建一个安全连接以查看我们的进度。我们将添加 JavaScript 功能，将 HTML 和 CSS 框架转化为一个功能完整的应用程序，并使用 localStorage 存储数据。使用你构建的功能完整的 Web 应用程序，我们将通过添加清单文件、图标和 service worker 来逐步将这个网络应用程序增强为支持离线的渐进式 Web 应用程序。

教程步骤包括：

应用的 HTML 和 CSS

逐行解释网站静态内容的 HTML，即经期跟踪器的静态内容，以及用于为内容设置样式的 CSS。

本地开发环境或安全连接

尽管所有的网站都应该通过 https 提供服务，但对于 PWA 来说，https 是一个要求。service worker 和 PWA 只能在安全上下文中使用，包括使用 `https://` 协议提供的 SSL 环境和使用 `http://` 协议提供的本地资源（包括 `127.0.0.1` 和 `localhost` 这两个 URL）。我们将使用 `file://` 协议查看当前状态的页面，然后在教程步骤中逐步介绍创建安全的本地主机连接以测试你的代码的选项。我们还将介绍如何使用 GitHub 页面提供你的 PWA。

JavaScript 和 LocalStorage

完整解释用于创建客户端经期跟踪器 Web 应用程序的 JavaScript 功能，以便我们拥有一个功能齐全的应用程序，可以逐步增强为 PWA，并使用 `localStorage` 来存储经期信息。

清单文件：标识、外观和图标

PWA 需要一个清单文件，它是一个 JSON 文件，描述了应用程序的名称、图标、描述以及在安装 PWA 的操作系统中应用程序的其他定义。我们将创建一个清单文件，定义应用程序在安装时的外观，包括根据用户设备使用哪些图标以及应用程序视口的参数。我们还将使用浏览器开发者工具调试清单文件。

service worker

service worker 使应用程序能够离线工作。通过前面的安全连接，首次访问页面时提供其基本功能，同时下载 service worker。安装和激活 service worker 后，它将控制页面以提供更高的可靠性和速度。

有一定的 HTML、CSS 和 JavaScript 基础将对完成本教程有一定的帮助。本教程提供了创建清单文件、启动 service worker，以及设置本地开发环境的说明，这样你就可以查看自己的进度。

虽然安全连接是一个要求，但创建 PWA 除了使用任何文本编辑器编写代码和使用浏览器查看代码之外，没有其他软件要求。

你可以体验在线经期跟踪器，并在 GitHub 上查看经期跟踪器的源代码。

* 下一页


让 PWA 易于安装 - 渐进式 Web 应用（PWA）

让 PWA 易于安装
==========

* 上一页
* Overview: js13kGames
* 下一页

在上一篇文章，我们了解了如何通过 Service Worker 让 js13kPWA 离线工作。我们还可以更进一步，让用户如同本地应用一样在支持的移动浏览器上安装 web 应用。这篇文章讲述如何通过网页清单和添加到主屏特性来做到这一点。

这些技术允许应用从设备主屏直接启动，而不需启动浏览器键入 URL。你的 Web 应用可以作为一等公民和原生应用肩并肩。这样可以使应用更易于访问，你还可以指定应用全屏运行，没有浏览器界面，这样看起来更像一个原生应用。

要求
--

可安装网站需要满足以下条件：

* 一份网页清单，填好正确的字段
* 网站的协议必须是安全的（即使用 HTTPS 协议）
* 一个在设备上代表应用的图标
* 一个注册好的 Service Worker，可以让应用离线工作（这仅对于安卓设备上的 Chrome 浏览器是必需的）
### 清单文件 (Manifest)

离线访问的关键在于一份网页清单，它通过 JSON 形式列举了网站的所有信息。

它通常位于网页应用的根目录，包含一些有用的信息，比如应用的标题、在移动设备操作系统上显示的代表该应用的不同大小的图标（例如主屏图标）的路径，和用于加载页或启动画面的背景颜色。浏览器需要这些信息来安装 web 应用并使其在主屏上显示。

js13kPWA 的 `js13kpwa.webmanifest` 文件包含在 `index.html` 文件的 `<head>` 段，如下行所示：

```html
<link rel="manifest" href="js13kpwa.webmanifest" />
```

**备注：** 这类清单文件有一些曾经常被使用的扩展名：`manifest.webapp` 在 Firefox OS 应用清单中很流行，而许多人使用 `manifest.json` 作为网页清单（因为内容是 JSON 格式的）。但是，`.webmanifest` 扩展名是在 W3C 清单规范中显式指定的，所以这里我们使用这个扩展名。

文件的内容是这个样子的：

```json
{
  "name": "js13kGames Progressive Web App",
  "short\_name": "js13kPWA",
  "description": "Progressive Web App that lists games submitted to the A-Frame category in the js13kGames 2017 competition.",
  "icons": [
    {
      "src": "icons/icon-32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    // ...
    {
      "src": "icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start\_url": "/pwa-examples/js13kpwa/index.html",
  "display": "fullscreen",
  "theme\_color": "#B12A34",
  "background\_color": "#B12A34"
}
```

大部分字段无需解释，但是我们还是详细解释一下为妙：

* `name`: 网站应用的全名。
* `short_name`: 显示在主屏上的短名字。
* `description`: 一两句话解释你的应用的用途。
* `icons`: 一串图标信息：源 URL，大小和类型。多包含几个图标，这样就能选中一个最适合用户设备的。
* `start_url`: 启动应用时打开的主页。
* `display`: 应用的显示方式；可以是 `fullscreen`、`standalone`、`minimal-ui` 或者 `browser`。
* `theme_color`: UI 主颜色，由操作系统使用。
* `background_color`: 背景色，用于安装和显示启动画面时。

一份网页清单最少需要 `name` 和一个图标 (带有 `src`, `size` 和 `type`)。最好也要提供 `description`、`short_name`、和 `start_url`。除了上述字段，还有一些其他的字段供你使用，请查看网页应用清单参考获取详细信息。

添加到主屏
-----

"添加到主屏" (或者英语短语 A2HS (Add to Home Screen)) 是移动浏览器实现的一个特性，它利用网页清单中的信息来在设备主屏上显示应用图标和文字。只有应用满足上述必备条件，这个功能才可以正常运作。

当用户使用支持的移动浏览器访问 PWA 时，浏览器会显示一条横幅信息表示可以安装这个应用。

![Add to Home screen popup of js13kPWA.](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Installable_PWAs/js13kpwa-icon.png)

在用户单击这个横幅后，安装横幅信息会显示出来。它是浏览器基于网页清单信息创建的，名字和图标可以在提示中看到。

![Install banner of js13kPWA.](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Installable_PWAs/js13kpwa-banner.png)

如果用户单击安装到主屏按钮，会显示应用图标的样子，让用户确认是否安装这个应用。

![Add to Home screen popup of js13kPWA.](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Installable_PWAs/js13kpwa-add.png)

确认之后，应用就安装到主屏上了。

![](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Installable_PWAs/js13kpwa-installed.png)

之后，用户可以立刻启动并使用应用。注意，在某些浏览器/移动操作系统中，PWA 会在图标右下角显示浏览器图标，告诉用户这是一个网页应用。

### 启动画面

在一些浏览器中，可以通过清单信息产生一个启动画面，当 PWA 启动时显示。

![](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Installable_PWAs/js13kpwa-splash.png)

这个启动画面由给定的图标、主题和背景色生成。

总结
--

在本文中，我们学习了如何使用网页清单和安装到主屏特性让 PWA 可安装。

为了获得添加到主屏的更多信息，请阅读添加到主屏指南。浏览器支持当前限于 Android 平台上的 Firefox 58+、Mobile Chrome、Android Webview 31+ 以及 Android Opera 32+, 但是将来这些限制的影响会逐渐减小。

现在让我们将目光转移到 PWA 的最后一个难点：通过推送通知再次启动（re-engagement）。

* 上一页
* Overview: js13kGames
* 下一页


渐进式加载 - 渐进式 Web 应用（PWA）

渐进式加载
=====

* 上一页
* Overview: js13kGames

在前面的文章我们介绍了很多 API，例如：Service Worker (en-US)、Web 应用程序清单 (en-US)、通知和推送 (en-US)，它们让我们的示例应用 js13kPWA 成为一个渐进式 Web 应用。在这篇文章里我们将会更进一步，使用资源渐进式加载来提高整个应用的性能。

首次有效渲染
------

尽快把有效的信息输送给用户是一件非常重要的事情：等待页面加载的时间越长，用户在页面加载完成之前离开的概率就越大。为了达到这个目的，网页加载完成前，我们应该用占位符在最终资源将会加载的地方展示最起码的视图骨架。

这个功能可以用渐进式加载来实现，它也被称为惰性加载。它的做法是延迟加载尽可能多的资源（HTML、CSS 和 JavaScript），只有在用户第一次使用到它的时候，它才会被立刻加载。

打包还是拆分
------

大部分用户不会用到一个网站的所有页面，但我们通常的做法却是把所有的功能都打包进一个很大的文件里面。一个 `bundle.js` 文件的大小可能会有几 M，一个打包后的 `style.css` 会包含网站的一切样式，从 CSS 结构定义到网站在各个版本的样式：移动端、平板、桌面、打印版等等。

通常来说，只加载一个较大的打包后文件会比加载很多个小文件要快一些，但如果用户并不是一开始就需要所有的资源，我们就可以首先加载那些关键的资源，其他的资源等到需要的时候再去加载它。

阻塞渲染的资源
-------

将所有文件打包到一起是不良做法，因为浏览器在把计算结果渲染到屏幕之前，需要先把 HTML、CSS 和 JavaScript 下载下来。在页面被打开到加载完成的这段时间里，用户将会看到一个空白的页面，体验显然很差。

为了解决这个问题，举个例子，我们可以在 script 标签上面加上一个 `defer`：

```html
<script src="app.js" defer></script>
```

它们会等到文档解析完成之后再开始下载和执行，所以不会阻塞 HTML 页面的渲染。我们还可以拆分 CSS 文件并给它们加上 media 属性：

```html
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="print.css" media="print" />
```

这种做法告诉浏览器，只有在条件满足的情况下才加载这些资源（例如指定了 print，则在打印环境下才会加载这些资源，译者注）。

在我们这个 js13kPWA 应用里面，由于 CSS 非常的简单，因此所有样式都被放到一个文件里面，并没有具体的规则来指导它如何加载 CSS。但我们可以做得更好，例如把 `style.css` 里面的所有内容移动到一个 `<style>` 标签里面，并把它放到 `index.html` 的 `<head>` 标签里面。这样做可以进一步提升应用的性能，但为了使代码更具可读性，我们并未选择这么做。

图片
--

除了 JavaScript 和 CSS，网站通常还会包含大量的图片。当你把 `<img>` 元素添加到网站里面时，对应的所有图片资源都会在页面初始化时被下载下来。在网站就绪之前下载几 M 的图片资源并不罕见，但它还是会给人一种性能不好的印象。我们并不需要在一打开网站的时候就以最高的画质呈现所有的图片。

这也是可以优化的。首先，你可以使用类似 TinyPNG 这类服务，它可以在不过分降低画质的情况下压缩文件的大小。如果你已经做了这一点，那就可以考虑一下如何通过 JavaScript 来优化图片的下载了，我们将会在下面的篇幅提到这些内容。

### 图片占位符

在 js13kPWA 示例中，我们可以通过 JavaScript 有选择地加载图片，而不是把所有的游戏截图路径都直接放进 `<img>` 标签的 `src` 属性里面，因为这会使浏览器自动下载所有的图片。在图片最终加载之前，示例页面会将图片的最终路径存放到 `data-src` 中。在这个阶段，应用会使用图片占位符来代替真正的图片，它更轻量级，体积更小（加载也更快，译者注）。

```html
<img src="data/img/placeholder.png" data-src="data/img/SLUG.jpg" alt="NAME" />
```

这些图片会在网站构建完 HTML 主体框架之后通过 JavaScript 进行加载。图片占位符被缩放到和真正的图片一样大小，所以它会占据同样的空间，在真正的图片完成加载后，也不会导致页面重绘。

### 通过 JavaScript 进行加载

`app.js` 这个文件处理 `data-src` 属性的过程如下所示：

```js
let imagesToLoad = document.querySelectorAll("img[data-src]");
const loadImages = (image) => {
  image.setAttribute("src", image.getAttribute("data-src"));
  image.onload = () => {
    image.removeAttribute("data-src");
  };
};
```

当函数 `loadImages` 把图片地址从 `data-src` 移动到 `src` 上时，`imagesToLoad` 变量包含了所有图片的链接。当每个图片都已经加载完成时，我们会把 `data-src` 属性移除掉，因为它已经没有任何用处了。我们遍历所有的图片，然后加载它们：

```js
imagesToLoad.forEach((img) => {
  loadImages(img);
});
```

### 用 CSS 制造模糊效果

为了让整个过程视觉效果更好，图片占位符的样式用 CSS 做了模糊处理：

![js13kPWA 应用中占位符图片的效果截图](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Loading/js13kpwa-placeholders.png)

我们一开始用模糊滤镜来渲染图像，然后就可以实现从模糊到清晰图像的过渡效果：

```css
article img[data-src] {
  filter: blur(0.2em);
}

article img {
  filter: blur(0em);
  transition: filter 0.5s;
}
```

这个样式会在半秒钟内移除模糊效果，它会让加载效果看起来更好看。

按需加载
----

上面讨论的图片加载机制还不错：在 HTML 文档加载完成之后再开始加载图片，在加载过程中还提供了一个很漂亮的过渡效果。问题是，即使用户有可能只看前两张或者三张图片，它仍然会一次性加载所有的图片。

这个问题可以用新的 Intersection Observer API 来解决。通过这个 API，我们可以确保只有当图片出现在可见区域时，它才会被加载。

### Intersection Observer

这是为上述可用示例提供的一个渐进增强功能：Intersection Observer 只有在用户向下滚动页面，并且图片显示在屏幕上时，才会开始加载目标图片。

这里有相关的代码示例：

```js
if ("IntersectionObserver" in window) {
  const observer = new IntersectionObserver((items, observer) => {
    items.forEach((item) => {
      if (item.isIntersecting) {
        loadImages(item.target);
        observer.unobserve(item.target);
      }
    });
  });
  imagesToLoad.forEach((img) => {
    observer.observe(img);
  });
} else {
  imagesToLoad.forEach((img) => {
    loadImages(img);
  });
}
```

如果浏览器支持 `IntersectionObserver` 对象，应用会新建一个它的实例。当一个或多个监听对象跟 Observer 发生交互时（即图片出现在视口中时），作为参数传递的函数可以用来处理一些回调事务（例如图片加载，译者注）。我们可以迭代每一个对象，并对它们进行相应的处理：当图片可见时，我们开始加载真正的图片并且停止监听这张图片，因为在图片加载完成之后，我们已经没必要再知道它的状态了。

重申我们之前提到的渐进增强理念：代码这么写的好处在于，不管浏览器支不支持 Intersection Observer，应用都能够正常工作。如果浏览器不支持 Intersection Observer，我们就会用上面提到的基础方法来实现图片的加载。

更多改进
----

要记得，有许多方法都可以用来优化应用加载时间，这个示例只探讨了其中的一种方法。你可以尝试让你的应用变的更加健壮，让它在没有 JavaScript 的情况下也能工作，例如使用 `<noscript>` 标签来展示已经分配了最终 `src` 路径的图片，或者在 `<img>` 外面套上一个 `<a>` 标签并指向对应的图片资源，当用户想看的时候，他们可以点击图片来查看。

不过这个 app 本身依赖于 JavaScript，所以我们没有这么做：没有 JavaScript，游戏列表就无法加载，Service Worker 相关的代码也将无法执行。

我们可以重写整个加载过程，让它不止加载图片，而是加载整个列表项，包括详细介绍和链接。当用户往下滚动的时候，就加载新的项目，让它像一个可以无限滚动的页面。这样，HTML 页面体积就能达到最小，加载时间可以更短，我们也可以获得更大的性能优势。

结论
--

初始化时加载更少的文件、分拆成更小的模块、使用占位符，以及按需加载更多的内容，这些都能让我们获得更短的首次加载时间。这既能让应用开发者受益，也能给用户提供更加丝滑的体验。

记住渐进增强的要点：不管在任何硬件或平台，都能提供一个可用的应用，但在现代浏览器中可以有更好的用户体验。

结语
--

这就是这个系列的所有内容了。我们通过 js13kPWA 示例应用的源码 学习了渐进式 Web 应用的的用法，包括 PWA 介绍、PWA 结构、通过 Service Worker 让 PWA 离线工作 (en-US)、让 PWA 易于安装 (en-US)，以及最后的通知功能。在 Service Worker Cookbook 的帮助下，我们还解释了推送的原理。而在本篇文章中，我们探讨了渐进式加载的概念，包括一个使用了 Intersection Observer API 的有趣示例。

请随意试验这些代码，使用 PWA 的特性来让你现有的应用更加健壮，或者创作出一点新东西。相对于常规的 Web 应用，PWA 存在巨大的优势。

* 上一页
* Overview: js13kGames


通过通知推送让 PWA 可重用 - 渐进式 Web 应用（PWA）

通过通知推送让 PWA 可重用
===============

* 上一页
* Overview: js13kGames
* 下一页

以本地缓存实现离线应用是一个强大的特性。允许用户在主屏幕上安装 Web 应用程序则显得更了不起。但除了单纯依赖用户手动打开应用之外，我们还可以走得更远，利用通知和消息推送提高用户参与度，并且随时提供新的内容。

两个 API，一个目标
-----------

推送 API和通知 API是两个相互独立的 API，但在提高用户参与度这件事上，它们可以配合得很好。推送 API 可以用来从服务端推送新的内容而无需客户端介入，它是由应用的 Service Worker 来实现的；通知功能则可以通过 Service Worker 来向用户展示一些新信息，或者至少提醒用户应用已经更新了某些功能。

跟 Service Worker 一样，这些工作是在浏览器外部实现的，所以即使应用被隐藏到后台甚至被关闭了，我们仍然能够推送更新或者通知给用户。

通知
--

让我们先从通知 API 开始，它能够脱离推送 API 单独工作，但两者结合起来会非常的有用，我们先看看它单独能做什么。

### 请求授权

为了能够显示通知，我们需要先请求用户的授权。最佳的实践经验告诉我们，不应该直接请求授权，而应该引导用户点击一个按钮，然后才弹出授权窗口：

```js
var button = document.getElementById("notifications");
button.addEventListener("click", function (e) {
  Notification.requestPermission().then(function (result) {
    if (result === "granted") {
      randomNotification();
    }
  });
});
```

下面的图片展示了如何通过系统的通知服务显示一个授权窗口：

![js13kPWA 的授权窗口。](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Re-engageable_Notifications_Push/js13kpwa-notification.png)

当用户确定接收通知，应用就可以获得推送通知的功能。用户的授权的结果有三种，default（默认）、granted（允许）或者 denied（拒绝），当用户没有做出选择的时候，授权结果会返回 default，另外两种结果分别在用户选择了允许或者拒绝的时候返回。

一旦用户选择授权，这个授权结果对通知 API 和推送 API 两者都有效。

### 创建一个通知

我们的示例应用通过可用的数据来创建通知。随机选择一个游戏数据来填充通知的主体：用游戏的名称来作为通知的标题，通知的主体会提及游戏的作者，用游戏的图片来作为通知的图标：

```js
function randomNotification() {
  var randomItem = Math.floor(Math.random() \* games.length);
  var notifTitle = games[randomItem].name;
  var notifBody = "Created by " + games[randomItem].author + ".";
  var notifImg = "data/img/" + games[randomItem].slug + ".jpg";
  var options = {
    body: notifBody,
    icon: notifImg,
  };
  var notif = new Notification(notifTitle, options);
  setTimeout(randomNotification, 30000);
}
```

上述代码每隔三十秒会创建一个通知，直到用户觉得通知有点烦人并手动关闭掉它为止。一个真正的 App，对通知的频率必须进行严格的控制，通知内容也要更加有用才行。通知 API 的优势在于它使用了操作系统的通知功能，这意味着即使用户当前并没有在使用我们的应用，我们的通知也能够展示，它看起来跟一个原生应用发出的通知差不多。

推送
--

推送比通知要复杂一些，我们需要从服务端订阅一个服务，之后服务端会推送数据到客户端应用。应用的 Service Worker 将会接收到从服务端推送的数据，这些数据可以用来做通知推送，或者实现其他的需求。

这个技术还处在非常初级的阶段，一些可用示例使用了谷歌云的消息推送平台，但它们正在被重写以支持 VAPID (Voluntary Application Identification)，它能为你的应用提供一层额外的安全防护。你可以看一下 Service Workers Cookbook 里的一些例子，尝试用 Firebase 配置一个消息推送服务，或者构建自己的推送服务（例如使用 Node.js）。

之前提到，为了接收到推送的消息，你需要有一个 Service Worker，这部分的基础知识我们已经在文章通过 Service workers 让 PWA 离线工作 (en-US)里面解释过。在 Service Worker 内部，存在一个消息推送服务订阅机制。

```js
registration.pushManager.getSubscription().then(/\* ... \*/);
```

一旦用户订阅了通知服务，他们就能接收到服务器推送的通知。

从服务端的角度来看，出于安全的目的，这整个过程必须使用非对称加密技术进行保护：允许任何人用你的应用发送未加密的消息就大有问题了。你可以通过阅读 Web 推送数据加密测试页里面的详细信息来保护你的服务器。当用户订阅服务时，服务器会储存所有接收到的信息，以便在后续需要的时候能将信息推送出去。

为了能够接收到推送的消息，我们需要在 Service Worker 文件里面监听 `push` (en-US) 事件：

```js
self.addEventListener("push", function (e) {
  /\* ... \*/
});
```

这些数据被接收后，将会以通知的方式立刻展现给用户，例如提醒用户一些待办事项，或者让用户知道 App 有了新内容。

### 推送例子

推送需要服务端，但因为应用托管在 GitHub 上，而 GitHub 只提供静态页面托管，我们没办法将服务端的例子包含进 js13kPWA 这个应用里面。Service Worker Cookbook 这个页面上有详细的说明，demo 请看 Push Payload Demo。

这个 demo 包含三个文件：

* index.js, 包含我们应用的源代码
* server.js,包含服务端的源代码 (用 Node.js 编写)
* service-worker.js, 包含 Service Worker 的具体代码。

让我们一起看看这些代码：

#### index.js

index.js 的开头注册了 Service Worker：

```js
navigator.serviceWorker
  .register("service-worker.js")
  .then(function (registration) {
    return registration.pushManager
      .getSubscription()
      .then(async function (subscription) {
        // 注册部分
      });
  })
  .then(function (subscription) {
    // 订阅部分
  });
```

这个 Service Worker 比我们在 js13kPWA demo 看到的要稍微复杂一些。在这部分代码里，Service Worker 注册完成之后，我们使用 registration 对象来发起订阅，然后使用 subscription 对象来完成整个流程。

注册部分的代码看起来大致是这个样子：

```js
if (subscription) {
  return subscription;
}
```

如果用户已经完成订阅，我们直接返回 subscription 对象并且跳转到订阅部分。如果没有，我们会初始化一个新的 subscription 对象：

```js
const response = await fetch("./vapidPublicKey");
const vapidPublicKey = await response.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
```

App 会从服务器请求一个公钥并且把响应结果转化成文本，最后它还需要转化成一个 Uint8Array（为了兼容 Chrome）。如果你想学习更多关于 VAPID 的内容可以阅读 Sending VAPID identified WebPush Notifications via Mozilla’s Push Service 这篇文章。

App 现在可以使用 `PushManager` 来订阅新用户。这个方法支持传递两个参数：第一个是 `userVisibleOnly: true`，意思是发送给用户的所有通知对他们都是可见的，第二个是 `applicationServerKey`，这个参数包含我们之前从服务端取得并转化的 VAPID key。

```js
return registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: convertedVapidKey,
});
```

现在我们把注意力转移到订阅部分：App 首先使用 fetch 将 subscription 以 JSON 的方式发送给服务器。

```js
fetch("./register", {
  method: "post",
  headers: {
    "Content-type": "application/json",
  },
  body: JSON.stringify({
    subscription: subscription,
  }),
});
```

接着给“注册”按钮绑定了一个函数：

```js
document.getElementById("doIt").onclick = function () {
  const payload = document.getElementById("notification-payload").value;
  const delay = document.getElementById("notification-delay").value;
  const ttl = document.getElementById("notification-ttl").value;

  fetch("./sendNotification", {
    method: "post",
    headers: {
      "Content-type": "application/json",
    },
    body: JSON.stringify({
      subscription: subscription,
      payload: payload,
      delay: delay,
      ttl: ttl,
    }),
  });
};
```

当按钮被点击的时候，`fetch` 会请求服务器根据给定的参数发送通知，`payload` 参数包含通知里面要显示的文本，`delay` 参数定义了通知将会延迟展示的秒数，`ttl` 是 time-to-live 的缩写，用来设置通知在服务器上的存活时间，依然是以秒为单位。

接着，我们进入下一个 JavaScript 文件。

#### server.js

服务端的代码是用 Node.js 编写的，它需要被托管在合适的地方。这部分内容已经够单独再写一篇文章了。所以这里我们只提供一个简单的概览。

web-push 模块被用来配置 VAPID 密钥，如果没有的话，还可以生成一个。

```js
const webPush = require("web-push");

if (!process.env.VAPID\_PUBLIC\_KEY || !process.env.VAPID\_PRIVATE\_KEY) {
  console.log(
    "You must set the VAPID\_PUBLIC\_KEY and VAPID\_PRIVATE\_KEY " +
      "environment variables. You can use the following ones:",
  );
  console.log(webPush.generateVAPIDKeys());
  return;
}

webPush.setVapidDetails(
  "https://github.com/mdn/serviceworker-cookbook/",
  process.env.VAPID\_PUBLIC\_KEY,
  process.env.VAPID\_PRIVATE\_KEY,
);
```

接着，一个模块定义并导出了所有应用需要处理的路由：获取 VAPID 公钥、注册、发送通知等等。你可以看到来自 `index.js` 的 `payload`, `delay` 和`ttl` 变量在这里被用到了。

```js
module.exports = function (app, route) {
  app.get(route + "vapidPublicKey", function (req, res) {
    res.send(process.env.VAPID\_PUBLIC\_KEY);
  });

  app.post(route + "register", function (req, res) {
    res.sendStatus(201);
  });

  app.post(route + "sendNotification", function (req, res) {
    const subscription = req.body.subscription;
    const payload = req.body.payload;
    const options = {
      TTL: req.body.ttl,
    };

    setTimeout(function () {
      webPush
        .sendNotification(subscription, payload, options)
        .then(function () {
          res.sendStatus(201);
        })
        .catch(function (error) {
          console.log(error);
          res.sendStatus(500);
        });
    }, req.body.delay \* 1000);
  });
};
```

#### service-worker.js

最后我们要看的文件是 Service Worker。

```js
self.addEventListener("push", function (event) {
  const payload = event.data ? event.data.text() : "no payload";
  event.waitUntil(
    self.registration.showNotification("ServiceWorker Cookbook", {
      body: payload,
    }),
  );
});
```

它做的就只是监听 `push` (en-US) 事件，创建 payload 变量，这个变量包含了来自 event.data 的文本（如果 data 是空的，就设置成 "no payload" 字符串），然后一直等到通知推送给用户为止。

如果你想知道它们具体是怎么处理的，请随意探索 Service Worker Cookbook 里面剩下的例子。GitHub 上面提供了完整的源代码，也有大量的示例展示了各种用法，包括 Web 推送、缓存策略、性能、离线运行等等。

* 上一页
* Overview: js13kGames
* 下一页


PWA 结构 - 渐进式 Web 应用（PWA）

PWA 结构
======

* 上一页
* Overview: js13kGames
* 下一页

现在，我们已经知道了 PWA 背后的原理，让我们来看一个推荐的 PWA 结构，这个案例来自一个真实的应用。我们从分析 js13kPWA 这个应用开始：为什么它要这样构建？这样做又有什么好处？

应用架构
----

渲染网站主要有两种方法 - 在服务器上或在客户端上。它们都有其优点和缺点，你可以适当地混合使用这两种方法

* 服务器端渲染（SSR）的意思是在服务器上渲染网页，因此首次加载会更快，但是在不同页面之间导航都需要下载新的 HTML 内容。它的跨浏览器兼容性良好，但代价是页间加载时间延长，也就是总体感知上的性能降低：每加载一个页面，都需要一个服务器请求往返的时间。
* 客户端渲染（CSR）允许在导航到不同页面时几乎立即在浏览器中更新网站，但在开始时需要更多的初始下载和客户端上的额外渲染。首次访问时网站速度较慢，但后续访问速度要快得多。

将 SSR 与 CSR 混用可以获得最佳效果：你可以在服务器上渲染网站，缓存其内容，然后在客户端需要时更新渲染。因为使用了 SSR，第一页加载很快；因为客户端可以仅使用已更改的部分重新渲染页面，所以页面之间的导航也是平滑的。

你可以按自己喜欢的方式构建 PWA，但有些方式更合适。最流行的是“App Shell”概念，它完全按照上述方式混用 SSR 和 CSR；此外还遵循“离线优先”方法，这个我们将在后续文章中详细解释，也会在示例应用程序中使用。我们还会简要提及另一种涉及 Streams API 的新方法

App Shell 概念
------------

App Shell 概念试图尽快加载最小用户界面，然后缓存它，以便在后续访问时可以离线使用，然后再加载应用程序的所有内容。这样，下次有人从设备访问应用程序时，UI 立即从缓存加载；如果缓存数据不可用的话，就从服务器请求新内容。

这种结构的页面很快，给用户的感觉也很快：用户会立即看到内容而不是加载动画或空白页。如果网络连接不可用，它还允许离线访问网站。

我们可以通过 service worker 控制从服务器请求的内容以及从缓存中检索的内容，这将在下一篇文章中详细解释。现在让我们关注这个结构本身。

### 我为什么要用它？

这种架构允许网站从 PWA 功能中获益最多：它可以缓存 App Shell 并以提升大量性能的方式管理动态内容。除了基本外壳之外，你还可以添加其他功能，例如添加到主屏幕或推送通知。即使用户的浏览器不支持这些功能，你也可以放心应用可以正常运行，这就是渐进增强的美妙之处。

在不妥协 web 优势的前提下，网站感觉就像一个原生应用，交互及时、性能可靠。

### 可链接、渐进式和响应式

记住 PWA 的优点并在设计应用程序时牢记这一点非常重要。app shell 方案允许网站：

* 可链接（Linkable）：即使行为类似于原生应用，它仍然是一个网站：你可以点击页面内的链接，也可以通过发送 URL 的方式分享网站给别人。
* 渐进式（Progressive）：从“美好的旧式基础网站”开始，逐步添加新功能，在过程中检测其在浏览器上的可用性，并且优雅地处理不支持案例下发生的报错。举个例子，service workers 辅助下的离线模式只是提升网站体验的额外特性，但没有它网站也仍然完全可用。
* 响应式（Responsive）：响应式网页设计也适用于渐进式网络应用程序，因为它们都主要用于移动设备。拥有浏览器的设备太多太杂，所以确保网站在不同屏幕宽度、视口和像素密度上都可以访问就变得尤为重要。viewport meta tag、CSS 媒体查询、Flexbox 和 CSS Grid 等技术都可以助你实现这个目标。
另一种概念：流
-------

使用 Streams API 可以实现完全不同的服务器端或客户端渲染方法。在 service worker 的帮助下，它可以极大改进内容解析的方式。

App shell 概念要求在网站开始呈现之前所有资源就已可用。在 HTML 的下载过程中，用户可以从网站的加载和渲染过程看出资源的下载进度，但 JavaScript 必须完全下载完成才能运行。

Streams API 则允许开发人员直接访问来自服务器的数据流。如果你想对数据执行操作（例如给视频添加过滤器），不再需要等待所有数据流下载并转换为 blob（或者别的），而是可以立即开始。它提供精细的粒度控制，将数据流启动、与另一个流链接、取消、查错等等。

从理论上讲，数据流是更好的模型，但也更复杂。在撰写本文时（2018 年 3 月），Streams API 的制订仍在进行，并且在任何主流浏览器都不完全可用。当它可用时，它将是提供内容的最快方式，在性能上会有巨大的好处。

有关可用实例和更多信息，请参阅 Streams API 文档。

示例应用的结构
-------

js13kPWA 这个网站的结构比较简单：它包含一个 HTML 页面（index.html）、一个 CSS 样式表（style.css）、一些图片、JS 脚本和字体。它的文件结构如下所示：

![Folder structure of js13kPWA.](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/js13kGames/App_structure/js13kpwa-directory.png)

### HTML 页面

从 HTML 的角度，App Shell 就是 content 节之外的一切：

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>js13kGames A-Frame entries</title>
    <meta
 name="description"
 content="A list of A-Frame entries submitted to the js13kGames 2017 competition, used as an example for the MDN articles about Progressive Web Apps." />
    <meta name="author" content="end3r" />
    <meta name="theme-color" content="#B12A34" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta property="og:image" content="icons/icon-512.png" />
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="stylesheet" href="style.css" />
    <link rel="manifest" href="js13kpwa.webmanifest" />
    <script src="data/games.js" defer></script>
    <script src="app.js" defer></script>
  </head>
  <body>
    <header>
      <p>
        <a class="logo" href="http://js13kgames.com"
 ><img src="img/js13kgames.png" alt="js13kGames"
 /></a>
      </p>
    </header>
    <main>
      <h1>js13kGames A-Frame entries</h1>
      <p class="description">
        List of games submitted to the
        <a href="http://js13kgames.com/aframe">A-Frame category</a> in the
        <a href="http://2017.js13kgames.com">js13kGames 2017</a> competition.
        You can
        <a href="https://github.com/mdn/pwa-examples/blob/master/js13kpwa"
 >fork js13kPWA on GitHub</a
 >
        to check its source code.
      </p>
      <button id="notifications">Request dummy notifications</button>
      <section id="content">// Content inserted in here</section>
    </main>
    <footer>
      <p>
        © js13kGames 2012-2018, created and maintained by
        <a href="http://end3r.com">Andrzej Mazur</a> from
        <a href="http://enclavegames.com">Enclave Games</a>.
      </p>
    </footer>
  </body>
</html>
```

`<head>` 一节包含一些基本信息，例如标题、描述、CSS 链接、描述文件、游戏内容的 JS 文件，以及 app.js，也就是我们这个 JavaScript 应用程序的入口点。`<body>` 标签分为 `<header>`（包含一张带链接的图片）、`<main>` 页面主体（有标题、描述和放置内容的区域），以及 `<footer>`（包含版权信息和链接）。

这个应用的唯一功能就是列出 js13kGames 2017 年比赛中的 A-Frame（一个用来构建虚拟现实（VR）应用的网页开发框架，译者注）项目列表。如你所见，这是一个很普通的单页应用，目的是用一个简单的东西来展示 PWA 的真实功能。

### CSS 部分

CSS 部分也是尽可能的简单：运用 `@font-face` 来加载和使用自定义字体，以及给 HTML 元素提供简单的样式，总体的目标是通过使用响应式布局，让页面在移动端和桌面设备上都漂亮。

### 主角 app.js

我们会在下一篇文章中详细分析 app.js 所做的这些工作。首先它用下面的模板生成了 content 中的内容：

```js
var template =
  "<article>\n\
 <img src='data/img/SLUG.jpg' alt='NAME'>\n\
 <h3>#POS. NAME</h3>\n\
 <ul>\n\
 <li><span>Author:</span> <strong>AUTHOR</strong></li>\n\
 <li><span>Twitter:</span> <a href='https://twitter.com/TWITTER'>@TWITTER</a></li>\n\
 <li><span>Website:</span> <a href='http://WEBSITE/'>WEBSITE</a></li>\n\
 <li><span>GitHub:</span> <a href='https://GITHUB'>GITHUB</a></li>\n\
 <li><span>More:</span> <a href='http://js13kgames.com/entries/SLUG'>js13kgames.com/entries/SLUG</a></li>\n\
 </ul>\n\
</article>";
var content = "";
for (var i = 0; i < games.length; i++) {
  var entry = template
    .replace(/POS/g, i + 1)
    .replace(/SLUG/g, games[i].slug)
    .replace(/NAME/g, games[i].name)
    .replace(/AUTHOR/g, games[i].author)
    .replace(/TWITTER/g, games[i].twitter)
    .replace(/WEBSITE/g, games[i].website)
    .replace(/GITHUB/g, games[i].github);
  entry = entry.replace("<a href='http:///'></a>", "-");
  content += entry;
}
document.getElementById("content").innerHTML = content;
```

接着，它注册了一个 service worker：

```js
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/pwa-examples/js13kpwa/sw.js");
}
```

下面这部分代码实现了一个功能：点击按钮时请求用户授权，用来向用户推送通知。

```js
var button = document.getElementById("notifications");
button.addEventListener("click", function (e) {
  Notification.requestPermission().then(function (result) {
    if (result === "granted") {
      randomNotification();
    }
  });
});
```

最后这部分是创建通知的代码，它会随机展示游戏列表中的一个项目：

```js
function randomNotification() {
  var randomItem = Math.floor(Math.random() \* games.length);
  var notifTitle = games[randomItem].name;
  var notifBody = "Created by " + games[randomItem].author + ".";
  var notifImg = "data/img/" + games[randomItem].slug + ".jpg";
  var options = {
    body: notifBody,
    icon: notifImg,
  };
  var notif = new Notification(notifTitle, options);
  setTimeout(randomNotification, 30000);
}
```

### service worker

最后我们来快速浏览一下 Service Worker 相关的文件 sw.js。它首先引入 games.js 这个文件：

```js
self.importScripts("data/games.js");
```

接着，程序会对 App Shell 和主体内容里面的数据创建一个缓存列表：

```js
var cacheName = "js13kPWA-v1";
var appShellFiles = [
  "/pwa-examples/js13kpwa/",
  "/pwa-examples/js13kpwa/index.html",
  "/pwa-examples/js13kpwa/app.js",
  "/pwa-examples/js13kpwa/style.css",
  "/pwa-examples/js13kpwa/fonts/graduate.eot",
  "/pwa-examples/js13kpwa/fonts/graduate.ttf",
  "/pwa-examples/js13kpwa/fonts/graduate.woff",
  "/pwa-examples/js13kpwa/favicon.ico",
  "/pwa-examples/js13kpwa/img/js13kgames.png",
  "/pwa-examples/js13kpwa/img/bg.png",
  "/pwa-examples/js13kpwa/icons/icon-32.png",
  "/pwa-examples/js13kpwa/icons/icon-64.png",
  "/pwa-examples/js13kpwa/icons/icon-96.png",
  "/pwa-examples/js13kpwa/icons/icon-128.png",
  "/pwa-examples/js13kpwa/icons/icon-168.png",
  "/pwa-examples/js13kpwa/icons/icon-192.png",
  "/pwa-examples/js13kpwa/icons/icon-256.png",
  "/pwa-examples/js13kpwa/icons/icon-512.png",
];
var gamesImages = [];
for (var i = 0; i < games.length; i++) {
  gamesImages.push("data/img/" + games[i].slug + ".jpg");
}
var contentToCache = appShellFiles.concat(gamesImages);
```

下面的代码用来配置 Service Worker，缓存上述列表的工作就发生在这里：

```js
self.addEventListener("install", function (e) {
  console.log("[Service Worker] Install");
  e.waitUntil(
    caches.open(cacheName).then(function (cache) {
      console.log("[Service Worker] Caching all: app shell and content");
      return cache.addAll(contentToCache);
    }),
  );
});
```

最后，如果条件允许，Service Worker 将从缓存中请求内容所需的数据，从而提供离线应用功能：

```js
self.addEventListener("fetch", function (e) {
  e.respondWith(
    caches.match(e.request).then(function (r) {
      console.log("[Service Worker] Fetching resource: " + e.request.url);
      return (
        r ||
        fetch(e.request).then(function (response) {
          return caches.open(cacheName).then(function (cache) {
            console.log(
              "[Service Worker] Caching new resource: " + e.request.url,
            );
            cache.put(e.request, response.clone());
            return response;
          });
        })
      );
    }),
  );
});
```

### JavaScript 数据

项目中所用的游戏数据放置在 data 文件夹下面，以 JavaScript 对象的形式提供（games.js）：

```js
var games = [
  {
    slug: "lost-in-cyberspace",
    name: "Lost in Cyberspace",
    author: "Zosia and Bartek",
    twitter: "bartaz",
    website: "",
    github: "github.com/bartaz/lost-in-cyberspace",
  },
  {
    slug: "vernissage",
    name: "Vernissage",
    author: "Platane",
    twitter: "platane\_",
    website: "github.com/Platane",
    github: "github.com/Platane/js13k-2017",
  },
  // ...
  {
    slug: "emma-3d",
    name: "Emma-3D",
    author: "Prateek Roushan",
    twitter: "",
    website: "",
    github: "github.com/coderprateek/Emma-3D",
  },
];
```

每一个入口在 data/img 文件夹下面都有属于它自己的图片。这些就是我们的内容数据，我们通过 JS 将这些数据加载到主体内容中。

下一步
---

下一篇文章中，我们会探讨更多的细节：Service Worker 如何帮助我们缓存 App Shell 和内容，从而让我们实现离线功能。

* 上一页
* Overview: js13kGames
* 下一页


通过 Service workers 让 PWA 离线工作 - 渐进式 Web 应用（PWA）

通过 Service workers 让 PWA 离线工作
=============================

* 上一页
* Overview: js13kGames
* 下一页

我们已经看到了 js13kPWA 的结构，并且看到了 shell 启动和运行的基本方式，那么现在让我们把目光转向如何使用 Service Worker 实现离线功能。在本文，我们将看到它在 js13kPWA example 中是如何使用的（另请参阅源代码）。我们将研究如何添加脱机功能。

Service Worker 解释
-----------------

Service Worker 是浏览器和网络之间的虚拟代理。它们终于解决了前端开发人员多年来一直在努力解决的一些问题，其中最值得关注的是，解决了如何正确缓存网站资源并使其在离线时可用的问题。

Service Worker 运行在一个与页面 JavaScript 主线程独立的线程上，并且无权访问 DOM 结构。这引入了一种与传统 Web 编程不同的方式：它的 API 是非阻塞的，并且可以在不同的上下文之间发送和接收信息。你可分配给 Service Worker 一些任务，并通过基于 Promise 的方法在任务完成时收到结果。

它不仅仅提供离线功能，还可以做包括处理通知、在单独的线程上执行繁重的计算等事务。Service workers 非常强大，因为他们可以控制网络请求、修改网络请求、返回缓存的自定义响应，或者合成响应。

### 安全

因为它们非常强大，所以 Service Worker 只能在安全的上下文中执行（即 HTTPS）。如果你想在将代码推送到生产环境之前先进行实验，则可以始终在本地主机上进行测试或设置 GitHub Pages，这两者都支持 HTTPS。

离线优先
----

“离线优先”或“缓存优先”模式是向用户提供内容的最流行策略。如果资源已缓存且可脱机使用，就在尝试从服务器下载资源之前先将其返回；如果它已经不在缓存中，就下载并缓存以备将来使用。

PWA 中的 P（渐进，Progressive）
------------------------

Service Worker 当正确地作为渐进功能实装时，可以为支持其 API 的现代浏览器用户提供离线支持使其受益，但也不会使用旧版浏览器的用户造成负面影响。

js13kPWA 应用程序中的 Service worker
------------------------------

理论看够了，让我们看一些源代码！

### 注册 Service Worker

首先让我们看看在 app.js 文件中注册新 Service Worker 的代码：

```js
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/pwa-examples/js13kpwa/sw.js");
}
```

如果浏览器支持 Service Workers API，则使用 `ServiceWorkerContainer.register()` 方法在该站点注册。其内容在 sw.js 文件中，可以在注册成功后执行。它是 app.js 文件中唯一与 Service Worker 有关的代码; 其他关于 Service Worker 的内容都写在 sw.js 文件中。

### Service Worker 的生命周期

注册完成后，sw.js 文件会自动下载、安装，然后激活。

#### 安装

Service Workers API 允许我们为我们感兴趣的关键事件添加事件监听器 - 第一个是 `install` 事件：

```js
self.addEventListener("install", function (e) {
  console.log("[Service Worker] Install");
});
```

在 `install` 的监听函数中，我们可以初始化缓存并添加离线应用时所需的文件，js13kPWA 应用确实也是这么做的。

首先，创建一个存储缓存名字的变量，App Shell 所需的文件也被记录在一个数组上：

```js
var cacheName = "js13kPWA-v1";
var appShellFiles = [
  "/pwa-examples/js13kpwa/",
  "/pwa-examples/js13kpwa/index.html",
  "/pwa-examples/js13kpwa/app.js",
  "/pwa-examples/js13kpwa/style.css",
  "/pwa-examples/js13kpwa/fonts/graduate.eot",
  "/pwa-examples/js13kpwa/fonts/graduate.ttf",
  "/pwa-examples/js13kpwa/fonts/graduate.woff",
  "/pwa-examples/js13kpwa/favicon.ico",
  "/pwa-examples/js13kpwa/img/js13kgames.png",
  "/pwa-examples/js13kpwa/img/bg.png",
  "/pwa-examples/js13kpwa/icons/icon-32.png",
  "/pwa-examples/js13kpwa/icons/icon-64.png",
  "/pwa-examples/js13kpwa/icons/icon-96.png",
  "/pwa-examples/js13kpwa/icons/icon-128.png",
  "/pwa-examples/js13kpwa/icons/icon-168.png",
  "/pwa-examples/js13kpwa/icons/icon-192.png",
  "/pwa-examples/js13kpwa/icons/icon-256.png",
  "/pwa-examples/js13kpwa/icons/icon-512.png",
];
```

接下来，从 data/games.js 的内容中解析出来的图片链接被赋值到另一个数组上，之后，两个数组会用 `Array.prototype.concat()` 方法合并起来。

```js
var gamesImages = [];
for (var i = 0; i < games.length; i++) {
  gamesImages.push("data/img/" + games[i].slug + ".jpg");
}
var contentToCache = appShellFiles.concat(gamesImages);
```

接着我们可以监听 install 事件：

```js
self.addEventListener("install", function (e) {
  console.log("[Service Worker] Install");
  e.waitUntil(
    caches.open(cacheName).then(function (cache) {
      console.log("[Service Worker] Caching all: app shell and content");
      return cache.addAll(contentToCache);
    }),
  );
});
```

有两件事需要在这里解释一下：`ExtendableEvent.waitUntil` 做了什么？`caches` 对象又是什么东西？

Service Worker 会等到 `waitUntil` 里面的代码执行完毕之后才开始安装。它返回的是一个 Promise——因为安装过程可能需要一些时间，而我们必须等待它完成。

`caches` 是一个特殊的 `CacheStorage` 对象，它能在 Service Worker 指定的范围内提供数据存储的能力（Service Worker 在注册时，第二个参数是选填的，可以被用来指定你想让 Service Worker 控制的内容的子目录，译者注）。因为 Web Storage 的执行是同步的（此处理解为 Web Storage 并不返回一个 Promise，译者注），在 Service Worker 中使用 Web Storage 将不会有效果，所以我们使用 Cache API 作为替代。

这里，我们使用给定的名字开启了一个缓存，并且将我们的应用所需要缓存的文件全部添加进去，当我们再次加载这些资源时，由请求 URL 确定的对应缓存就是可用的。

#### 激活

还有一个 `activate` 事件，它的用法跟 `install` 事件相同。这个事件通常用来删除那些我们已经不需要的文件或者做一些清理工作。因为在我们的 App 里面没有使用到，这里我们暂时跳过它。

### 响应请求

`fetch` 事件对我们很有用，它在每次应用发起 HTTP 请求的时候被触发。这个事件对我们来说非常有用，它允许我们拦截请求并对请求作出自定义的响应，下面是一个简单的例子：

```js
self.addEventListener("fetch", function (e) {
  console.log("[Service Worker] Fetched resource " + e.request.url);
});
```

请求的响应可以是任何我们想要的东西：请求过的文件缓存下来的副本，或者一段做了具体操作的 JavaScript 代码——这里有无限的可能。

在我们的示例代码中，当缓存存在时，我们使用缓存来提供服务，而不重新请求数据。不管当前应用是在线还是离线，我们都这么做。当请求的文件不在缓存中时，我们会在响应之前将数据添加到缓存中。

```js
self.addEventListener("fetch", function (e) {
  e.respondWith(
    caches.match(e.request).then(function (r) {
      console.log("[Service Worker] Fetching resource: " + e.request.url);
      return (
        r ||
        fetch(e.request).then(function (response) {
          return caches.open(cacheName).then(function (cache) {
            console.log(
              "[Service Worker] Caching new resource: " + e.request.url,
            );
            cache.put(e.request, response.clone());
            return response;
          });
        })
      );
    }),
  );
});
```

上述代码中，对于请求我们首先会在缓存中查找资源是否被缓存：如果存在，将会返回缓存的资源；如果不存在，会转而从网络中请求数据，然后将它缓存起来，这样下次有相同的请求发生时，我们就可以直接使用缓存。

`FetchEvent.respondWith` 方法将会接管响应控制，它会作为服务器和应用之间的代理服务。它允许我们对每一个请求作出我们想要的任何响应：Service Worker 会处理这一切，从缓存中获取这些数据，并在需要的情况下对它们进行修改。

就是这样，我们的应用会在 install 触发时缓存资源，并且在 fetch 事件触发时返回缓存中的资源，这就是它甚至在离线状态下也能使用的原因。当我们添加新的内容时，它也会随时将其缓存下来。

更新
--

还有一点需要考虑：当我们的应用有了一个新版本，并且它包含了一些可用的新资源时，我们应该如何去更新它的 Service Worker？我们存放在缓存名称中的版本号是这个问题的关键：

```js
var cacheName = "js13kPWA-v1";
```

当我们把版本号更新到 v2，Service Worker 会将我们所有的文件（包括那些新的文件）添加到一个新的缓存中。

```js
contentToCache.push("/pwa-examples/js13kpwa/icons/icon-32.png");

// ...

self.addEventListener("install", function (e) {
  e.waitUntil(
    caches.open("js13kPWA-v2").then(function (cache) {
      return cache.addAll(contentToCache);
    }),
  );
});
```

这个时候一个新的 Service Worker 会在后台被安装，而旧的 Service Worker 仍然会正常运行，直到没有任何页面使用到它为止，这时候新的 Service Worker 将会被激活，然后接管所有的页面。

缓存的清理
-----

还记得我们前面跳过的那个 `activate` 事件吗？它可以用来清理那些我们不再需要的缓存：

```js
self.addEventListener("activate", function (e) {
  e.waitUntil(
    caches.keys().then(function (keyList) {
      return Promise.all(
        keyList.map(function (key) {
          if (cacheName.indexOf(key) === -1) {
            return caches.delete(key);
          }
        }),
      );
    }),
  );
});
```

这样能确保只有那些我们需要的文件会保留在缓存中，我们不需要留下任何的垃圾，毕竟浏览器的缓存空间是有限的，手动清理掉这些不需要的缓存是不错的主意。

其他用途
----

从缓存中加载资源并不是 Service Worker 的唯一能力，如果你有比较耗时的计算，你可以把它们从主线程中抽离出来，在 Service Worker 中进行计算，最后在它们计算完毕的时候从 Service Worker 中取得计算结果。性能方面，你可以在 Service Worker 中对即将使用到的资源进行预加载，这样当你使用到这些资源的时候，应用的加载速度会更快。

总结
--

在这篇文章中我们简单的了解了如何使用 Service Worker 让你的 PWA 实现离线功能。如果你想要学习更多关于 Service Worker API 的概念，以及使用 Service Worker 方面的相关细节，你可以进一步查阅我们的文档。

Service Worker 在处理消息推送的时候也会经常被用到，这一部分我们将会在后面的章节进行讨论。

* 上一页
* Overview: js13kGames
* 下一页


经期跟踪器：清单和图标 - 渐进式 Web 应用（PWA）

经期跟踪器：清单和图标
===========

* 上一页
* Overview: CycleTracker
* 下一页

PWA 清单文件是一个 JSON 文件，它提供了关于该应用程序的特性信息，使其在用户设备上安装时看起来和表现得像原生应用。清单包含了你的应用的元数据，包括它的名称、图标和呈现指令。

虽然根据规格，清单的所有键或成员都是可选的，但是一些浏览器、操作系统和应用分发机构对于 Web 应用成为 PWA 有特定成员的要求。通过包含名称或简称、启动 URL、满足一定最低要求的图标，以及 PWA 应该在什么样的应用程序视口中显示，你的应用将满足 PWA 的清单要求。

我们的月经周期跟踪应用的最小化清单文件可能如下所示：

```js
{
  "short\_name": "CT",
  "start\_url" : "/",
  "icons": [
    {
      "src": "icon-512.png",
      "sizes": "512x512"
    }
  ],
  "display": "standalone"
}
```

在保存清单文件并从我们的 HTML 文件中链接到它之前，我们可以开发一个仍然简洁但具有更多信息的 JSON 对象，以定义 PWA 的标识、展示和图标。是的，上面的内容会起作用，但让我们讨论一下这个示例中的成员以及其他一些成员，这些成员使清单文件能够更好地定义我们的经期跟踪器 PWA 的外观。

应用标识
----

为了识别你的 PWA，JSON 必须包含一个 `name` 或 `short_name` 成员，或者两者都包含，以定义 PWA 的名称。它还可以包含一个 `description`。

`name` (en-US)

PWA 的名称。这是操作系统列出应用程序时使用的名称，也是应用图标旁边的标签，等等。

`short_name` (en-US)

如果没有足够的空间显示 `name`，则显示给用户的 PWA 的名称。它被用作手机屏幕上图标的标签，包括在 iOS 的“添加到主屏幕”对话框中。

当 `name` 和 `short_name` 都存在时，大多数情况下都会使用 `name`，在显示应用程序名称的空间有限时使用 `short_name`。

`description` (en-US)

说明应用程序的功能。它提供了一个关于应用程序目的和功能的无障碍描述 (en-US)。

### 任务

编写你的清单文件的前几行。你可以使用下面的文本，或者更为谨慎或描述性的值，并选择你自己的描述。

### 示例解决方案

```js
{
  "name": "CycleTracker: Period Tracking app",
  "short\_name": "CT",
  "description": "Securely and confidentially track your menstrual cycle. Enter the start and end dates of your periods, saving your private data to your browser on your device, without sharing it with the rest of the world."
}
```

应用程序外观
------

PWA 的安装后和离线体验的外观由清单文件定义。外观清单成员包括 `start_url` 和 `display`，以及可以用于自定义应用程序颜色 (en-US)的成员，包括 `theme_color` 和 `background_color`。

`start_url` (en-US)

用户启动 PWA 时的起始页面。

`display` (en-US)

控制应用程序的显示模式，包括 `fullscreen`、`standalone`，后者可以将 PWA 显示为独立应用程序 (en-US)、`minimal-ui`，类似独立视图但具有用于控制导航的 UI 元素，以及 `browser`，即在常规浏览器视图中打开应用程序。

还有一个 `orientation` (en-US) 成员，用于将 PWA 的默认方向定义为 `portrait` 或 `landscape`。由于我们的应用程序在两种方向下都能很好地工作，因此我们将省略此成员。

### 颜色

`theme_color` (en-US)

默认的操作系统和浏览器 UI 元素的颜色 (en-US)，例如某些移动体验中的状态栏和桌面操作系统上的应用程序标题栏。

`background_color`

在 CSS 加载之前，显示为应用程序背景 (en-US)的占位符颜色。为了在应用启动和加载之间创建平滑过渡，建议使用声明为应用程序 `background-color` 颜色的 `<color>`。

### 任务

在之前任务开始创建的清单文件中添加外观定义。

### 示例解决方案

由于示例应用程序是一个单页面，我们可以使用 `"/"` 作为 `start_url`，或者完全省略该成员。出于同样的原因，我们可以通过将 `display` 设置为 `standalone` 来不显示浏览器 UI 而显示应用程序。

在我们的 CSS 中，`background-color: #efe;` 被设置在 `body` 元素选择器上。我们使用 `#eeffee` 以确保从占位符外观到应用加载之间实现平滑过渡。

```js
{
  "name": "...",
  "short\_name": "...",
  "description": "...",
  "start\_url": "/",
  "theme\_color": "#eeffee",
  "background\_color": "#eeffee",
  "display": "standalone"
}
```

应用图标
----

PWA 图标帮助用户识别你的应用程序，使其更具视觉吸引力，并提高可发现性。PWA 应用图标出现在主屏幕、应用启动器或应用商店搜索结果中。渲染的图标大小和文件要求因显示位置和显示方而异。清单文件是定义图像的地方。

在清单 JSON 对象中，`icons` 成员指定用于不同上下文的一个或多个图标对象数组，每个对象都有一个 `src` 和 `sizes` 成员，还有可选的 `type` 和 `purpose` 成员。每个图标对象的 `src` 列出单个图像文件的源。`sizes` 成员提供空格分隔的尺寸列表，指明应该对该特定图像使用哪些尺寸，或者使用关键字 `any`；该值与 `<link>` 元素的 `sizes` 属性相同。`type` 成员列出图像的 MIME 类型。

```js
{
  "name": "MyApp",
  "icons": [
    {
      "src": "icons/tiny.webp",
      "sizes": "48x48"
    },
    {
      "src": "icons/small.png",
      "sizes": "72x72 96x96 128x128 256x256",
      "purpose": "maskable"
    },
    {
      "src": "icons/large.png",
      "sizes": "512x512"
    },
    {
      "src": "icons/scalable.svg",
      "sizes": "any"
    }
  ]
}
```

所有图标都应该有相同的外观和感觉，以确保用户可以识别你的 PWA。但图标越大，它可以包含的细节就越丰富。尽管所有图标文件都是正方形的，但有些操作系统会以不同的形状渲染图标，裁剪部分区域或“遮挡”图标以适应 UI，或者如果图标不可遮挡则会缩小并居中放置在背景上。安全区域 (en-US)，即图标被遮挡为圆形时也会正常渲染的内部 80% 区域。通过将 `purpose` 成员设置为 `maskable`，可以将图标定义为自适应图标。

在 Safari 以及 iOS 和 iPadOS 中，如果通过 `<link>` 在 HTML 文档的 `<head>` 中包含非标准的 `apple-touch-icon`，它们会优先于清单中声明的图标。

### 任务

将图标添加到你一直在构建的清单文件中。

围绕经期跟踪器的“循环”和“周期”这两个词以及我们选择的绿色主题色，我们可以将所有图标图像设计成浅绿色正方形，中间有一个绿色圆圈。我们最小尺寸的图标文件 `circle.ico` 只是一个代表句号标点符号和应用程序主题颜色的圆圈；而中间尺寸的图像 `circle.svg`、`tire.svg` 和 `wheel.svg` 则随着尺寸变大而增加更多细节，从简单的圆圈变成轮胎；我们最大尺寸的图标则是一个有辐条和阴影的精细轮子。不过，设计图标已经超出了本教程的范围。
```

<div>
  <img alt="一个绿色圆圈" src="circle.svg" role="img" />
  <img alt="一个简单的轮子" src="tire.svg" role="img" />
  <img alt="一个详细的轮子" src="wheel.svg" role="img" />
</div>
```

```
div {
  display: flex;
  gap: 5px;
}
img {
  width: 33%;
}
```

### 示例解决方案

```js
{
  "name": "...",
  "short\_name": "...",
  "description": "...",
  "start\_url": "...",
  "theme\_color": "...",
  "background\_color": "...",
  "display": "...",
  "icons": [
    {
      "src": "circle.ico",
      "sizes": "48x48"
    },
    {
      "src": "icons/circle.svg",
      "sizes": "72x72 96x96",
      "purpose": "maskable"
    },
    {
      "src": "icons/tire.svg",
      "sizes": "128x128 256x256"
    },
    {
      "src": "icons/wheel.svg",
      "sizes": "512x512"
    }
  ]
}
```

将清单添加到应用中
---------

你现在已经有一个完全可用的清单文件了。是时候保存它并从我们的 HTML 文件中链接到它了。

清单文件的文件扩展名可以是规范建议的 `.webappmanifest`。但是，作为 JSON 文件，它最常见的保存扩展名是浏览器支持的 `.json`。

PWA 需要从应用的 HTML 文档中链接到清单文件。我们已经有一个功能完全的应用，但它还不是 PWA，因为它还没有链接到我们的外部 manifest JSON 文件。为了包含外部 JSON 资源，我们使用 `<link>` 元素，带有 `rel="manifest"` 属性，并将 `href` 属性设置为资源的位置。

```html
<link rel="manifest" href="cycletracker.json" />
```

`<link>` 元素最常用于链接样式表，在 PWA 中，它用于链接必须的清单文件，但也用于建立站点图标（包括“favicon”风格的图标和移动设备主屏幕和应用程序上的图标）等其他用途。

```html
<link rel="icon" href="icons/circle.svg" />
```

使用 `.webmanifest` 扩展名时，如果你的服务器不支持那种 MIME 类型，请设置 `type="application/manifest+json"`。

#### 任务

保存你在上面的步骤中创建的清单文件，然后从 `index.html` 文件中链接到它。

可选地，也可以从 HTML 中链接到一个快捷图标。

#### 示例解决方案

`index.html` 的 `<head>` 现在可能如下所示：

```html
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <title>Cycle Tracker</title>
  <link rel="stylesheet" href="style.css" />
  <link rel="manifest" href="cycletracker.json" />
  <link rel="icon" href="icons/circle.svg" />
</head>
```

查看 `cycletracker.json` 文件和在 GitHub 上查看项目源代码。

有了清单文件，Safari 会将你的网站识别为 web 应用。要使 web 应用成为完整的 PWA, 我们仍需要添加 service worker。

调试清单文件
------

一些浏览器开发者工具可以洞察应用清单文件。在 Edge、Firefox 和 Chrome 开发者工具中，清单成员及其值可在“Application”面板下看到。

![在开发者工具中，左侧面板包含指向清单的链接。右侧显示应用清单文件，文件名称是一个指向 JSON 文件的链接。](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/CycleTracker/Manifest_file/debugger_devtools.jpg)

Manifest 应用面板提供了清单文件名称作为链接，以及标识、外观和图标部分。

![标识和外观清单成员以及存在的任何值。](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/CycleTracker/Manifest_file/manifest_identity_and_presentation.jpg)

支持的清单成员将显示出来，以及所有包含的值。在此截图中，虽然我们没有包括 `orientation` 或 `id` 成员，但它们都被列出来了。应用面板可以用来查看清单成员，甚至可以了解：在这个例子中，我们了解到要指定与当前标识匹配的应用 Id，需要将 `id` 字段设置为“/”。

![由于我们没有 service worker，我们的应用不是可安装的 PWA。](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/CycleTracker/Manifest_file/manifest_installability.jpg)

Chrome 和 Edge 还提供错误和警告、协议处理程序以及改进清单和图标的信息。

我们的 web 应用没有任何协议处理程序；这是一个本教程未涵盖的主题。如果我们包含一些协议处理程序，它们会在“Protocol Handlers”下找到。由于该部分为空，开发者工具会链接到该主题的更多信息。

![清单文件中包含的四个图标，已选中“仅显示可遮罩图标的最小安全区域”以移除背景。](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/CycleTracker/Manifest_file/manifest_icons.jpg)

清单面板还包括关于可遮罩图标安全区域的信息，以及指向 PWA 图像生成器的链接。该工具为 Android、Apple 操作系统和 Windows 创建 100 多个正方形 PNG 图像，以及列出所有图像及其尺寸的 JSON 对象。它生成的图像可能不符合你的需要，但是为每个操作系统生成的图像尺寸列表展示了 PWA 可以提供服务的位置和方式的多样性。

开发者工具在识别支持的清单成员方面非常有用。请注意，Firefox 开发者工具中甚至包括 `dir`、`lang`、`orientation`、`scope` 和 `id` 的条目，尽管我们的清单文件没有包括这些成员。Firefox 还包括每个图标的 `purpose` 成员的值，如果目的没有明确设置，会显示 `any`。

![Firefox 开发者工具中的 Manifest 面板，显示未包含的 dir、scope 和 id 成员的值，以及没有关联值的 lang 和 orientation 成员。](/zh-CN/docs/Web/Progressive_web_apps/Tutorials/CycleTracker/Manifest_file/manifest_firefox.jpg)

接下来
---

要从其他浏览器和所有支持 PWA 的操作系统中获得 PWA 的好处，我们需要添加 service worker，我们将在不使用框架的情况下完成这项工作。

* 上一页
* Overview: CycleTracker
* 下一页