事件介绍 - 学习 Web 开发

事件介绍
====

* 上一页
* Overview: Building blocks
* 下一页

事件是你正在编程的系统中发生的事情，系统会告诉你有关这些事件的信息，以便你的代码能够对它们做出反应。

例如：如果用户在网页上单击一个按钮，你可能想通过显示一个信息框来响应这个动作。在这篇文章中，我们将讨论一些关于事件的重要概念，并且观察它们在浏览器上如何运行。这篇文章不会面面俱到，仅聚焦于你现阶段需要掌握的知识。

|  |  |
| --- | --- |
| 前提： | 基本的计算机知识，对 HTML 和 CSS 的基本了解，学习了 JavaScript 第一步。 |
| 目标： | 了解事件的基本理论，它们怎么在浏览器上运行的，以及在不同的编程环境下事件有何不同。 |

什么是事件？
------

事件是发生在你正在编程的系统中的事情——当事件发生时，系统产生（或“触发”）某种信号，并提供一种机制，当事件发生时，可以自动采取某种行动（即运行一些代码）。事件是在浏览器窗口内触发的，并倾向于附加到驻留在其中的特定项目。这可能是一个单一的元素，一组元素，当前标签中加载的 HTML 文档，或整个浏览器窗口。有许多不同类型的事件可以发生。

例如：

* 用户选择、点击或将光标悬停在某一元素上。
* 用户在键盘中按下某个按键。
* 用户调整浏览器窗口的大小或者关闭浏览器窗口。
* 网页结束加载。
* 表单提交。
* 视频播放、暂停或结束。
* 发生错误。

你可以从这里（以及从 MDN 事件参考文档）中看出，有**相当多**的事件可以被触发。

为了对一个事件做出反应，你要给它附加一个**事件处理器**。这是一个代码块（通常是你作为程序员创建的一个 JavaScript 函数），在事件发生时运行。当这样一个代码块被定义为响应一个事件而运行时，我们说我们在**注册一个事件处理器**。注意，事件处理器有时候被叫做**事件监听器**——从我们的用意来看这两个名字是相同的，尽管严格地来说这块代码既监听也处理事件。监听器留意事件是否发生，处理器对事件发生做出回应。

**备注：** web 事件不是 JavaScript 语言的核心——它们被定义成内置于浏览器的 API。

### 示例：处理点击事件

在这个示例中，我们在页面中有一个 `<button>` 元素：

```html
<button>改变颜色</button>
```

```
button {
  margin: 10px;
}
```

然后我们有一些 JavaScript。我们将在下一节中更详细地讨论这个问题，但现在我们可以说：它为按钮的 `"click"` 事件添加了一个事件处理器，该处理器对该事件的反应是将页面背景设置为随机颜色：

```js
const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() \* (number + 1));
}

btn.addEventListener("click", () => {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
});
```

示例输出如下，试试点击按钮吧：

使用 addEventListener()
---------------------

正如我们在上一个示例中所看到的，能够触发事件的对象有一个 `addEventListener()` 方法，这就是推荐的添加事件处理器的机制。

让我们仔细看一下上一个示例的代码：

```js
const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() \* (number + 1));
}

btn.addEventListener("click", () => {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
});
```

HTML `<button>` 元素将在用户点击按钮时触发一个事件。所以它定义了一个 `addEventListener()` 函数，我们在这里调用它。我们要传入两个参数：

* 字符串 `"click"`，表示我们要监听点击事件。按钮可以触发很多其他的事件，比如当用户将鼠标移到按钮上时（`"mouseover"` 事件），或者当用户按下一个键并且按钮被聚焦时（`"keydown"` 事件）。
* 当事件发生时所调用的函数。在我们的例子中，该函数生成一个随机的 RGB 颜色，并将页面 `<body>` 的 `background-color` 设置为该颜色。

把处理函数作为一个单独的具名函数也是可以的，像这样：

```js
const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() \* (number + 1));
}

function changeBackground() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

btn.addEventListener("click", changeBackground);
```

### 监听其他事件

有许多不同的事件可以由一个按钮元素来触发。让我们来做个实验。

首先，在本地创建 random-color-addeventlistener.html 的副本，并在浏览器中打开。这只是我们已经玩过的简单的随机颜色示例的一个副本。现在试着依次将 `click` 改为以下不同的值，并观察示例中的结果：

* `focus` 和 `blur`：当按钮被聚焦或失焦时，颜色会改变；尝试按下 tab 键来聚焦于按钮，再次按下该键来使按钮失去焦点。这些事件通常用于在聚焦时显示填入表单字段的信息，或者在表单字段填入不正确的值时显示错误信息。
* `dblclick`：颜色只在按钮被双击时改变。
* `mouseover` 和 `mouseout`：当鼠标指针在按钮上悬停，或指针移出按钮时，颜色分别会改变。

一些事件，如 `click`（点击事件），几乎对任何元素都可用。其他事件则更具体，只在某些情况下有用：例如，`play` 事件只对某些元素有效，如 `<video>` 元素。

### 移除监听器

如果你使用 `addEventListener()` 添加了一个事件处理器，你可以使用 `removeEventListener()` 方法再次删除它。例如，这将删除 `changeBackground()` 事件处理器：

```js
btn.removeEventListener("click", changeBackground);
```

事件处理器也可以通过传递 `AbortSignal` 到 `addEventListener()`，然后在拥有 `AbortSignal` 的控制器上调用`abort()`，从而删除事件处理器。例如，要添加一个可以使用 `AbortSignal` 来删除的事件处理器，可以这样做：

```js
const controller = new AbortController();

btn.addEventListener("click",
  () => {
    const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
    document.body.style.backgroundColor = rndCol;
  },
  { signal: controller.signal } // 向该处理器传递 AbortSignal
);
```

然后可以像这样删除上面代码创建的事件处理器：

```js
controller.abort(); // 移除任何/所有与该控制器相关的事件处理器
```

对于简单的小程序，清理旧的、未使用的事件处理器是没有必要的，但对于更大的、更复杂的程序，它可以提高效率。另外，删除事件处理器的能力允许你让同一个按钮在不同的情况下执行不同的动作：你所要做的就是添加或删除处理程序。

### 在单个事件上添加多个监听器

通过对 `addEventListener()` 的多次调用，每次提供不同的处理器，你可以为一个事件设置多个处理器：

```js
myElement.addEventListener("click", functionA);
myElement.addEventListener("click", functionB);
```

当点击按钮时，所有处理器函数都会运行。

### 了解更多

对于 `addEventListener()` 来说，还有很多强大的特性和选项。

这些有些超出了本文的范畴，如果你想了解更多，请参见 `addEventListener` 和 `removeEventListener()` 参考页面。

其他事件监听器机制
---------

我们推荐你使用 `addEventListener()` 来注册事件处理器。这是最强大的方法，在更复杂的程序中，它的扩展性最好。然而，还有两种注册事件处理器的方法，你可能会看到：*事件处理器属性*和*内联事件处理器*。

### 事件处理器属性

可以触发事件的对象（如按钮）通常也有属性，其名称是 `on`，后面是事件的名称。例如，元素有一个属性 `onclick`。这被称为*事件处理器属性*。为了监听事件，你可以将处理函数分配给该属性。

例如，我们可以像这样重写随机颜色示例：

```js
const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() \* (number + 1));
}

btn.onclick = () => {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
};
```

你也可以将处理器属性分配给具名函数：

```js
const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() \* (number + 1));
}

function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

btn.onclick = bgChange;
```

对于事件处理器属性，你不能为一个事件添加一个以上的处理程序。例如，你可以在一个元素上多次调用 `addEventListener('click', handler)`，并在第二个参数中指定不同的函数：

```js
element.addEventListener("click", function1);
element.addEventListener("click", function2);
```

对于事件处理器属性来说，这是不可能的，因为任何后续尝试都会覆写较早设置的属性：

```js
element.onclick = function1;
element.onclick = function2;
```

### 内联事件处理器——不要使用

你可能也会在代码中看到这种形式：

```html
<button onclick="bgChange()">按下我</button>
```

```js
function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}
```

在 web 上发现的最早的注册事件处理器的方法涉及到*事件处理器 HTML 属性*（或*内联事件处理器*），如示例所示。属性值就是你想在事件发生时运行的 JavaScript 代码。上面的示例调用了同一页面上 `<script>` 元素内定义的一个函数，但你也可以直接在属性内插入 JavaScript，比如说：

```html
<button onclick="alert('你好，这是来自旧式事件处理器的一条消息');">
  按下我
</button>
```

你可以找到许多事件处理器属性的 HTML 属性的等价表示；但是，你不应该使用这些属性——它们被认为是不好的做法。如果你正在做一些非常快速的事情，使用事件处理器属性可能看起来很容易，但它们很快就会变得无法管理和低效。

首先，把你的 HTML 和你的 JavaScript 混在一起不是一个好主意，因为它变得难以阅读。把你的 JavaScript 分开是一个好的做法，如果它在一个单独的文件中，你可以把它应用到多个 HTML 文档中。

即使在单个文件中，内联事件处理器也不是个好主意。一个按钮是可以的，但如果你有 100 个按钮呢？你必须在文件中添加 100 个属性；这将很快变成一个维护的噩梦。通过使用 JavaScript，你可以很容易地为页面上的所有按钮添加一个事件处理函数，不管有多少个，使用这样的方法：

```js
const buttons = document.querySelectorAll("button");

for (const button of buttons) {
  button.addEventListener("click", bgChange);
}
```

最后，作为一项安全措施，许多常见的服务器配置将禁止内联 JavaScript。

**你永远不应该使用 HTML 事件处理器属性**——那些已经过时了，使用它们是不好的做法。

事件对象
----

有时候在事件处理函数内部，你可能会看到一个固定指定名称的参数，例如 `event`、`evt` 或 `e`。这被称为**事件对象**，它被自动传递给事件处理函数，以提供额外的功能和信息。例如，让我们稍稍重写一遍我们的随机颜色示例：

```js
const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() \* (number + 1));
}

function bgChange(e) {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}

btn.addEventListener("click", bgChange);
```

**备注：** 你可以在 GitHub 上查看这个示例的完整代码 ，或者在这里查看实时演示。

在这里，可以看到我们在函数中包括一个事件对象 `e`，并在函数中设置背景颜色样式在 `e.target` 上——它指的是按钮本身。事件对象 `e` 的 `target` 属性始终是事件刚刚发生的元素的引用。所以在这个例子中，我们在按钮上设置一个随机的背景颜色，而不是页面。

**备注：** 参见事件委托来了解使用 `event.target` 的示例。

**备注：** 可以使用任何喜欢的名称作为事件对象——只需要选择一个名称，然后可以在事件处理函数中引用它。开发人员最常使用 `e`/`evt`/`event`，因为它们很简单易记。保持一致总是好的——至少对自己。如果可能的话，对别人也是如此。

### 事件对象的额外属性

大多数事件对象都有一套标准的属性和方法，请参阅 `Event` 对象参考，以获得完整的列表。

一些事件对象添加了与该特定类型的事件相关的额外属性。例如，`keydown` 事件在用户按下一个键时发生。它的事件对象是 `KeyboardEvent`，它是一个专门的 `Event` 对象，有一个 `key` 属性，告诉你哪个键被按下：

```html
<input id="textBox" type="text" />
<div id="output"></div>
```

```js
const textBox = document.querySelector("#textBox");
const output = document.querySelector("#output");
textBox.addEventListener("keydown", (event) => {
  output.textContent = `You pressed "${event.key}".`;
});
```

```
div {
  margin: 0.5rem 0;
}
```

试着在文本框中输入文字，观察输出：

阻止默认行为
------

有时，你会遇到一些情况，你希望事件不执行它的默认行为。最常见的例子是 Web 表单，例如自定义注册表单。当你填写详细信息并按提交按钮时，自然行为是将数据提交到服务器上的指定页面进行处理，并将浏览器重定向到某种“成功消息”页面（或相同的页面，如果另一个没有指定）。

当用户没有正确提交数据时，麻烦就来了——作为开发人员，你希望停止提交信息给服务器，并给他们一个错误提示，告诉他们什么做错了，以及需要做些什么来修正错误。一些浏览器支持自动的表单数据验证功能，但由于许多浏览器不支持，因此建议你不要依赖这些功能，并实现自己的验证检查。我们来看一个简单的例子。

首先，这里有一个简单的 HTML 表单，需要你填入名（first name）和姓（last name）：

```html
<form>
  <div>
    <label for="fname">First name: </label>
    <input id="fname" type="text" />
  </div>
  <div>
    <label for="lname">Last name: </label>
    <input id="lname" type="text" />
  </div>
  <div>
    <input id="submit" type="submit" />
  </div>
</form>
<p></p>
```

```
div {
  margin-bottom: 10px;
}
```

接下来是 JavaScript 代码——这里我们在 `submit` 事件（表单提交时触发提交事件）的处理程序中实现一个非常简单的检查，测试文本字段是否为空。如果是这样，我们就在事件对象上调用 `preventDefault()` 函数，停止表单提交，然后在我们的表单下面的段落中显示错误信息，告诉用户出了什么问题：

```js
const form = document.querySelector("form");
const fname = document.getElementById("fname");
const lname = document.getElementById("lname");
const para = document.querySelector("p");

form.addEventListener("submit", (e) => {
  if (fname.value === "" || lname.value === "") {
    e.preventDefault();
    para.textContent = "You need to fill in both names!";
  }
});
```

显然，这是一种非常弱的表单验证——例如，用户输入空格或数字提交表单，表单验证并不会阻止用户提交——但对于演示来说已经足够。输出如下：

**备注：** 你可以在 GitHub 上查看这个示例的完整源代码 preventdefault-validation.html，也可以在线运行它。

事件冒泡
----

事件冒泡描述了浏览器如何处理针对嵌套元素的事件。

### 在父元素上设置监听器

考虑像这样的网页：

```html
<div id="container">
  <button>点我！</button>
</div>
<pre id="output"></pre>
```

这里有一个在其他元素（`<div>`）内部的按钮，可以说这里的 `<div>` 元素是其中包含元素的**父元素**。当我们在父元素附加单击事件处理器，并点击按钮时，会发生什么？

```js
const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `你在 ${e.currentTarget.tagName} 元素上进行了点击\n`;
}

const container = document.querySelector("#container");
container.addEventListener("click", handleClick);
```

你会发现在用户单击按钮时，父元素上触发了单击事件：
```

你在 DIV 元素上进行了点击
```

这是有道理的：按钮在 `<div>` 里面，所以当你点击按钮的时候，你也隐含地点击了它所在的元素。

### 冒泡示例

如果在按钮*及*其父元素上同时添加事件处理器，会发生什么？

```html
<body>
  <div id="container">
    <button>点我！</button>
  </div>
  <pre id="output"></pre>
</body>
```

让我们试着给按钮、它的父元素（`<div>`）以及包含它们的 `<body>` 元素添加点击事件处理器：

```js
const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `你在 ${e.currentTarget.tagName} 元素上进行了点击\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
```

你会发现在用户单击按钮时，所有三个元素都触发了单击事件：
```

你在 BUTTON 元素上进行了点击
你在 DIV 元素上进行了点击
你在 BODY 元素上进行了点击
```

在这种情况下：

* 最先触发按钮上的单击事件
* 然后是按钮的父元素（`<div>` 元素）
* 然后是 `<div>` 的父元素（`<body>` 元素）

我们可以这样描述：事件从被点击的最里面的元素**冒泡**而出。

这种行为可能是有用的，也可能引起意想不到的问题。在接下来的章节中，我们将看到它引起的一个问题，并找到解决方案。

### 视频播放器示例

在这个示例中，我们的页面包含一个视频，最初它为隐藏状态；还有一个标记为“显示视频”的按钮。我们希望有如下交互：

* 当用户单击“显示视频”按钮时，显示包含视频的盒子，但不要开始播放视频。
* 当用户在视频上单击时，开始播放视频。
* 当用户单击盒子内视频以外的任何区域时，隐藏盒子。

HTML 代码看起来像这样：

```html
<button>显示视频</button>

<div class="hidden">
  <video>
    <source
 src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
 type="video/webm" />
    <p>
      你的浏览器不支持 HTML 视频，这里有视频的<a href="rabbit320.mp4"
 >替代链接</a
 >。
    </p>
  </video>
</div>
```

它包含：

* 一个 `<button>` 元素
* 一个 `<div>` 元素，最初其包含 `class="hidden"` 属性
* 一个嵌套在 `<div>` 元素中的 `<video>` 元素

我们使用 CSS 来隐藏具有 `"hidden"` 类的元素。
```

div {
  width: 100%;
  height: 100%;
  background-color: #eee;
}

.hidden {
  display: none;
}

div video {
  padding: 40px;
  display: block;
  width: 400px;
  margin: 40px auto;
}
```

JavaScript 代码看起来像这样：

```js
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", () => video.play());
box.addEventListener("click", () => box.classList.add("hidden"));
```

它添加了三个 `'click'` 事件处理器：

* 一个在 `<button>` 上，它显示了包含 `<video>` 的 `<div>`
* 一个在 `<video>` 上，用于开始播放视频
* 一个在 `<div>` 上，用于隐藏视频

让我们看看这个如何工作：

你应该看到，当你点击按钮时，盒子和它所包含的视频都显示出来。但当你点击视频时，视频开始播放，但盒子又被隐藏起来了！

视频在 `<div>` 内（是它的一部分），所以点击视频会*同时*运行两个事件处理器，导致这种行为。

### 使用 stopPropagation() 修复问题

正如我们在上一节所看到的，事件冒泡有时会产生问题，但有一种方法可以防止这些问题。`Event` 对象有一个可用的函数，叫做 `stopPropagation()`，当在一个事件处理器中调用时，可以防止事件向任何其他元素传递。

我们可以通过修改 JavaScript 代码来修复当前的问题：

```js
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));

video.addEventListener("click", (event) => {
  event.stopPropagation();
  video.play();
});

box.addEventListener("click", () => box.classList.add("hidden"));
```

我们在这里所做的是在 `<video>` 元素的 `'click'` 事件的处理器中对事件对象调用 `stopPropagation()`。这将阻止该事件向盒子内传递。现在试着点击按钮，然后再点击视频：
```

<button>显示视频</button>

<div class="hidden">
  <video>
    <source
 src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
 type="video/webm" />
    <p>
      你的浏览器不支持 HTML 视频，这里有视频的<a href="rabbit320.mp4"
 >替代链接</a
 >。
    </p>
  </video>
</div>
```

```
div {
  width: 100%;
  height: 100%;
  background-color: #eee;
}

.hidden {
  display: none;
}

div video {
  padding: 40px;
  display: block;
  width: 400px;
  margin: 40px auto;
}
```

### 事件捕获

事件传播的另一种形式是*事件捕获*。这就像事件冒泡，但顺序是相反的：事件不是先在最内层的目标元素上发生，然后在连续较少的嵌套元素上发生，而是先在*最小嵌套*元素上发生，然后在连续更多的嵌套元素上发生，直到达到目标。

事件捕获默认是禁用的，你需要在 `addEventListener()` 的 `capture` 选项中启用它。

以下示例类似于之前看到的冒泡示例，除了使用了 `capture` 选项以外：

```html
<body>
  <div id="container">
    <button>点我！</button>
  </div>
  <pre id="output"></pre>
</body>
```

```js
const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `你在 ${e.currentTarget.tagName} 元素上进行了点击\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick, { capture: true });
container.addEventListener("click", handleClick, { capture: true });
button.addEventListener("click", handleClick);
```

在这种情况下，消息出现的顺序发生了颠倒：`<body>` 事件处理器首先触发，然后是 `<div>` 的，最后是 `<button>` 的：
```

你在 BODY 元素上进行了点击
你在 DIV 元素上进行了点击
你在 BUTTON 元素上进行了点击
```

为什么要同时使用捕获和冒泡功能？在过去的坏日子里，当浏览器的交叉兼容性远不如现在时，Netscape 只使用事件捕捉，而 Internet Explorer 只使用事件冒泡。当 W3C 决定尝试将行为标准化并达成共识时，他们最终确定了这个包括这两种行为的系统，这也是现代浏览器所实现的。

默认情况下，几乎所有的事件处理程序都是在冒泡阶段注册的，这在大多数情况下更有意义。

事件委托
----

在上一节中，我们看了一个由事件冒泡引起的问题以及如何解决它。不过，事件冒泡并不只是令人讨厌：它可以非常有用。特别是，它可以实现**事件委托**。在这种做法中，当我们想在用户与大量的子元素中的任何一个互动时运行一些代码时，我们在它们的父元素上设置事件监听器，让发生在它们身上的事件冒泡到它们的父元素上，而不必在每个子元素上单独设置事件监听器。

让我们回到第一个例子，当用户点击一个按钮时，我们设置整个页面的背景颜色。假设取而代之的是，页面被分为 16 个区域，我们想在用户点击每个区域时将其设置为随机颜色。

这里是 HTML 代码：

```html
<div id="container">
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
</div>
```

我们有一些 CSS 代码，来设置每一个区域的尺寸和位置：

```css
.tile {
  height: 100px;
  width: 25%;
  float: left;
}
```

在 JavaScript 代码中，我们向每一个区域中添加单击事件处理器。但是，一个更简单、更有效的选择是在父节点上设置点击事件处理器，并依靠事件冒泡来确保用户点击每个区域时处理程序被执行：

```js
function random(number) {
  return Math.floor(Math.random() \* number);
}

function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  return rndCol;
}

const container = document.querySelector("#container");

container.addEventListener("click", (event) => {
  event.target.style.backgroundColor = bgChange();
});
```

示例输出如下（试着随便点击）：

**备注：** 在这个例子中，我们使用 `event.target` 来获取事件的目标元素（也就是最里面的元素）。如果我们想访问处理这个事件的元素（在这个例子中是容器），我们可以使用 `event.currentTarget`。

**备注：** 完整的源代码见 useful-eventtarget.html；也可以在这里在线运行它。

并不只是网页
------

事件不是 JavaScript 独有的——大多数编程语言都有某种事件模型，而模型的工作方式往往与 JavaScript 的方式不同。事实上，网页的 JavaScript 的事件模型与其他环境中使用的 JavaScript 的事件模型也是不同的。

例如，Node.js 是一个非常流行的 JavaScript 运行时，使开发人员能够使用 JavaScript 来构建网络和服务器端应用程序。Node.js 事件模型依靠监听器来监听事件，依靠发射器来定期发射事件。听起来没有什么不同，但代码却很不同，利用 `on()` 等函数来注册一个事件监听器，而 `once()` 则是注册一个一次性事件监听器，在运行一次后取消注册。HTTP 连接事件文档提供了一个很好的例子。

你也可以使用一种叫做 WebExtensions 的技术编写 JavaScript，建立跨浏览器的附加组件（浏览器功能增强）。其中的事件模型类似于 Web 事件模型，但有点不同——事件监听器的属性是驼峰式的（如 `onMessage` 而不是 `onmessage`），需要与 `addListener` 函数相结合。参见 `runtime.onMessage` (en-US) 页面的示例。

在这个学习阶段，你不需要了解其他此类环境的情况；我们只是想让你明白，在不同的编程环境中，事件会有所不同。

技能测试！
-----

你已经到了本文的结尾，但你能记住最重要的信息吗？在你继续前进之前，要验证你是否记住了这些信息，请前往技能测试：事件 (en-US)页面进行测试。

结论
--

现在你应该知道在这个早期阶段需要了解的所有 web 事件。如上所述，事件并不是 JavaScript 的核心部分——它们是在浏览器 Web API 中定义的。

另外，理解 JavaScript 在不同环境下使用不同的事件模型很重要——从 Web API 到其他领域，如浏览器 WebExtensions 和 Node.js（服务器端 JavaScript）。我们并不期望现在了解所有这些领域，但是当你在学习 web 开发的过程中，理解这些事件的基础是很有帮助的。

如果你有什么不明白的地方，请重新阅读这篇文章，或者联系我们寻求帮助。

参见
--

* domevents.dev——一个非常有用的交互式游乐场应用程序，能够通过探索来学习 DOM 事件系统的行为。
* 事件参考
* Event order（探讨了捕获和冒泡）——由 Peter-Paul Koch 撰写的非常详尽的文章。
* Event accessing（探讨了事件对象）——另外一篇由 Peter-Paul Koch 撰写的非常详尽的文章。
* 上一页
* Overview: Building blocks
* 下一页


在代码中做决定——条件语句 - 学习 Web 开发

在代码中做决定——条件语句
=============

* Overview: Building blocks
* 下一页

在任何的编程语言中，代码需要依靠不同的输入作出决定并且采取行动。例如，在游戏中，如果玩家的生命值变成了 0，那么游戏就结束了。在天气应用中，如果在早晨运行，就显示一张日出的图片；如果在晚上，就显示星星和月亮的图片。在这篇文章中，我们将探索在 JavaScript 中所谓的条件语句是怎样工作的。

|  |  |
| --- | --- |
| 前提： | 基本的计算机知识，对 HTML 和 CSS 有基本的了解，已经阅读了 JavaScript 第一步。 |
| 目标： | 了解怎样在 JavaScript 中使用条件语句控制结构。 |

只需一个条件你就可以拥有……！
---------------

人类（以及其他的动物）无时无刻不在做决定，这些决定都影响着他们的生活，从小事（“我应该吃一片还是两片饼干”）到重要的大事（“我应该留在我的祖国，在我父亲的农场工作；还是应该去美国学习天体物理学”）。

条件语句结构允许我们来描述在 JavaScript 中这样的选择，从不得不作出的选择（例如：“一片还是两片”）到产生的结果或这些选择（也许是“吃一片饼干”可能会“仍然感觉饿”，或者是“吃两片饼干”可能会“感觉饱了，但妈妈会因为我吃掉了所有的饼干而骂我”。）

![一个类似于人的卡通人物拿着一个标有“Cookies”的饼干罐。该人物的头上有一个问号。有两个说话的气泡。左边的说话泡泡有一个饼干。右边的气泡有两块饼干。这意味着该人物正试图决定是要一块饼干还是两块饼干。](/zh-CN/docs/Learn/JavaScript/Building_blocks/conditionals/cookie-choice-small.png)

if...else 语句
------------

让我们看看到目前为止你将会在 JavaScript 中用到的最常见的条件语句类型——`if...else` 语句。

### 基本的 if...else 语法

基本的 `if...else` 语法看起来这样：

```js
if (condition) {
  /\* 条件为真时运行的代码 \*/
} else {
  /\* 否则，运行其他的代码 \*/
}
```

在这里我们有：

1. 关键字 `if`，并且后面跟随括号。
2. 要测试的条件，放到括号里（通常是“这个值大于另一个值吗”或者“这个值存在吗”）。这个条件会利用比较运算符（我们会在最后的模块中讨论）进行比较，并且返回 `true` 或者 `false`。
3. 一组花括号，在里面我们有一些代码——可以是任何我们喜欢的代码，并且只会在条件语句返回 `true` 的时候运行。
4. 关键字 `else`。
5. 另一组花括号，在里面我们有一些代码——可以是任何我们喜欢的代码，并且当条件语句返回值不是 `true`（换句话说，这个返回值为 `false`）的话，它才会运行。

这段代码真的非常易懂——它说“**如果**（if）**条件**（condition）返回 `true`，运行代码 A，**否则**（else）运行代码 B”

注意：你不一定需要 `else` 和第二个花括号——下面的代码也是符合语法规则的：

```js
if (condition) {
  /\* 条件为真时运行的代码 \*/
}

/\* 运行其他的代码 \*/
```

不过，这里你需要注意——在这种情况下，第二段代码不被条件语句控制，所以它**总会**运行，不管条件返回的是 `true` 还是 `false`。这不一定是一件坏事，但这可能不是你想要的——你经常只想要运行一段代码*或者*另一段，而不是两个都运行。

最后，虽然不推荐使用，但有时你可能会看到不加大括号的 `if...else` 语句：

```js
if (condition) /\* 条件为真时运行的代码 \*/
else /\* 否则，运行其他的代码 \*/
```

这是完全有效的代码，但不建议这样使用——因为如果有花括号进行代码切割的话，整体代码被切割为多行代码，更易读和易用。

### 一个真实的例子

为了更好的理解这种语法，让我们考虑一个真实的例子。想像一个孩子被他的父母要求帮助他们做家务。父母可能会说“嗨，宝贝儿，如果你帮我去购物，我会给你额外的零花钱，这样你就能买得起你想要的玩具了。”在 JavaScript 中，我们可以这样表示：

```js
let shoppingDone = false;
let childsAllowance;

if (shoppingDone === true) {
  childsAllowance = 10;
} else {
  childsAllowance = 5;
}
```

这段代码显示的结果是变量 `shoppingDone` 总是返回 `false`，意味着对我们的穷孩子来说很失望。如果孩子去购物的话，就需要依靠我们提供机制来使父母把变量 `shoppingDone` 变成 `true`。

**备注：** 你可以在 Github 上找到这个例子（也可以在线运行）

### else if

最后一个例子提供给我们两个选择或结果，但是如果我们想要两个以上呢？

有一种方法来让你的 `if...else` 连接你的额外的选择和结果——使用 `else if`。每一个额外的选择要求放到 `if () { }` 和 `else { }` 里——看看下面更多涉及到的例子，它们属于一个普通的天气预报的应用的一部分。

```html
<label for="weather">选择今天的天气：</label>
<select id="weather">
  <option value="">--作出选择--</option>
  <option value="sunny">晴天</option>
  <option value="rainy">雨天</option>
  <option value="snowing">雪天</option>
  <option value="overcast">阴天</option>
</select>

<p></p>
```

```js
const select = document.querySelector("select");
const para = document.querySelector("p");

select.addEventListener("change", setWeather);

function setWeather() {
  const choice = select.value;

  if (choice === "sunny") {
    para.textContent = "阳光明媚。穿上短裤吧！去海滩，或公园，吃个冰淇淋。";
  } else if (choice === "rainy") {
    para.textContent = "外面下着雨；带上雨衣和雨伞，不要在外面呆太久。";
  } else if (choice === "snowing") {
    para.textContent =
      "大雪纷飞，天寒地冻！最好呆在家里喝杯热巧克力，或者去堆个雪人。";
  } else if (choice === "overcast") {
    para.textContent =
      "虽然没有下雨，但天空灰蒙蒙的，随时都可能变天，所以要带一件雨衣以防万一。";
  } else {
    para.textContent = "";
  }
}
```

1. 这里我们有 HTML `<select>` 元素让我们选择不同的天气，以及一个简单的段落。
2. 在 JavaScript 中，我们同时存储了对 `<select>` 和 `<p>` 的引用，并对 `<select>` 添加了一个事件监听器，因此，当它的值改变时，`setWeather()` 函数被执行。
3. 当函数运行时，我们首先新建了一个 `choice` 变量去存储当前被选的 `<select>` 中的值。接着我们用条件判断语句根据 `choice` 的值选择性的展示段落中的文本。注意 `else if () { }` 块中的条件是怎么被判断的，除了第一个，它是在 `if () { }` 中被判断的。
4. 最后一个 `else { }` 中的选择通常被叫做“最后招数”——在所有的条件都不为 `true` 时其中的代码会被执行。在这个例子中，如果用户没有选择任何一个选项，它会将段落中的文本清空，例如当用户决定重新选择最开始出现的“--作出选择--”选项时，就会有这样的效果。

**备注：** 你可以在 GitHub 上找到这个例子（也可以在线运行。）

### 关于比较运算符

比较运算符是用来判断条件语句中的条件的。我们先回过头来看看 JavaScript 中的基本算术——数字与运算符文章中的比较运算符。我们有如下选择：

* `===` 和 `!==`——判断一个值是否严格等于，或不等于另一个。
* `<` 和 `>`——判断一个值是否小于，或大于另一个。
* `<=` 和 `>=`——判断一个值是否小于或等于，或者大于或等于另一个。

我们想特别提到测试布尔值（`true` / `false`），和一个你会频繁遇到的通用模式，任何不是 `false`、`undefined`、`null`、`0`、`NaN`、或一个空字符串（`''`）在作为条件语句进行测试时实际返回 `true`，因此可以简单地使用变量名称来测试它是否为真，甚至是否存在（即它不是 undefined 的）。例如：

```js
let cheese = "Cheddar";

if (cheese) {
  console.log("耶！这里有一些制作奶酪吐司的奶酪。");
} else {
  console.log("今天你的吐司上没有奶酪了。");
}
```

而且，回到我们以前关于孩子为自己的父母做家务的例子，你可以这样写：

```js
let shoppingDone = false;
let childsAllowance;

// 不必直接明确指定 'shoppingDone === true'
if (shoppingDone) {
  childsAllowance = 10;
} else {
  childsAllowance = 5;
}
```

### 嵌套 if...else

将一个 `if...else` 语句放在另外一个中嵌套是完全可行的。例如，我们可以更新我们的天气预报应用程序，以显示更多的选择，具体取决于温度：

```js
if (choice === "sunny") {
  if (temperature < 86) {
    para.textContent = `外面现在是 ${temperature} 华氏度——风和日丽。让我们去海滩或公园，吃个冰激凌。`;
  } else if (temperature >= 86) {
    para.textContent = `外面现在是 ${temperature} 华氏度——很热！如果你想出去，一定要涂抹一些防晒霜。`;
  }
}
```

即使代码全部一起工作，每个 `if...else` 语句完全独立于另一个。

### 逻辑运算符：与、或、非

如果要测试多个条件，而不需要编写嵌套 `if...else` 语句，逻辑运算符可以帮助你。当在条件下使用时，前两个执行以下操作：

* `&&`——逻辑与。允许你把两个或多个表达式连在一起，这样所有的表达式都必须单独评估为 `true`，整个表达式才能返回 `true`。
* `||`——逻辑或。允许你把两个或多个表达式连在一起，其中一个或多个表达式必须单独评估为 `true`，整个表达式才能返回 `true`。

举一个逻辑与的例子，刚才的那段代码片段可以写成下面这样：

```js
if (choice === "sunny" && temperature < 86) {
  para.textContent =
    "外面现在是" +
    temperature +
    " 华氏度——风和日丽。让我们去海滩或公园，吃个冰激凌。";
} else if (choice === "sunny" && temperature >= 86) {
  para.textContent =
    "外面现在是" +
    temperature +
    " 华氏度——很热！如果你想出去，一定要涂抹一些防晒霜。";
}
```

所以，只有当 `choice === 'sunny'` *并且* `temperature < 86` 都返回 `true` 时，第一个代码块才能运行。

让我们快速看一个逻辑或的例子：

```js
if (iceCreamVanOutside || houseStatus === "on fire") {
  console.log("你需要赶紧离开这间房子。");
} else {
  console.log("或许你应该呆在这里。");
}
```

最后一种类型的逻辑运算符， **逻辑非** `!` 运算符可以用于对一个表达式取否。让我们把**非运算符**结合上一个例子里的或运算看看：

```js
if (!(iceCreamVanOutside || houseStatus === 'on fire')) {
  console.log('或许你应该呆在这里。');
} else {
  console.log('你需要赶紧离开这间房子。);
}
```

在这一段代码中，如果**逻辑或**所在的语句返回 `true`，则**非运算符**会将其取否，于是整个表达式的返回值将会是 `false`。

可以在任何结构中随意合并很多个逻辑表达式。接下来的例子将会只在**或运算符**两边的语句同时返回 true 时才会执行代码，这也就意味着整个**与运算符**语句将会返回 true：

```js
if ((x === 5 || y > 3 || z <= 10) && (loggedIn || userName === "Steve")) {
  // 执行代码
}
```

在条件语句中运用**或逻辑运算符**最常见的错误是尝试声明变量后，仅检查该变量一次的情况下赋予很多个都会返回 true 的值，不同的值之间用 `||`（或）运算符分隔。比如：

```js
if (x === 5 || 7 || 10 || 20) {
  // 执行代码
}
```

在这个例子里 `if ()` 里的条件总为真，因为 7（或者其他非零的数）的值总是为真。这个条件实际意思是“如果 x 等于 5，或者 7 为真——它总是成立的”。这不是我们想要的逻辑，为了让它正常工作，你必须在每个或运算符的两边指定完整的测试：

```js
if (x === 5 || x === 7 || x === 10 || x === 20) {
  // 执行代码
}
```

switch 语句
---------

`if...else` 语句能够很好地实现条件代码，但是它们不是没有缺点。它们主要适用于只有几个选择的情况，每个都需要相当数量的代码来运行，和/或条件很复杂的情况（例如多个逻辑运算符）。对于只想将变量设置一系列为特定值的选项或根据条件打印特定语句的情况，语法可能会很麻烦，特别是如果有大量选择的时候。

`switch` 语句 在这里是你的朋友——它们以单个表达式/值作为输入，然后查看多个选项，直到找到与该值相匹配的选项，执行与之相关的代码。这里有一些伪代码，可以给你一点灵感：
```

switch (表达式) {
  case 选择1:
    运行这段代码
    break;

  case 选择2:
    否则，运行这段代码
    break;

  // 包含尽可能多的情况

  default:
    实际上，仅仅运行这段代码
}
```

这里我们有：

1. 关键字 `switch`, 后跟一组括号。
2. 括号内可以是表达式或值。
3. 关键字 `case`, 后跟一个选项的表达式/值，后面跟一个冒号。
4. 如果选项与表达式匹配，则运行一些代码。
5. 一个 `break` 语句，分号结尾。如果先前的选择与表达式/值匹配，则浏览器在此停止执行代码块，并执行 switch 语句之后的代码。
6. 你可以添加任意的 case 选项（选项 3-5 个）。
7. 关键字 `default`, 后面跟随和 `case` 完全相同的代码模式（选项 3–5 个），`default` 之后不需要再有选项，并且不需要 `break` 语句，因为之后没有任何运行代码。如果之前没有选项匹配，则运行 `default` 选项。

**备注：** `default` 部分不是必须的——如果表达式不可能存在未知值，则可以安全地省略它。然而，如果有这样的机会，你需要包括它来处理未知的情况。

### switch 语句示例

我们来看一个真实的例子——我们将重写天气预报应用程序，以改用 switch 语句：

```html
<label for="weather">选择今天的天气：</label>
<select id="weather">
  <option value="">--作出选择--</option>
  <option value="sunny">晴天</option>
  <option value="rainy">雨天</option>
  <option value="snowing">雪天</option>
  <option value="overcast">阴天</option>
</select>

<p></p>
```

```js
const select = document.querySelector("select");
const para = document.querySelector("p");

select.addEventListener("change", setWeather);

function setWeather() {
  const choice = select.value;

  switch (choice) {
    case "sunny":
      para.textContent = "阳光明媚。穿上短裤吧！去海滩，或公园，吃个冰淇淋。";
      break;
    case "rainy":
      para.textContent = "外面下着雨；带上雨衣和雨伞，不要在外面呆太久。";
      break;
    case "snowing":
      para.textContent =
        "大雪纷飞，天寒地冻！最好呆在家里喝杯热巧克力，或者去堆个雪人。";
      break;
    case "overcast":
      para.textContent =
        "虽然没有下雨，但天空灰蒙蒙的，随时都可能变天，所以要带一件雨衣以防万一。";
      break;
    default:
      para.textContent = "";
  }
}
```

**备注：** 你可以在 GitHub 上找到这个例子（也可以在线运行。）

三元运算符
-----

在我们举一些例子之前，我们要介绍一下最后一句语法。三元或条件运算符是一个语法的小点，用于测试一个条件，并返回一个值/表达式，如果它是 `true` 则返回其中一个，否则（`false`）返回另外一个——这在某些情况下是很有用的，如果你通过 `true`/`false` 条件来选择，这比 `if...else` 块占用的代码要少很多。伪代码看起来像这样：
```

condition ? 运行这段代码 : 否则，运行这段代码
```

所以我们来看一个简单的例子：

```js
const greeting = isBirthday
  ? "小王生日快乐，祝你有个美好的一天！"
  : "小王早上好。";
```

在这里我们有一个变量叫做 `isBirthday`——如果它是 `true`，我们给客人一个生日快乐的消息；如果不是，改为标准的每日问候。

### 三元运算符示例

三元运算符不仅用于设置变量值，你还可以运行函数或代码行——任何你喜欢的东西。下面的实时示例显示了一个简单的主题选择器，其中网站的样式是用三元运算符应用的。

```html
<label for="theme">选择主题：</label>
<select id="theme">
  <option value="white">白</option>
  <option value="black">黑</option>
</select>

<h1>这是我的网页</h1>
```

```js
const select = document.querySelector("select");
const html = document.querySelector("html");
document.body.style.padding = "10px";

function update(bgColor, textColor) {
  html.style.backgroundColor = bgColor;
  html.style.color = textColor;
}

select.addEventListener("change", () =>
  select.value === "black"
    ? update("black", "white")
    : update("white", "black"),
);
```

在这里，我们有一个 `<select>` 选择主题（黑色或白色）的元素，加上一个简单 h1 以显示网站标题。我们也有一个函数叫做 `update()`，它将两种颜色作为参数（输入）。网站的背景颜色设置为第一个提供的颜色，其文本颜色设置为第二个提供的颜色。

最后，我们还有一个 onchange事件监听器，用于运行一个包含三元运算符的函数。它以 `select.value === 'black'` 测试条件开始。如果这返回 `true`，我们运行带有黑色和白色参数的函数 `update()`，这意味着我们最终得到黑色的背景颜色和白色的文字颜色。如果返回 `false`，我们运行带有白色和黑色参数的函数 `update()`，这意味着站点颜色被反转。

**备注：** 你可以在 GitHub 上找到这个例子（也可以在线运行。）

主动学习：一个简单的日历
------------

在这个例子中，你将帮助我们完成一个简单的日历应用程序。在你的代码中：

* 一个 `<select>` 元素表示一个控件，提供一个选项菜单元素，允许用户在不同月份之间进行选择。
* `onchange` 事件处理器检测 `<select>` 菜单中选择的值何时更改。
* 一个叫做 `createCalendar()` 的函数，它用来绘制日历并在 h1 元素中显示正确的月份。

我们需要你在 `onchange` 处理函数中写一个条件语句，就在 `// ADD CONDITIONAL HERE` 注释的下面。这应该：

1. 查看所选月份（存储在 `choice` 变量中，这将是 `<select>` 值更改后的元素值，例如“1 月”）。
2. 设置一个名为 `days` 的变量，使之等于所选月份的天数。要做到这一点，你必须查询一年中每个月的天数。在这个例子中，你可以忽略闰年。

提示：

* 建议你使用逻辑或将多个月组合成一个单一条件，许多月份共享相同的天数。
* 考虑最常用的天数，并将其用作默认值。

如果你犯了错误，可以随时使用“重置”按钮重置该示例。如果真的卡住了，请按“查看解答”查看解决方案。
```

<h2>实时输出</h2>
<div class="output" style="height: 500px;overflow: auto;">
  <label for="month">Select month: </label>
  <select id="month">
    <option value="January">一月</option>
    <option value="February">二月</option>
    <option value="March">三月</option>
    <option value="April">四月</option>
    <option value="May">五月</option>
    <option value="June">六月</option>
    <option value="July">七月</option>
    <option value="August">八月</option>
    <option value="September">九月</option>
    <option value="October">十月</option>
    <option value="November">十一月</option>
    <option value="December">十二月</option>
  </select>

  <h1></h1>

  <ul></ul>
</div>

<h2>可编辑代码</h2>
<p class="a11y-label">按下 Esc 以将焦点移出代码区（Tab 键添加制表符）。</p>

<textarea id="code" class="playable-code" style="height: 400px;width: 95%">
const select = document.querySelector('select');
const list = document.querySelector('ul');
const h1 = document.querySelector('h1');

select.addEventListener('change', () => {
  const choice = select.value;

  // ADD CONDITIONAL HERE

  createCalendar(days, choice);
});

function createCalendar(days, choice) {
  list.innerHTML = '';
  h1.textContent = choice;
  for (let i = 1; i <= days; i++) {
    const listItem = document.createElement('li');
    listItem.textContent = i;
    list.appendChild(listItem);
  }
}

createCalendar(31,'January');
</textarea>

<div class="playable-buttons">
  <input id="reset" type="button" value="重置" />
  <input id="solution" type="button" value="查看解答" />
</div>
```

```
.output \* {
  box-sizing: border-box;
}

.output ul {
  padding-left: 0;
}

.output li {
  display: block;
  float: left;
  width: 25%;
  border: 2px solid white;
  padding: 5px;
  height: 40px;
  background-color: #4a2db6;
  color: white;
}

html {
  font-family: sans-serif;
}

h2 {
  font-size: 16px;
}

.a11y-label {
  margin: 0;
  text-align: right;
  font-size: 0.7rem;
  width: 98%;
}

body {
  margin: 10px;
  background: #f5f9fa;
}
```

```
const textarea = document.getElementById("code");
const reset = document.getElementById("reset");
const solution = document.getElementById("solution");
let code = textarea.value;
let userEntry = textarea.value;

function updateCode() {
  eval(textarea.value);
}

reset.addEventListener("click", function () {
  textarea.value = code;
  userEntry = textarea.value;
  solutionEntry = jsSolution;
  solution.value = "查看解答";
  updateCode();
});

solution.addEventListener("click", function () {
  if (solution.value === "查看解答") {
    textarea.value = solutionEntry;
    solution.value = "隐藏解答";
  } else {
    textarea.value = userEntry;
    solution.value = "查看解答";
  }
  updateCode();
});

const jsSolution = `const select = document.querySelector('select');
const list = document.querySelector('ul');
const h1 = document.querySelector('h1');

select.addEventListener('change', () => {
 const choice = select.value;
 let days = 31;
 if (choice === 'February') {
 days = 28;
 } else if (choice === 'April' || choice === 'June' || choice === 'September'|| choice === 'November') {
 days = 30;
 }

 createCalendar(days, choice);
});

function createCalendar(days, choice) {
 list.innerHTML = '';
 h1.textContent = choice;
 for (let i = 1; i <= days; i++) {
 const listItem = document.createElement('li');
 listItem.textContent = i;
 list.appendChild(listItem);
 }
}

createCalendar(31,'January');`;

let solutionEntry = jsSolution;

textarea.addEventListener("input", updateCode);
window.addEventListener("load", updateCode);

// stop tab key tabbing out of textarea and
// make it write a tab at the caret position instead

textarea.onkeydown = function (e) {
  if (e.keyCode === 9) {
    e.preventDefault();
    insertAtCaret("\t");
  }

  if (e.keyCode === 27) {
    textarea.blur();
  }
};

function insertAtCaret(text) {
  const scrollPos = textarea.scrollTop;
  let caretPos = textarea.selectionStart;
  const front = textarea.value.substring(0, caretPos);
  const back = textarea.value.substring(
    textarea.selectionEnd,
    textarea.value.length,
  );

  textarea.value = front + text + back;
  caretPos += text.length;
  textarea.selectionStart = caretPos;
  textarea.selectionEnd = caretPos;
  textarea.focus();
  textarea.scrollTop = scrollPos;
}

// Update the saved userCode every time the user updates the text area code

textarea.onkeyup = function () {
  // We only want to save the state when the user code is being shown,
  // not the solution, so that solution is not saved over the user code
  if (solution.value === "查看解答") {
    userEntry = textarea.value;
  } else {
    solutionEntry = textarea.value;
  }

  updateCode();
};
```

主动学习：更多颜色选择！
------------

在这个例子中，你将要采取我们前面看到的三元运算符示例，并将三元运算符转换为一个 switch 语句，这将允许我们对简单的网站应用更多的选择。看看 `<select>`——这次你会看到它不是两个主题选项，而是五个。你需要在 `// ADD SWITCH STATEMENT` 注释下面添加一个 switch 语句：

* 它应该接受 `choice` 变量作为其输入表达式。
* 对于每种情况，选择应该等于可以选择的可能值之一，即 `white`、`black`、`purple`、`yellow` 或 `psychedelic`。
* 对于每种情况，应运行 `update()` 函数，并传递两个颜色值，第一个颜色值为背景颜色，第二个颜色值为文本颜色。请记住，颜色值是字符串，因此需要用引号括起来。

如果你犯了错误，可以随时使用“重置”按钮重置该示例。如果真的卡住了，请按“查看解答”查看解决方案。
```

<h2>实时输出</h2>
<div class="output" style="height: 300px;">
  <label for="theme">选择主题：</label>
  <select id="theme">
    <option value="white">White</option>
    <option value="black">Black</option>
    <option value="purple">Purple</option>
    <option value="yellow">Yellow</option>
    <option value="psychedelic">Psychedelic</option>
  </select>

  <h1>这是我的网站</h1>
</div>

<h2>可编辑代码</h2>
<p class="a11y-label">按下 Esc 以将焦点移出代码区（Tab 键添加制表符）。</p>

<textarea id="code" class="playable-code" style="height: 450px;width: 95%">
const select = document.querySelector('select');
const html = document.querySelector('.output');

select.addEventListener('change', () => {
  const choice = select.value;

  // ADD SWITCH STATEMENT
}

function update(bgColor, textColor) {
  html.style.backgroundColor = bgColor;
  html.style.color = textColor;
}</textarea
>

<div class="playable-buttons">
  <input id="reset" type="button" value="重置" />
  <input id="solution" type="button" value="查看解答" />
</div>
```

```
html {
  font-family: sans-serif;
}

h2 {
  font-size: 16px;
}

.a11y-label {
  margin: 0;
  text-align: right;
  font-size: 0.7rem;
  width: 98%;
}

body {
  margin: 10px;
  background: #f5f9fa;
}
```

```
const textarea = document.getElementById("code");
const reset = document.getElementById("reset");
const solution = document.getElementById("solution");
let code = textarea.value;
let userEntry = textarea.value;

function updateCode() {
  eval(textarea.value);
}

reset.addEventListener("click", function () {
  textarea.value = code;
  userEntry = textarea.value;
  solutionEntry = jsSolution;
  solution.value = "查看解答";
  updateCode();
});

solution.addEventListener("click", function () {
  if (solution.value === "查看解答") {
    textarea.value = solutionEntry;
    solution.value = "隐藏解答";
  } else {
    textarea.value = userEntry;
    solution.value = "查看解答";
  }
  updateCode();
});

const jsSolution = `const select = document.querySelector('select');
const html = document.querySelector('.output');

select.addEventListener('change', () => {
 const choice = select.value;

 switch(choice) {
 case 'black':
 update('black','white');
 break;
 case 'white':
 update('white','black');
 break;
 case 'purple':
 update('purple','white');
 break;
 case 'yellow':
 update('yellow','darkgray');
 break;
 case 'psychedelic':
 update('lime','purple');
 break;
 }
});

function update(bgColor, textColor) {
 html.style.backgroundColor = bgColor;
 html.style.color = textColor;
}`;

let solutionEntry = jsSolution;

textarea.addEventListener("input", updateCode);
window.addEventListener("load", updateCode);

// stop tab key tabbing out of textarea and
// make it write a tab at the caret position instead
textarea.onkeydown = function (e) {
  if (e.keyCode === 9) {
    e.preventDefault();
    insertAtCaret("\t");
  }

  if (e.keyCode === 27) {
    textarea.blur();
  }
};

function insertAtCaret(text) {
  const scrollPos = textarea.scrollTop;
  let caretPos = textarea.selectionStart;
  const front = textarea.value.substring(0, caretPos);
  const back = textarea.value.substring(
    textarea.selectionEnd,
    textarea.value.length,
  );

  textarea.value = front + text + back;
  caretPos += text.length;
  textarea.selectionStart = caretPos;
  textarea.selectionEnd = caretPos;
  textarea.focus();
  textarea.scrollTop = scrollPos;
}

// Update the saved userCode every time the user updates the text area code

textarea.onkeyup = function () {
  // We only want to save the state when the user code is being shown,
  // not the solution, so that solution is not saved over the user code
  if (solution.value === "Show solution") {
    userEntry = textarea.value;
  } else {
    solutionEntry = textarea.value;
  }
  updateCode();
};
```

技能测试！
-----

你已经阅读到了本篇文章的末尾，但还能记清楚最重要的部分吗？你可以做一些深入的测试，来验证你是否在进行下一步学习之前，记住了这些知识，请参阅技能测试：条件语句 (en-US)

结论
--

这就是现在真正需要了解的 JavaScript 中的条件结构！如果有什么不明白的，请随时再次阅读文章，或者联系我们寻求帮助。

参见
--

* 比较运算符
* 条件声明详细
* if...else 参考
* 条件（三元）运算符参考
* Overview: Building blocks
* 下一页


函数返回值 - 学习 Web 开发

函数返回值
=====

* 上一页
* Overview: Building blocks
* 下一页

函数返回值 - 是本章中最后一个基础概念，让我们一起来瞧瞧.。有些函数在执行完毕后不会返回一个有用的值，但有些会，重要的是理解返回的是什么，怎样使用这些值在你的代码中，我们将在下面讨论这些。

|  |  |
| --- | --- |
| 前提： |
 基础的计算机知识，懂得基础的 HTML 和 CSS,
 JavaScript 第一步学习，函数-可重用的代码块。
  |
| 目标： | 理解什么函数的返回值 , 和如何使用它们 |

什么是返回值？
-------

返回值意如其名，是指函数执行完毕后返回的值。你已经多次遇见过返回值，尽管你可能没有明确的考虑过他们。让我们一起回看一些熟悉的代码：

```js
var myText = "I am a string";
var newString = myText.replace("string", "sausage");
console.log(newString);
// the replace() string function takes a string,
// replaces one substring with another, and returns
// a new string with the replacement made
```

在第一篇函数文章中，我们确切地看到了这一块代码。我们对 `myText` 字符串调用 replace() 功能，并通过这两个参数的字符串查找，和子串替换它。当这个函数完成（完成运行）后，它返回一个值，这个值是一个新的字符串，它具有替换的功能。在上面的代码中，我们保存这个返回值，以作为`newString`变量的内容。

如果你看看替换功能 MDN 参考页面，你会看到一个返回值。知道和理解函数返回的值是非常有用的，因此我们尽可能地包含这些信息。

一些函数没有返回值就像 (在我们的参考页中，返回值在这种情况下被列出为空值 `void` 或未定义值 `undefined` 。).例如，我们在前面文章中创建的 displayMessage() function , 由于调用的函数的结果，没有返回特定的值。它只是让一个提示框出现在屏幕的某个地方——就是这样！

通常，返回值是用在函数在计算某种中间步骤。你想得到最终结果，其中包含一些值。那些值需要通过一个函数计算得到，然后返回结果可用于计算的下一个阶段。

### 在自定义的函数中使用返回值

要从自定义函数返回值，你需要使用…等待它… return 关键字。我们最近在random-canvas-circles.html示例中看到了这一点。我们的 `draw()` 函数绘制 100 随机圆在 HTML 的`<canvas>`:

```js
function draw() {
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  for (var i = 0; i < 100; i++) {
    ctx.beginPath();
    ctx.fillStyle = "rgba(255,0,0,0.5)";
    ctx.arc(random(WIDTH), random(HEIGHT), random(50), 0, 2 \* Math.PI);
    ctx.fill();
  }
}
```

在每个循环迭代，`random()`函数调用了三次，分别生成当前圆的 x 坐标，一个随机值 Y 坐标和半径。`random()`函数接受一个参数 - 一个整数，返回 0 到这个整数之间的随机数。看起来像这样：

```js
function randomNumber(number) {
  return Math.floor(Math.random() \* number);
}
```

这也可以写成下面这样：

```js
function randomNumber(number) {
  var result = Math.floor(Math.random() \* number);
  return result;
}
```

但是第一个版本写得更快，而且更紧凑。

我们每次调用函数都返回`Math.floor(Math.random()*number)`计算的数学结果。这个返回值出现在调用函数的位置上，并且代码继续。例如，如果我们运行下面的行：

```js
ctx.arc(random(WIDTH), random(HEIGHT), random(50), 0, 2 \* Math.PI);
```

这三次`random()`调用分别返回值 500, 200 和 35，实际上这一行这样运行：

```js
ctx.arc(500, 200, 35, 0, 2 \* Math.PI);
```

在运行该行之前，首先运行该行上的函数调用，并用其返回值替换该函数调用。

主动学习：我们自己的返回值函数
---------------

让我们着手编写具有我们自己的返回值的函数。

1. 首先，从 GitHub 的function-library.html文件复制一份本地副本。这是一个简单的 HTML 页面包含一个 `<input>` 文本域和一个段落。还有一个 `<script>` 元素，我们在两个变量中存储了对两个 HTML 元素的引用。这个小页面允许你在文本框中输入一个数字，并在下面的段落中显示不同的数字。
2. 让我们添加一些有用的函数。在现有的两行 JavaScript 下面，添加以下函数定义：

```js
function squared(num) {
  return num \* num;
}

function cubed(num) {
  return num \* num \* num;
}

function factorial(num) {
  var x = num;
  while (x > 1) {
    num \*= x - 1;
    x--;
  }
  return num;
}
```

`squared()` 和 `cubed()` 功能是相当明显的 - 他们的平方或立方的数作为一个参数返回。factorial() 函数返回给定数字的阶乘。
3. 接下来，我们将包括一种打印输入到文本输入中的数字的信息的方法。在现有函数下面输入以下事件处理程序：

```js
input.onchange = function () {
  var num = input.value;
  if (isNaN(num)) {
    para.textContent = "You need to enter a number!";
  } else {
    para.textContent =
      num +
      " squared is " +
      squared(num) +
      ". " +
      num +
      " cubed is " +
      cubed(num) +
      ". " +
      num +
      " factorial is " +
      factorial(num) +
      ".";
  }
};
```

这里我们创建一个`onchange`事件处理程序，当文本框上面的 change 事件被触发的之后，事件处理程序就会运行 - 就是说，一个新的值被输入到文本框并且被提交（就比如，输入一个值，然后按 Tab）。当这个匿名函数运行时，输入框中的值将被存储在`num`变量中。
 接下来，我们进行条件测试——如果输入的值不是数字，则在段落中打印错误消息。if 语句判断isNaN(num)表达式是否返回 true。我们用`isNaN()`函数测试`num`的值是否不是一个数字 - 如果不是数字，就返回`true`，否则返回`false`。
 如果测试返回 false，则数值是一个数字，所以我们在段落元素中打印出一个句子，说明数字的平方、立方体和阶乘是什么。这句话叫 squared()，cubed()，和 factorial() 函数来获得所需的值。
4. 保存你的代码，将其加载到浏览器中，然后尝试。

**备注：** 如果你有麻烦让例子工作，对比GitHub 的已完成版检查你的代码（或看它在线运行），或寻求我们的帮助。

在这一点上，我们希望你编写一个自己的几个函数，并将它们添加到库中。这个数的平方根或立方根，或一个圆的周长和半径是多少？

这个练习提出了一些重要的观点，除了研究如何使用返回语句之外。此外，我们还有：

* 查看另一个将错误处理写入函数的示例。它是否提供了任何必要的参数通常是一个好主意，另一方面对可选参数提供默认值。这样，你的程序就不太可能出错了。
* 关于创建函数库思想的思考。随着你深入到你的编程生涯，你将开始一次又一次地做同样的事情。这是一个好主意，开始保持你自己的实用工具库，你经常使用 - 你可以把它们复制到你的新代码，甚至只是把它应用到任何你需要的 HTML 页面。
结论
--

因此，我们让它 - 功能是有趣的，非常有用的，虽然有很多要谈论他们的语法和功能，相当容易理解的正确的文章学习。

如果你有什么不明白的地方，可以再通读一遍，或者联系我们寻求帮助。

参见
--

* Functions in-depth — 详细介绍更多高级功能相关信息的指南。
* Callback functions in JavaScript — 一个常见的 JavaScript 模式是把一个函数传递给另一个函数作为参数，然后在第一个函数中调用它。这有点超出了这门课的范围，但值得学习很久。
* 上一页
* Overview: Building blocks
* 下一页


绘图 - 学习 Web 开发

绘图
==

* 上一页
* Overview: Client-side web APIs
* 下一页

浏览器包含一些非常强大的图形编程工具，从可缩放矢量图形（Scalable Vector Graphics，简称 SVG）语言到用于在 HTML `<canvas>` 元素上绘制图形的 API（参阅 Canvas API 和 WebGL）。本文对 `<canvas>` 进行介绍，并提供更多的学习资源。

|  |  |
| --- | --- |
| 前提： |
 JavaScript 基础（见
 JavaScript 第一步，创建 JavaScript 代码块，JavaScript 对象入门），Web API 基础知识。
  |
| 目标： | 学习 JavaScript 在 `<canvas>` 元素中绘图的基础知识。 |

网络图形
----

我们来讨论 HTML 的 多媒体和嵌入式 模块，早先的网页只有单调的文字，后来才引入了图像，起初是通过 `<img>` 元素的方式，后来出现了类似于 `background-image` 的 CSS 属性和 SVG 图像等方式。

然而，这还不够好。当你能够使用 CSS 和 JavaScript 让 SVG 矢量图动起来时，位图却依然没有相应的支持。同时 SVG 动画的可用工具也少得可怜。有效地生成动画、游戏画面、3D 场景和其他的需求依然没有满足，而这些在诸如 C++ 或者 Java 等底层语言中却司空见惯。

当浏览器开始支持 HTML 画布元素 `<canvas>` 和相关的 Canvas API（由苹果公司在 2004 年前后发明，后来其他的浏览器开始跟进）时，形势开始改善。下面你会看到，canvas 提供了许多有用的工具，特别是当捆绑了由网络平台提供的一些其他的 API 时。它们用来生成 2D 动画、游戏画面和数据分析图，以及其他类型的 app。

下面的例子显示的是一个基于 canvas 的简单的 2D 弹跳球动画，前面我们在 JavaScript 对象入门 的章节中见到过。

大约在 2006 - 2007 年，Mozilla 开始测试 3D 画布。后来演化为 WebGL，它获得了各大浏览器厂商的认可，于是大约在 2009 - 2010 年间得到了标准化。WebGL 可以让你在 web 浏览器中生成真正的 3D 图形。下面的例子显示了一个简单的旋转的 WebGL 立方体。

由于原生的 WebGL 代码非常复杂，本文主要针对 2D 画布。然而，你也可以通过 WebGL 介绍页面 找到 WebGL 原生代码的教程，来学习如何更容易地使用 WebGL 库来创建一个 3D 场景。

**备注：** 画布的基本功能有良好的跨浏览器支持。但存在例外：IE 8 及以下不支持 2D 画布，IE 11 及以下不支持 WebGL。

主动学习：开始使用 <canvas>
------------------

要在网页中创建 2D 或者 3D 场景，必须在 HTML 文件中插入一个 `<canvas>` 元素，以界定网页中的绘图区域。这很简单，如下所示：

```html
<canvas width="320" height="240"></canvas>
```

网页中会生成一块 320 × 240 像素的画布。

在 canvas 标签内，你可以放置一些反馈信息，如果用户的浏览器不支持画布功能，这些内容就会显示出来。

```html
<canvas width="320" height="240">
  <p>卧槽你的浏览器竟然不支持 canvas！</p>
</canvas>
```

当然这条信息实在是没什么用！在实际情况中最好提供与画布内容相关的反馈信息。比如，如果无法渲染实时更新的股价曲线图，就应提供一幅静态的、最近的股价曲线图，并用 alt 显示出价格信息。

### 创建画布并确定尺寸

让我们开始吧：创建画布，准备尝试绘制图形。

1. 首先下载 0\_canvas\_start.html 文件，用文本编辑器打开。
2. 在 `<body>` 标签下面填加以下代码。

```html
<canvas class="myCanvas">
  <p>添加恰当的反馈信息。</p>
</canvas>
```

我们为 `<canvas>` 元素添加了一个 `class`，使得在网页中选择多个画布时会容易些。这里我们移除了 `width` 和 `height` 属性 (你可以随时添上。但是我们会在下方用 JavaScript 把它们添加回来）。不明确指定宽高时，画布默认尺寸为 300 × 150 像素。
3. 现在，请在 `<script>` 元素内添加以下 JavaScript 代码：

```js
var canvas = document.querySelector(".myCanvas");
var width = (canvas.width = window.innerWidth);
var height = (canvas.height = window.innerHeight);
```

这里我们用 `canvas` 变量来存储画布的引用。第二行中我们将 `Window.innerWidth`（可视区域宽度）赋值给一个新变量 `width` 和画布的 `canvas.width` 变量，（第三行同理）。然后我们就得到了一个充满窗口的画布。
 你还可以看到我们使用了多个等号为多个变量进行连续赋值，这在 JavaScript 中是允许的，很适合为多个变量同时赋一个相同的值。后文中你会发现，使用 `width` 和 `height` 变量可以更快捷地访问画布的长宽（比如在画布正中央绘制一条垂直线）。
4. 如果现在保存文件，浏览器中什么也不会显示，这并没有问题，但是滚动条还是可见的，这就是问题了。原因是我们的“全窗尺寸画布”包含 `<body>` 元素的外边距（`margin`），使得文档比窗口略宽。为使滚动条消失，需要删除 `<body>` 元素的 `margin` 并将 `overflow` 设置为 `hidden`。在文档的 `<head>` 中添加以下代码即可：

```html
<style>
 body {
 margin: 0;
 overflow: hidden;
 }
</style>
```

此时滚动条就消失了。

**备注：** 如上文所讲，一般情况下图片的尺寸可以通过 HTML 属性或 DOM 属性来设定。你也可以使用 CSS，但问题是画布在渲染完毕后其尺寸就是固定的了，如果试图调整，就会与其他图象一样（其实渲染好的画布就是一副图片），所显示的内容将变得像素化或扭曲变形。

### 获取画布上下文（canvas context）并完成设置

画布模板设置还有最后一步。我们需要获得一个对绘画区域的特殊的引用（称为“上下文”）用来在画布上绘图。可通过 `HTMLCanvasElement.getContext()` 方法获得基础的绘画功能，需要提供一个字符串参数来表示所需上下文的类型。

这里我们需要一个 2d 画布，在 `<script>` 元素内添加以下 JS 代码即可：

```js
var ctx = canvas.getContext("2d");
```

**备注：** 可选上下文还包括 WebGL（`webgl`）、WebGL 2（`webgl2`）等等，但本文暂不涉及。

好啦，现已万事具备！`ctx` 变量包含一个 `CanvasRenderingContext2D` 对象，画布上所有绘画操作都会涉及到这个对象。

开始前我们先初尝一下 canvas API。在 JS 代码中添加以下两行，将画布背景涂成黑色：

```js
ctx.fillStyle = "rgb(0, 0, 0)";
ctx.fillRect(0, 0, width, height);
```

这里我们使用画布的 `fillStyle` 属性（和 CSS 属性 色值 一致）设置填充色，然后使用 `fillRect` 方法绘制一个覆盖整个区域的矩形（前两个参数是矩形左上顶点的坐标，后两个参数是矩形的长宽，现在你知道 `width` 和 `height` 的作用了吧）。

好的，模板已经就位，我们要开始了。

2D 画布基础
-------

如上文所讲，所有绘画操作都离不开 `CanvasRenderingContext2D` 对象（这里叫做 `ctx`）。许多操作都需要提供坐标来指示绘图的确切位置。画布左上角的坐标是 (0, 0)，横坐标（x）轴向右延伸，纵坐标（y）轴向下延伸。

![](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics/canvas_default_grid.png)

绘图操作可基于原始矩形模型实现，也可通过追踪一个特定路径后填充颜色实现。下面分别讲解。

### 简单矩形

让我们从简单矩形开始。

1. 首先，复制一份刚才创建的画布模板 (如果你没有严格按上述步骤进行，请下载 1\_canvas\_template.html)。
2. 然后在 JS 代码末尾添加下面两行：

```js
ctx.fillStyle = "rgb(255, 0, 0)";
ctx.fillRect(50, 50, 100, 150);
```

保存并刷新，画布上将出现一个红色的矩形。其左边和顶边与画布边缘距离均为 50 像素（由前两个参数指定），宽 100 像素、高 150 像素（由后两个参数指定）。
3. 然后再添加一个绿色矩形。在 JS 代码末尾添加下面两行：

```js
ctx.fillStyle = "rgb(0, 255, 0)";
ctx.fillRect(75, 75, 100, 100);
```

保存并刷新，新的矩形就会出现。这里引出了一个新问题：绘制矩形、线等操作按出现的顺序依次进行。就像粉刷墙面时，两层重叠时新层总会覆盖旧层。这一点是无法改变的，因此在绘制图形时一定要慎重考虑顺序问题。
4. 你还可以通过指定半透明的颜色来绘制半透明的图形，比如使用 `rgba()`。 `a` 指定了“α 通道”的值，也就是颜色的透明度。值越高透明度越高，底层的内容就越清晰。在代码中添加以下两行：

```js
ctx.fillStyle = "rgba(255, 0, 255, 0.75)";
ctx.fillRect(25, 100, 175, 50);
```

5. 现在你可以自己尝试绘制一些矩形了，玩得开心！
### 描边（stroke）和线条宽度

目前我们绘制的矩形都是填充颜色的，我们也可以绘制仅包含外部框线（图形设计中称为**描边**）的矩形。你可以使用 `strokeStyle` 属性来设置描边颜色，使用 `strokeRect` 来绘制一个矩形的轮廓。

1. 在上文的 JS 代码的末尾添加以下代码：

```js
ctx.strokeStyle = "rgb(255, 255, 255)";
ctx.strokeRect(25, 25, 175, 200);
```

2. 默认的描边宽度是 1 像素，可以通过调整 `lineWidth` 属性（接受一个表示描边宽度像素值的数字）的值来修改。在上文两行后添加以下代码（读者注：此代码要放到两行代码中间才有效）：

```js
ctx.lineWidth = 5;
```

现在可以看到白色的外边框变得更粗了。就这么简单，示例看上去像这样：

**备注：** 完整代码请访问 GitHub： 2\_canvas\_rectangles.html。

### 绘制路径

可以通过绘制路径来绘制比矩形更复杂的图形。路径中至少要包含钢笔运行精确路径的代码以确定图形的形状。画布提供了许多函数用来绘制直线、圆、贝塞尔曲线，等等。

重新复制一份（1\_canvas\_template.html），然后在其中绘制新的示例。

一些通用的方法和属性将贯穿以下全部内容：

* `beginPath()`：在钢笔当前所在位置开始绘制一条路径。在新的画布中，钢笔起始位置为 (0, 0)。
* `moveTo()`：将钢笔移动至另一个坐标点，不记录、不留痕迹，只将钢笔“跳”至新位置。
* `fill()`：通过为当前所绘制路径的区域填充颜色来绘制一个新的填充形状。
* `stroke()`：通过为当前绘制路径的区域描边，来绘制一个只有边框的形状。
* 路径也可和矩形一样使用 `lineWidth` 和 `fillStyle` / `strokeStyle` 等功能。

以下是一个典型的简单路径绘制选项：

```js
ctx.fillStyle = "rgb(255, 0, 0)";
ctx.beginPath();
ctx.moveTo(50, 50);
// 绘制路径
ctx.fill();
```

#### 画线

我们来在画布上绘制一个等边三角形。

1. 首先，在代码底部添加下面的辅助函数。它可以将角度换算为弧度，在为 JavaScript 提供角度值时非常实用，JS 基本上只接受弧度值，而人类更习惯用角度值。

```js
function degToRad(degrees) {
  return (degrees \* Math.PI) / 180;
}
```

2. 然后，在刚才复制好的文件中添加下面的内容，以开始路径的绘制。此处为我们为三角形设置了颜色，准备绘制，然后将钢笔移动至 (50, 50)（没有绘制任何内容）。然后准备在新的坐标开始绘制三角形。

```js
ctx.fillStyle = "rgb(255, 0, 0)";
ctx.beginPath();
ctx.moveTo(50, 50);
```

3. 接下来在脚本中添加以下代码：

```js
ctx.lineTo(150, 50);
var triHeight = 50 \* Math.tan(degToRad(60));
ctx.lineTo(100, 50 + triHeight);
ctx.lineTo(50, 50);
ctx.fill();
```

我们来逐行解释：
 首先绘制一条直线，终点坐标为 (150, 50)。此时路径沿 x 轴向右行走 100 像素。
 然后利用三角函数来计算等边三角形的高。这里我们要绘制的三角形是朝下的。等边三角形每个角均为 60°，为计算高的值，我们可以将三角形从正中心分割为两个直角三角形，每个直角三角形的三个角分别为 90°、60°、30°。对于边：
    * 最长的边称为 **斜边**。
    * 紧挨 60° 角的边称为 **临边**，显然地，它的长度是刚才绘制的线的一半，即 50 像素。
    * 60° 角对面的边称为 **对边**，即三角形的高，需要计算得到。![](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics/trigonometry.png)
 通过基本三角函数可得：临边长度乘以角的正切等于对边长度。于是可得三角形的高为 `50 * Math.tan(degToRad(60))`。由于 `Math.tan()` 接受数值的单位为弧度，于是我们用刚才的 `degToRad()` 函数将 60° 换算为弧度。
4. 有了三角形的高，我们来绘制另一条线，终点坐标为 `(100, 50+triHeight)`。X 坐标值很简单，应在刚才绘制的水平线两顶点正中间位置。Y 值应为 50 加上三角形的高，因为高即三角形底边到顶点的距离。
5. 下一条线的终点坐标为绘制整个三角形的起点坐标。
6. 最后，运行 `ctx.fill()` 来终止路径，并为图形填充颜色。

#### 画圆

下面来看可在画布中绘制圆的方法——`arc()`，通过连续的点来绘制整个圆或者弧（arc，即局部的圆）。

1. 在代码中添加以下几行，以向画布中添加一条弧。
```

ctx.fillStyle = 'rgb(0, 0, 255)';
ctx.beginPath();
ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false);
ctx.fill();
```

`arc()` 函数有六个参数。前两个指定圆心的位置坐标，第三个是圆的半径，第四、五个是绘制弧的起、止角度（给定 0° 和 360° 便能绘制一个完整的圆），第六个是绘制方向（`false` 是顺时针，`true` 是逆时针）。

**备注：** 0° 设定为水平向右。
2. 我们再来画一条弧：
```

ctx.fillStyle = 'yellow';
ctx.beginPath();
ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true);
ctx.lineTo(200, 106);
ctx.fill();
```

模式基本一样，但有两点不同：
    * 将 `arc()` 的最后一个参数设置为 `true`，意味着弧将逆时针绘制，也就意味着即使起、止角度分别设置为 -45°、45°，我们还是得到了区域外的一条 270° 的弧。如果把 `true` 改为`false` 重新运行，将得到 90° 的弧。
    * 在调用 `fill()` 前，我们绘制了一条终点为圆心的直线。然后我们就渲染出一个惟妙惟肖的吃豆人模型。如果删除这条线（试试呗）再重新运行代码，你只能得到一个起止点间被砍掉一块的圆。这向我们展示了画布的另一个重要事项：如果要填充一个未完成（也就是没有首尾相接）的路径，浏览器将在起、止点件绘制一条直线，然后直接填充。

示例现在应该跟下图一致：

**备注：** 完整代码可到 GitHub 下载：3\_canvas\_paths.html。

**备注：** 请访问我们的 用画布绘图 入门课程来学习更多高级的路径绘制功能，比如贝叶斯曲线。

### 文本

画布可用于绘制文本。我们简要学习一下。首先再次下载一份新的画布模板（1\_canvas\_template.htm），我们用它来绘制新的示例。

以下两个函数用于绘制文本：

* `fillText()` ：绘制有填充色的文本。
* `strokeText()`：绘制文本外边框（描边）。

这两个函数有三个基本的参数：需要绘制的文字、**文本框**（顾名思义，围绕着需要绘制文字的方框）左上顶点的 X、Y 坐标。

还有一系列帮助控制文本渲染的属性：比如用于指定字体族、字号的 `font`，它的值和语法与 CSS 的 `font` 属性一致。

在 JS 代码底部添加以下内容：

```js
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
ctx.font = "36px arial";
ctx.strokeText("Canvas text", 50, 50);

ctx.fillStyle = "red";
ctx.font = "48px georgia";
ctx.fillText("Canvas text", 50, 150);
```

将绘制两行文字，一行描边文字一行填充颜色的文字。示例最终如下所示：

**备注：** 完整代码可到 GitHub 下载：4\_canvas\_text.html.

可以自己尝试一下。访问 绘制文本 获得关于画布文本选项的更多信息。

### 在画布上绘制图片

可在画布上渲染外部图片，简单图片文件、视频帧、其他画布内容都可以。这里我们只考虑简单图片文件的情况：

1. 同上，下载画布模板（1\_canvas\_template.html）以绘制新的示例。这里还需要在同一目录下保存一个示例图片文件：firefox.png。
 `drawImage()` 方法可将图片绘制在画布上。最简单的版本需要三个参数：需要渲染的图片、图片左上角的 X、Y 坐标。
2. 将图片源嵌入画布中，代码如下：

```js
var image = new Image();
image.src = "firefox.png";
```

这里使用 `Image()` 构造器创建了一个新的 `HTMLImageElement` 对象。返回对象的类型与非空 `<img>` 元素的引用是一致的。然后将它的 `src` 属性设置为 Firefox 的图标。此时浏览器将开始载入这张图片。
3. 这次我们尝试用 `drawImage()` 函数来嵌入图片，应确保图片先载入完毕，否则运行会出错。可以通过 `onload` 事件处理器来达成，该函数只在图片调用完毕后才会调用。在上文代码末尾添加以下内容：

```js
image.onload = function () {
  ctx.drawImage(image, 50, 50);
};
```

保存刷新，可以看到图片成功嵌入画布中。
4. 还有更多方式。如果仅需要显示图片的某一部分，或者需要改变尺寸，该怎么做呢？复杂版本的 `drawImage()` 可解决这两个问题。请更新 `ctx.drawImage()` 一行代码为：

```js
ctx.drawImage(image, 20, 20, 185, 175, 50, 50, 185, 175);
```

    * 第一个参数不变，为图片引用。
    * 参数 2、3 表示裁切部分左上顶点的坐标，参考原点为原图片本身左上角的坐标。原图片在该坐标左、上的部分均不会绘制出来。
    * 参数 4、5 表示裁切部分的长、宽。
    * 参数 6、7 表示裁切部分左上顶点在画布中的位置坐标，参考原点为画布左上顶点。
    * 参数 8、9 表示裁切部分在画布中绘制的长、宽。本例中绘制时与裁切时面积相同，你也可以定制绘制的尺寸。

最终结果如下所示：

**备注：** 完整代码可到 GitHub 下载：5\_canvas\_images.html.

循环和动画
-----

目前我们学习了关于 2D 画布一些非常基础的用法，但是不学习动画你就无法体会画布的强大。画布是提供可编程图形的。如果你的作品不需要改变，那么你就只能永远面对那些静态图片了。

### 创建一个循环

在画布中使用循环是件有趣的事，你可以在 `for` 循环中运行画布命令，和其他 JS 代码一样。

我们来创建一个简单的示例。

1. 继续复制一份画布模板（1\_canvas\_template.html）在代码编辑器中打开。
2. 在 JS 代码末尾添加以下一行。这将创建一个新方法——`translate()`，可用于移动画布的原点。

```js
ctx.translate(width / 2, height / 2);
```

这会使原点 (0, 0) 从画布左上顶点移动至画布正中心。这个功能在许多场合非常实用，就像本示例，我们的绘制操作都是围绕着画布的中心点展开的。
3. 在 JS 代码末尾添加以下内容：

```js
function degToRad(degrees) {
  return (degrees \* Math.PI) / 180;
}

function rand(min, max) {
  return Math.floor(Math.random() \* (max - min + 1)) + min;
}

var length = 250;
var moveOffset = 20;

for (var i = 0; i < length; i++) {}
```

这里我们实现了一个与上文三角形示例中相同的 `degToRad()` 函数、一个返回给定范围内随机数 `rand()` 函数、`length` 和 `moveOffset` 变量（见下文），以及一个空的 `for` 循环。
4. 此处的理念是利用 `for` 循环在画布上循环迭代绘制好玩儿的内容。请将以下代码添加进 `for` 循环中：

```js
ctx.fillStyle =
  "rgba(" + (255 - length) + ", 0, " + (255 - length) + ", 0.9)";
ctx.beginPath();
ctx.moveTo(moveOffset, moveOffset);
ctx.lineTo(moveOffset + length, moveOffset);
var triHeight = (length / 2) \* Math.tan(degToRad(60));
ctx.lineTo(moveOffset + length / 2, moveOffset + triHeight);
ctx.lineTo(moveOffset, moveOffset);
ctx.fill();

length--;
moveOffset += 0.7;
ctx.rotate(degToRad(5));
```

在每次迭代中：
    * 设置 `fillStyle` 为略透明的紫色渐变色。渐变由每次迭代时 `length` 值的改变实现。随着循环的运行， `length` 值逐渐变小，从而使连续的三角形颜色逐渐变亮。
    * 开始路径。
    * 将钢笔移动至坐标 `(moveOffset, moveOffset)`；该变量定义了每次要绘制新三角形时需要移动的距离。
    * 画一条直线，终点坐标为 `(moveOffset+length, moveOffset)`。即一条长度为 `length` 与 X 轴平行的线。
    * 计算三角形的高，方法同上。
    * 向三角形底部顶点方向绘制一条直线，然后向三角形的起始点绘制一条直线。
    * 调用 `fill()` 为三角形填充颜色。
    * 更新次序变量，准备绘制下一个三角形。`length` 的值减一，使三角形每次迭代都变小一些；小幅增加 `moveOffset` 的值，使得下一个三角形略微错位；用一个新函数 `rotate()` 来旋转整块画布，在绘制下个三角形前画布旋转 5°。

好了，最终结果如下：

现在，你可以尝试这个示例，可以加一些创新哦。比如：

* 把三角形换成矩形、弧，甚至内嵌的图片。
* 修改 `length` 和 `moveOffset` 的值。
* 我们引入了 `rand()` 函数但是没有使用，你可以试着用它引入一些随机数。

**备注：** 完整代码可到 GitHub 下载：6\_canvas\_for\_loop.html.

### 动画

上文的循环示例很有趣，其实在重度画布应用（比如游戏或实时可视化）中恒定循环是至关重要的支持组件。如果期望画布显示的内容像一部电影，屏幕最好能够以 60 帧每秒的刷新率实时更新，这样人眼看到的动作才更真实、更平滑。

一些 JavaScript 函数可以让函数在一秒内重复运行多次，这里最适合的就是 `window.requestAnimationFrame()`。它只取一个参数，即每帧要运行的函数名。下一次浏览器准备好更新屏幕时，将会调用你的函数。如果你的函数向动画中绘制了更新内容，则在函数结束前再次调用 `requestAnimationFrame()`，动画循环得以保留。只有在停止调用 `requestAnimationFrame()` 时，或 `requestAnimationFrame()` 调用后、帧调用前调用了 `window.cancelAnimationFrame()` 时，循环才会停止。

\*\*注：\*\*动画结束后在主代码中调用 `cancelAnimationFrame()` 是良好习惯，可以确保不再有等待运行的更新。

浏览器自行处理诸如“使动画匀速运行”、“避免在不可见的内容浪费资源”等复杂细节问题。

我们简单回顾一下“弹球”示例（在线运行 或查看 源代码），来探究一下原理。以下是让弹球持续运行的循环代码：

```js
function loop() {
  ctx.fillStyle = "rgba(0, 0, 0, 0.25)";
  ctx.fillRect(0, 0, width, height);

  while (balls.length < 25) {
    var ball = new Ball();
    balls.push(ball);
  }

  for (i = 0; i < balls.length; i++) {
    balls[i].draw();
    balls[i].update();
    balls[i].collisionDetect();
  }

  requestAnimationFrame(loop);
}

loop();
```

我们在代码底部运行了一次 `loop()` 函数，它启动了整个循环，绘制了第一帧动画。接着 `loop()` 函数接管了`requestAnimationFrame(loop)` 的调用工作，即运行下一帧、再下一帧……的动画。

请注意每一帧我们都整体清除画布并重新渲染所有内容。（每帧创建一个新球（25 个封顶），然后绘制每个球，更新它们的位置，检查是否撞到了其他球。）向画布中绘制的新图形不能像 DOM 元素那样单独操作。你无法再画布中单独操作某一个球，因为只要绘制完毕了，它就是画布的一部分，而不是一个单独的球。你需要擦除再重画，可以将整帧擦除再重画整个画面，也可通过编程选择最小的部分进行擦除和重画。

优化图形动画是另一个编程主题，需要好多奇技淫巧。这超出我们的讨论范围啦。

一般地，在画布上制作动画需要以下步骤：

1. 清除画布内容（可用 `fillRect()` 或 `clearRect()`）。
2. （在需要时）用 `save()` 保存状态。（在进行下一步前保存所更新的设置，一般在复杂环境中用到）
3. 绘制动画图形。
4. 使用 `restore()` 恢复第 2 步中保存的状态。
5. 调用 `requestAnimationFrame()` 准备下一帧动画。

>
> **备注** `save()` 和 `restore()` 这里暂不展开，可以访问 变形 教程（及后续内容）来获取详细信息。
>
>
>

### 一个简单的人物动画

现在我们来创建一个简单的动画，我们找来一个复古的电脑游戏的主角制作一个在屏幕上行走的动画。

1. 继续复制一份画布模板（1\_canvas\_template.html）在代码编辑器中打开。下载 walk-right.png 并放在同一文件夹。
2. 在 JS 代码末尾添加下面一行，再次将画布的原点设置为中心点。

```js
ctx.translate(width / 2, height / 2);
```

3. 创建一个新的 `HTMLImageElement` 对象，把它的 `src` 设置为所需图片，添加一个 `onload` 事件处理器，使 `draw()` 函数在图片载入后触发。

```js
var image = new Image();
image.src = "walk-right.png";
image.onload = draw;
```

4. 添加一些变量，来追踪精灵图在屏幕上的位置，以及当前需要显示的精灵图的序号。

```js
var sprite = 0;
var posX = 0;
```

我们来解释一下“精灵图序列”（我们借鉴了麦克托马斯的 使用 CSS 动画创建人物行走的精灵图）。图片如下：

 ![](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics/walk-right.png)
 图中包含六个精灵，它们组成了一趟完整的行走序列。每个精灵的尺寸为 102 × 148 像素。为了整齐的显示一个精灵，可以通过 `drawImage()` 来从序列中裁切出单独的精灵并隐藏其他部分，就像上文中操作 Firefox 图标的方法。切片的 X 坐标应为 102 的倍数，Y 坐标恒为 0。切片尺寸恒为 102 × 148 像素。
5. 在代码末尾添加一个空的 `draw()` 函数，用来添加一些代码：

```js
function draw() {}
```

6. 本节剩余部分都在这个 `draw()` 中展开。首先，添加以下代码，清除画布，准备绘制新的帧。注意由于我们刚才将原点设置为 `width/2, height/2`，这里需要将矩形左上顶点的坐标设置为 `-(width/2), -(height/2)`。

```js
ctx.fillRect(-(width / 2), -(height / 2), width, height);
```

7. 下一步，我们使用 `drawImage()`（9 参数版本）来绘制图形，添加以下代码：

```js
ctx.drawImage(image, sprite \* 102, 0, 102, 148, 0 + posX, -74, 102, 148);
```

如你所见：
    * `image` 指定需要嵌入的图片。
    * 参数 2、3 指定切片左上顶点在原图的位置坐标，X 值为 `sprite`（精灵序列 0 - 5）乘 102，Y 值恒为 0。
    * 参数 4、5 指定切片尺寸：102 × 148 像素。
    * 参数 6、7 指定切片在画布绘制区域的坐上顶点坐标。X 坐标位置为 0 + `posX`，意味着我们可以通过修改 `posX` 的值来修改绘制的位置。
    * 参数 8、9 指定图片在画布中的尺寸。这里需要图片保持原始尺寸，因此我们指定宽、高值为 102、148。
8. 现在，我们在每帧绘制完毕（部分完毕）后修改 `sprite` 的值。在 `draw()` 函数底部添加以下内容：

```js
if (posX % 13 === 0) {
  if (sprite === 5) {
    sprite = 0;
  } else {
    sprite++;
  }
}
```

将整个功能块放置在 `if (posX % 13 === 0) { ... }` 内。用“模（`%`）运算符”（即 求余运算符）来检测 `posX` 是否可以被 13 整除。如果整除，则通过增加 `sprite` 的值转至下一个精灵（到 5 号精灵时归零）。这实际上意味着每隔 13 帧才更新一次精灵，每秒大约更新 5 帧（`requestAnimationFrame()` 每秒最多调用 60 帧）。我们故意放慢了帧率，因为精灵图只有六个，且如果每秒显示 60 帧的话，这个角色就会快到起飞。
 外部程序块中用一个 `if ... else` 语句来检测 `sprite` 的值是否为 5（精灵序号在 0 - 5 间循环，因此 5 代表最后一个精灵）。如果最后一个精灵已经显示，就把 `sprite` 重置为 0，否则加 1。
9. 下一步要算出每帧 `posX` 的值，在上文代码末尾添加以下内容：

```js
if (posX > width / 2) {
  newStartPos = -(width / 2 + 102);
  posX = Math.ceil(newStartPos / 13) \* 13;
  console.log(posX);
} else {
  posX += 2;
}
```

用另一个 `if ... else` 来检测 `posX` 的值是否超出了 `width/2`，那意味着角色走到了屏幕右侧边缘。如果这样就计算出一个让角色出现在屏幕左侧边缘的 X 坐标，然后将 `posX` 设置为最接近这个数的 13 的倍数。这里必须限定 13 的倍数这个条件，这是因为 `posX` 不可能是 13 的倍数，若不限定的话上一段代码就不会运行了。
 如果角色没有走到屏幕边缘，只需为 `posX` 加 2。这将让他在下次绘制时更靠右些。
10. 最后，通过在 `draw()` 函数末尾添加 `requestAnimationFrame()` 调用以实现动画的循环。

```js
window.requestAnimationFrame(draw);
```

成功了！最终效果如下所示：

**备注：** 完整代码可到 GitHub 下载：7\_canvas\_walking\_animation.html.

### 简单的绘图应用

下面来演示一个简单的绘图应用，作为最后一个绘画示例，它将向你展示动画循环如果与用户输入（本例中为鼠标移动）结合起来。我们不会带你一步一步来实现本示例，只对代码中最有趣的部分进行探究。

示例代码可到 GitHub 下载：8\_canvas\_drawing\_app.html，也可在线试玩：

下面我们就来看看代码的精华部分。首先，用 `curX`、`curY` 和 `pressed` 这三个变量来跟踪鼠标的 X、Y 坐标和点击状态。当鼠标移动时，触发一个函数作为 `onmousemove` 事件处理器，其应捕获当前的 X 和 Y 值。再用 `onmousedown` 和 `onmouseup` 事件处理器来修改鼠标键按下时 `pressed` 的值（按下为 `true`，释放为 `false`）。

```js
var curX;
var curY;
var pressed = false;

document.onmousemove = function (e) {
  curX = window.Event
    ? e.pageX
    : e.clientX +
      (document.documentElement.scrollLeft
        ? document.documentElement.scrollLeft
        : document.body.scrollLeft);
  curY = window.Event
    ? e.pageY
    : e.clientY +
      (document.documentElement.scrollTop
        ? document.documentElement.scrollTop
        : document.body.scrollTop);
};

canvas.onmousedown = function () {
  pressed = true;
};

canvas.onmouseup = function () {
  pressed = false;
};
```

在按下“Clear canvas”（清除画布）按钮时，我们运行一个简单的函数来清除整个画布的内容至纯黑色，和刚才的方法一致：

```js
clearBtn.onclick = function () {
  ctx.fillStyle = "rgb(0, 0, 0)";
  ctx.fillRect(0, 0, width, height);
};
```

这次的绘图循环非常简单，如果 `pressed` 为 `true`，则绘制一个圆，该圆以颜色选择器中设定的颜色为背景，以滑动选择器设定的数值为半径。

```js
function draw() {
  if (pressed) {
    ctx.fillStyle = colorPicker.value;
    ctx.beginPath();
    ctx.arc(
      curX,
      curY - 85,
      sizePicker.value,
      degToRad(0),
      degToRad(360),
      false,
    );
    ctx.fill();
  }

  requestAnimationFrame(draw);
}

draw();
```

**备注：** `range` 和 `color` 两个 `<input>` 的类型有良好的跨浏览器支持，但 Safari 暂不支持 `color`，IE 10 以下版本两者均不支持。如果你的浏览器不支持这些输入类型，它们将降格为简单文字输入区域，可以直接输入合法的数字来表示半径和颜色的值。

WebGL
-----

2D 内容告一段落，现在简单了解一下 3D 画布。3D 画布内容可通过的 WebGL API 实现，尽管它和 2D canvas API 都可在 `<canvas>` 元素上进行渲染，但两者是彼此独立的。

WebGL 基于 OpenGL 图形编程语言实现，可直接与 GPU 通信，基于此，编写纯 WebGL 代码与常规的 JavaScript 不尽相同，更像 C++ 那样的底层语言，更加复杂，但无比强大。

### 使用库

由于 3D 绘图的复杂性，大多数人写代码时会使用第三方 JavaScript 库（比如 Three.js、PlayCanvas (en-US) 或 Babylon.js (en-US)）。大多数库的原理都基本类似，提供创建基本的、自定义性状的功能、视图定位摄影和光效、表面纹理覆盖，等等。库负责 与 WebGL 通信，你只需完成更高阶工作。

接触任何一个库都意味着要学一套全新的 API（这里是第三方的版本），但与纯 WebGL 编程都大同小异。

### 创建魔方

我们来看一个简单的示例，用一套 WebGL 库（这里我们选择 Three.js，最流行的 3D 绘图库之一）来创建我们在本文开头看到的旋转魔方。

1. 首先，下载 index.html、metal003.png 并保存在同一个文件夹。图片将用于魔方的表面纹理。
2. 然后，继续在同一个文件夹内创建 `main.js` 文件。
3. 在编辑器中打开 `index.html` 可以看到其中有两个 `<script>` 元素，第一个将 `three.min.js` 嵌入页面，第二个将我们的 `main.js` 嵌入页面。需要自行 下载 three.min.js 库 并保存到同一个文件夹中。
4. 将 `three.js` 嵌入页面后，就可以在 `main.js` 中添加新的代码对其加以应用了。请添加下面一行：

```js
var scene = new THREE.Scene();
```

`Scene()` 构造器创建一个新的场景，表示即将显示的整个 3D 世界。
5. 下一步，我们需要一部**摄影机**来看到整个场景。在 3D 绘图语境中，摄影机表示观察者在世界里的位置，可通过下面代码创建一部摄影机：

```js
var camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000,
);
camera.position.z = 5;
```

`PerspectiveCamera()` 构造器有四个参数：
    * 观察区域：镜头视角大小，用角度表示。
    * 屏幕宽高比：一般情况下，宽高比等于屏幕的宽比上屏幕的高。使用其他的值会使场景扭曲（也许正是你需要的，但一般都不是）。
    * 近裁切面：停止渲染前对象离摄影机的最近距离。设想一下，举起一个手指，逐渐移近双眼，某个点后就在也看不到这根手指了。
    * 远裁切面：停止渲染前离摄像机最远的对象的距离。将摄像机的位置设定为距 Z 轴 5 个距离单位的位置。与 CSS 类似，在屏幕之外你（观察者）的位置。
6. 第三个重要参数是渲染器。我们用它来渲染给定的场景，可通过给定位值得摄影机观察。现在我们使用 `WebGLRenderer()` 构造器创建一个渲染器供稍后使用。添加以下代码：

```js
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
```

第一行创建一个新的渲染器，第二行设定渲染器在当前摄影机视角下的尺寸，第三行将渲染好的 `<canvas>` 对象加入 HTML 的 `<body>` 中。现在渲染器绘制的内容将在窗口中显示出来。
7. 下一步，在画布中创建魔方。把以下代码添加到 JS 文件中：

```js
var cube;

var loader = new THREE.TextureLoader();

loader.load("metal003.png", function (texture) {
  texture.wrapS = THREE.RepeatWrapping;
  texture.wrapT = THREE.RepeatWrapping;
  texture.repeat.set(2, 2);

  var geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4);
  var material = new THREE.MeshLambertMaterial({
    map: texture,
    shading: THREE.FlatShading,
  });
  cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  draw();
});
```

内容很多，我们来剥丝抽茧：
    * 首先，创建一个全局变量 `cube`，这样就可以在代码任意位置访问我们的魔方。
    * 然后，创建一个 `TextureLoader` 对象，并调用 `load()`。这里 `load()` 包含两个参数（其他情况可以有更多参数）：需要调用的纹理图（PNG 文件）和纹理加载成功后调用的函数。
    * 函数内部，我们用 `texture` 对象的属性指明我们要在魔方的每个面渲染 2 × 2 的图片，然后创建一个 `BoxGeometry` 对象和一个 `MeshLambertMaterial` 对象，将两者作为 `Mesh` 的参数来创建我们的魔方。 `Mesh` 一般就需要两个参数：一个几何（形状）和一个素材（形状表面外观）。
    * 最后，将魔方添加进场景中，调用我们的 `draw()` 函数开始动画。
8. 定义 `draw()` 函数前，我们需要先为场景打光，以照亮场景中的物体。请添加以下代码：

```js
var light = new THREE.AmbientLight("rgb(255, 255, 255)"); // soft white light
scene.add(light);

var spotLight = new THREE.SpotLight("rgb(255, 255, 255)");
spotLight.position.set(100, 1000, 1000);
spotLight.castShadow = true;
scene.add(spotLight);
```

`AmbientLight` 对象是可以轻度照亮整个场景的柔光，就像户外的阳光。而 `SpotLight` 对象是直射的硬光，就像闪光灯和手电筒（或者它的英文字面意思——聚光灯）。
9. 最后，在代码末尾添加我们的 `draw()` 函数：

```js
function draw() {
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);

  requestAnimationFrame(draw);
}
```

这段代码很直观，每一帧我们都沿 X 轴 和 Y 轴将魔方轻微转动，然后按摄像机视角渲染场景，最后调用 `requestAnimationFrame()` 来准备下一帧。

回顾一下最终效果：

你可以 到 Github 下载最终代码。

**备注：** 在我们的 GitHub repo 还有另一个趣味 3D 魔方示例——Three.js Video Cube（在线查看）。其中通过 `getUserMedia()` 来从电脑摄像头获取一段视频，将其投影到魔方上作为纹理。

小结
--

此刻你以经了解了一些 Canvas 和 WebGL 图形编程的基本理念和简单应用，你一定产生了不少创作灵感，玩得开心！

参见
--

本文我们只涉及到画布最为基本的内容，以下内容帮你探索更多：

* Canvas 教程：一个详尽的教程系列，更细致深入地讲解了 2D 画布所需的知识。必读。
* WebGL 教程：纯 WebGL 编程教程系列。
* 用 Three.js 创建一个简单的示例：Three.js 基础教程。我们还提供 PlayCanvas (en-US) 和 Babylon.js (en-US) 的基础教程。
* 游戏开发：MDN web 游戏开发目录页。提供与 2D、3D 画布相关的实用教程和技术，可参考“技术”和“教程”菜单项。
示例
--

* Violent theramin：用 Web 音频 API 创建声音，用画布显示漂亮的视觉效果以配合音乐。
* Voice change-o-matic：用画布为 Web 音频 API 产生的音效提供实时的视觉效果。
* 上一页
* Overview: Client-side web APIs
* 下一页


从服务器获取数据 - 学习 Web 开发

从服务器获取数据
========

* 上一页
* Overview: Client-side web APIs
* 下一页

现代网站和应用中另一个常见的任务是从服务端获取个别数据来更新部分网页而不用加载整个页面。这看起来是小细节却对网站性能和行为产生巨大的影响。所以我们将在这篇文章介绍概念和技术使它成为可能，例如：Fetch API。

|  |  |
| --- | --- |
| 预备条件： |
 JavaScript 基础（参见
 JavaScript 第一步、创建代码块、JavaScript 对象）、客户端 API 基础 |
| 目标： | 了解如何从服务器获取数据并使用它来更新网页内容。 |

这里有什么问题？
--------

网页由 HTML 页面和（通常也有）各种其他文件组成，例如样式表、脚本和图像。Web 加载页面的基本模型为：你的浏览器向服务器发起一个或多个 HTTP 请求以获取显示网页所需的文件，然后服务器响应请求的文件。如果你访问另一个页面，浏览器会请求新的文件，服务器则会响应这些请求。

![传统页面加载流程](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data/traditional-loading.svg)

这个模型适用于许多站点。但请考虑一个几乎是以数据驱动的网站。例如，温哥华图书馆这样的图书馆网站。你可以将此类网站视作数据库的用户界面。它可能会允许你搜索特定类型的书籍，或者根据你之前借过的书籍向你推荐你可能喜欢的书籍。在你这样操作时，它需要使用新的书集来更新用于显示的页面。但请注意，大部分的页面内容（包括页眉、侧边栏和页脚等元素）将保持不变。

传统模型的问题在于我们必须获取并加载整个页面，即使我们只需要更新页面的一部分也是如此。这是低效的，而且会带来糟糕的用户体验。

因此，与传统模型不同，许多网站使用 JavaScript API 从服务器请求数据，并在不重新加载页面的情况下更新页面。因此，当用户搜索新产品时，浏览器仅请求更新页面所需的数据——例如要显示的新书集。

![使用 fetch 来更新页面](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data/fetch-update.svg)

这里主要的 API 是 Fetch API。它允许页面中运行的 JavaScript 向服务器发起 HTTP 请求来获取特定的资源。当服务器提供了这些资源时，JavaScript 可以使用这些数据更新页面（通常是通过使用 DOM 操作 API）。请求的数据通常是 JSON，这是一种很好的传输结构化的格式，但也可以是 HTML 或纯文本。

这是数据驱动网站（如 Amazon、YouTube、eBay 等）的常见模式。使用此模型，使得：

* 页面更新更加迅速，你不必等待页面刷新，这意味这网站的体验更加流畅、响应更加迅速。
* 每次更新时下载的数据更少，这意味着浪费的带宽更少。这在使用宽带连接的台式机上可能不是什么问题，但在移动设备或没有高速互联网连接的国家/地区则是一个主要问题。

**备注：** 在早期，这种通用技术被称为异步的 JavaScript 与 XML 技术（Ajax），因为倾向于请求 XML 数据。但现在通常不是这种情况（你更有可能请求 JSON），但结果依然相同，并通常依旧使用术语“Ajax”来描述该技术。

为了进一步加快速度，某些网站还会在首次请求时将资源和数据存储在用户的计算机上，这意味这在后续的访问中，会使用这些内容的本地版本，而不是在每次重新加载页面时都下载新的副本。内容仅在更新后才会从服务器重新加载。

Fetch API
---------

让我们看几个 Fetch API 的示例。

### 获取文本内容

对于此示例，我们将从几个不同的文本文件中请求数据，并使用它们来填充内容区域。

这一系列文件将假定为我们的数据库；在实际的应用程序中，我们更有可能使用服务端语言（如 PHP、Python 或 Node）从数据库中获取数据。但在这里，我们希望保持简单并专注于客户端部分。

要开始此示例，请在计算机的新的目录中创建 fetch-start.html 和四个文本文件（verse1.txt、verse2.txt、verse3.txt 和 verse4.txt）的本地拷贝。在这个示例中，当你在下拉菜单中选择一个选项时，我们会获取不同的诗歌（你可能会认识）。

在 `<script>` 元素中，添加以下代码。这会存储对 `<select>` 和 `<pre>` 元素的引用，并对 `<select>` 元素添加一个事件监听器，以便在用户选择一个新的值时，新值将作为参数传递给名为 `updateDisplay()` 的函数。

```js
const verseChoose = document.querySelector("select");
const poemDisplay = document.querySelector("pre");

verseChoose.addEventListener("change", () => {
  const verse = verseChoose.value;
  updateDisplay(verse);
});
```

下面让我们定义 `updateDisplay()` 函数。首先，将以下内容放在你之前的代码块下方——这是该函数的空壳。

```js
function updateDisplay(verse) {}
```

让我们将通过构造一个指向我们要加载的文本文件的相对 URL 来开始编写函数，因为我们稍后需要它。任何时候 `<select>` 元素的值都与所选的 `<option>` 内的文本相同（除非在值属性中指定了不同的值）——例如“Verse 1”。相应的诗歌文本文件是“verse1.txt”，并与 HTML 文件位于同一目录中，因此只需要文件名即可。

但是，web 服务器往往是区分大小写的，且文件名没有空格。要将“Verse 1”转换为“verse1.txt”，我们需要将 `V` 转换为小写、删除空格，并在末尾添加“.txt”。这可以通过 `replace()`、`toLowerCase()` 和模板字符串来完成。在 `updateDisplay()` 函数中添加以下代码：

```js
verse = verse.replace(" ", "").toLowerCase();
const url = `${verse}.txt`;
```

最后，我们可以开始使用 Fetch API 了：

```js
// 调用 `fetch()`，传入 URL。
fetch(url)
  // fetch() 返回一个 promise。当我们从服务器收到响应时，
  // 会使用该响应调用 promise 的 `then()` 处理器。
  .then((response) => {
    // 如果请求没有成功，我们的处理器会抛出错误。
    if (!response.ok) {
      throw new Error(`HTTP 错误：${response.status}`);
    }
    // 否则（如果请求成功），我们的处理器通过调用
    // response.text() 以获取文本形式的响应，
    // 并立即返回 `response.text()` 返回的 promise。
    return response.text();
  })
  // 若成功调用 response.text()，会使用返回的文本来调用 `then()` 处理器，
  // 然后我们将其拷贝到 `poemDisplay` 框中。
  .then((text) => (poemDisplay.textContent = text))
  // 捕获可能出现的任何错误，
  // 并在 `poemDisplay` 框中显示一条消息。
  .catch((error) => (poemDisplay.textContent = `获取诗歌失败：${error}`));
```

这里有很多内容需要展开。

首先，Fetch API 的入口点是一个名为 `fetch()` 的全局函数，它以 URL 为参数（其使用另一个可选参数来进行自定义设置，但我们在这里不使用它）。

接下来，`fetch()` 是一个异步 API，会返回一个 `Promise`。如果你不知道什么是 Promise，请参阅异步 JavaScript 章节，然后再回到这里。你会发现那篇文章也介绍了 `fetch()` API！

因为 `fetch()` 返回一个 promise，所以我们将一个函数传递给它返回的 promise 的 `then()` 方法。此方法会在 HTTP 请求收到服务器的响应时被调用。在它的处理器中，我们检查请求是否成功，并在请求失败时抛出一个错误。否则，我们调用 `response.text()` 以获取文本形式的响应正文。

`response.text()` *也*是异步的，所以我们返回它返回的 promise，并向新的 promise 的 `then()` 方法传递一个函数。这个函数会在响应文本可用时被调用，在这个函数中，我们会使用该文本更新 `<pre>` 块。

最后，我们在最后链式调用 `catch()` 处理器，以捕获我们调用的异步函数或其他处理器中抛出的任何错误。

此示例的一个问题是，它在第一次加载时不会显示任何诗歌。要解决此问题，请在代码的最后（`</script>` 结束标签之前）添加以下两行代码，以默认加载第一首诗歌，并确保 `<select>` 元素始终显示正确的值：

```js
updateDisplay("Verse 1");
verseChoose.value = "Verse 1";
```

### 在服务端运行示例

如果只是使用本地文件运行示例，现代浏览器将不会执行 HTTP 请求。这是因为安全限制（更多关于 web 安全性的限制，请参阅网站安全）。

为了解决这个问题，我们需要通过在本地 web 服务器上运行它来测试这个示例。要了解如何实现这一点，请阅读我们的设置本地测试服务器指南。

### 罐头商店

在这个示例中，我们创建了一个名为 The Can Store 的示例站点——它是一个虚构的超市，仅销售罐头食品。你可以在 GitHub 上找到这个可用示例，并查看源代码 。

![一个虚构的电子商务网站，在左侧边栏中显示搜索的选项，在右侧边栏中显示产品的搜索结果。](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data/can-store.png)

默认情况下，站点会显示所有产品，但你可以使用左侧边栏中的表单控件按类别或搜索词或两者进行筛选。

有很多复杂的代码按类别和搜索词来过滤产品、操作字符串以便数据在 UI 中正确显示，等等。我们不会在本文中讨论所有的这些，但是你可以在代码中找到大量的注释（见 can-script.js）。

但是，我们会解释 Fetch 代码的含义。

第一个使用 Fetch 的代码块可以在 JavaScript 的开头找到：

```js
fetch("products.json")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP 错误：${response.status}`);
    }
    return response.json();
  })
  .then((json) => initialize(json))
  .catch((err) => console.error(`Fetch 错误：${err.message}`));
```

`fetch()` 函数返回一个 promise。如果成功完成，第一个 `.then()` 代码块中的函数包含通过网络返回的响应（`response`）。

在此函数中，我们：

* 检查服务器是否返回错误（例如 `404 Not Found`）。如果返回了错误，我们抛出一个错误。
* 对响应调用 `json()`。它会将数据检索为 JSON 对象。然后我们返回 `response.json()` 返回的 promise。

接着，我们将一个函数传递给返回的 promise 的 `then()` 方法。该函数会被传入一个包含 JSON 格式的响应数据的对象，我们将这个对象传递给 `initialize()` 函数。该函数会开始在用户界面中显示所有产品的过程。

为了处理错误，我们将一个 `.catch()` 代码块串联到 promise 链的末尾。如果 promise 由于某种原因失败了，它就会被运行。在该代码块中，我们包含一个接收 `err` 对象的函数。该 `err` 对象可用于报告已发生错误的性质，在本例中，我们仅使用了 `console.log()`。

然而，一个完整的网站会通过在用户屏幕上显示一条消息，可能还会提供这种情况的补救选项来更优雅地处理此类错误，但我们在这里不需要比 `console.error()` 更复杂的东西。

你可以自己测试失败的情况：

1. 创建示例文件的本地副本。
2. 通过 web 服务器运行代码（如上所述，在服务端运行示例）。
3. 修改要获取的文件的路径，比如“produc.json”（确保你拼写的是错误的）。
4. 现在在你的浏览器上加载索引文件（通过 `localhost:8000`）然后查看你浏览器的开发者控制台。你将看到一条类似于“Fetch 错误：HTTP 错误：404”的消息。

第二个 Fetch 块可以在 `fetchBlob()` 函数中找到：

```js
fetch(url)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP 错误：${response.status}`);
    }
    return response.blob();
  })
  .then((blob) => showProduct(blob, product))
  .catch((err) => console.error(`Fetch 错误：${err.message}`));
```

它的工作原理和前一个差不多，除了我们放弃 `json()` 而使用 `blob()`——在本例中，我们希望以图像文件的形式返回响应，为此使用的数据格式是 Blob——这个词是“二进制大对象”的缩写，基本上可以用来表示大型类文件对象——比如图像或视频文件。

一旦我们成功地接收到我们的 blob，我们将其传入到用于显示图像的 `showProduct()` 函数中。

XMLHttpRequest API
------------------

有时，尤其是在旧的代码中，你会看到另一个名为 `XMLHttpRequest`（经常会被简写为“XHR”）的 API，它也用于发送 HTTP 请求。其早于 Fetch API，而且是第一个广泛用于实现 AJAX 的 API。如果可以，我们建议你使用 Fetch：它是一个更简单的 API，而且比 `XMLHttpRequest` 的特性更多。我们不再详细介绍使用 `XMLHttpRequest` 的示例，但我们将向你展示罐头商店示例的第一个请求的 `XMLHttpRequest` 版本：

```js
const request = new XMLHttpRequest();

try {
  request.open("GET", "products.json");

  request.responseType = "json";

  request.addEventListener("load", () => initialize(request.response));
  request.addEventListener("error", () => console.error("XHR error"));

  request.send();
} catch (error) {
  console.error(`XHR error ${request.status}`);
}
```

这里有五个阶段：

1. 创建一个新的 `XMLHttpRequest` 对象。
2. 调用它的 `open()` 以进行初始化。
3. 为其添加 `load` 事件的事件监听器，其会在响应加载完成时触发。在监听器中，我们调用 `initialize()` 函数。
4. 为其添加 `error` 事件的事件监听器，其会在请求失败时触发。
5. 发送请求。

我们还必须将整个事件包装在 try...catch 块中，以便处理 `open()` 或 `send()` 可能抛出的错误。

希望你会觉得 Fetch API 是对此的改进。特别是，在看完我们对两者的错误处理之后。

概述
--

本文介绍了如何使用 Fetch 从服务器获取数据。

参见
--

虽然本文中讨论了许多不同的主题，但是这些主题仅是触及了表面。有关这些主题的更多详细信息，请尝试阅读以下文章：

* 使用 Fetch
* Promise
* 使用 JSON 数据
* HTTP 概述
* 服务器端网页编程
* 上一页
* Overview: Client-side web APIs
* 下一页


客户端存储 - 学习 Web 开发

客户端存储
=====

* 上一页
* Overview: Client-side web APIs

现代 web 浏览器提供了很多在用户电脑 web 客户端存放数据的方法 — 只要用户的允许 — 可以在它需要的时候被重新获得。这样能让你存留的数据长时间保存，保存站点和文档在离线情况下使用，保留你对其站点的个性化配置等等。本篇文章只解释它们工作的一些很基础的部分。

|  |  |
| --- | --- |
| Prerequisites: |
 JavaScript 基础 (查看
 第一步,
 构建的块,
 JavaScript 对象),
 基础的客户端 API |
| Objective: | 学习如何使用客户端存储 API 来存储应用数据。 |

客户端存储？
------

在其他的 MDN 学习中我们已经讨论过 静态网站（static sites）和动态网站（ dynamic sites）的区别。大多数现代的 web 站点是动态的— 它们在服务端使用各种类型的数据库来存储数据 (服务端存储), 之后通过运行服务端（ server-side）代码来重新获取需要的数据，把其数据插入到静态页面的模板中，并且生成出 HTML 渲染到用户浏览上。

客户端存储以相同的原理工作，但是在使用上有一些不同。它是由 JavaScript APIs 组成的因此允许你在客户端存储数据 (比如在用户的机器上)，而且可以在需要的时候重新取得需要的数据。这有很多明显的用处，比如：

* 个性化网站偏好（比如显示一个用户选择的窗口小部件，颜色主题，或者字体）。
* 保存之前的站点行为 (比如从先前的 session 中获取购物车中的内容，记住用户是否之前已经登陆过)。
* 本地化保存数据和静态资源可以使一个站点更快（至少让资源变少）的下载，甚至可以在网络失去链接的时候变得暂时可用。
* 保存 web 已经生产的文档可以在离线状态下访问。

通常客户端和服务端存储是结合在一起使用的。例如，你可以从数据库中下载一个由网络游戏或音乐播放器应用程序使用的音乐文件，将它们存储在客户端数据库中，并按需要播放它们。用户只需下载音乐文件一次——在随后的访问中，它们将从数据库中检索。

**备注：** 使用客户端存储 API 可以存储的数据量是有限的（可能是每个 API 单独的和累积的总量）;具体的数量限制取决于浏览器，也可能基于用户设置。有关更多信息，获取更多信息，请参考浏览器存储限制和清理标准。

### 传统方法：cookies

 客户端存储的概念已经存在很长一段时间了。从早期的网络时代开始，网站就使用 cookies 来存储信息，以在网站上提供个性化的用户体验。它们是网络上最早最常用的客户端存储形式。
 因为在那个年代，有许多问题——无论是从技术上的还是用户体验的角度——都是困扰着 cookies 的问题。这些问题非常重要，以至于当第一次访问一个网站时，欧洲居民会收到消息，告诉他们是否会使用 cookies 来存储关于他们的数据，而这是由一项被称为欧盟 Cookie 条例的欧盟法律导致的。

![](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage/cookies-notice.png)

 由于这些原因，我们不会在本文中教你如何使用 cookie。毕竟它过时、存在各种安全问题，而且无法存储复杂数据，而且有更好的、更现代的方法可以在用户的计算机上存储种类更广泛的数据。
 cookie 的唯一优势是它们得到了非常旧的浏览器的支持，所以如果你的项目需要支持已经过时的浏览器（比如 Internet Explorer 8 或更早的浏览器），cookie 可能仍然有用，但是对于大多数项目（很明显不包括本站）来说，你不需要再使用它们了。其实 cookie 也没什么好说的，`document.cookie`一把梭就完事了。

**备注：** 为什么仍然有新创建的站点使用 cookies？这主要是因为开发人员的习惯，使用了仍然使用 cookies 的旧库，以及存在许多 web 站点，提供了过时的参考和培训材料来学习如何存储数据。

### 新流派：Web Storage 和 IndexedDB

现代浏览器有比使用 cookies 更简单、更有效的存储客户端数据的 API。

* Web Storage API 提供了一种非常简单的语法，用于存储和检索较小的、由名称和相应值组成的数据项。当你只需要存储一些简单的数据时，比如用户的名字，用户是否登录，屏幕背景使用了什么颜色等等，这是非常有用的。
* IndexedDB API 为浏览器提供了一个完整的数据库系统来存储复杂的数据。这可以用于存储从完整的用户记录到甚至是复杂的数据类型，如音频或视频文件。

你将在下面了解更多关于这些 API 的信息。

### 未来：Cache API

 一些现代浏览器支持新的 `Cache` API。这个 API 是为存储特定 HTTP 请求的响应文件而设计的，它对于像存储离线网站文件这样的事情非常有用，这样网站就可以在没有网络连接的情况下使用。缓存通常与 Service Worker API 组合使用，尽管不一定非要这么做。
 Cache 和 Service Workers 的使用是一个高级主题，我们不会在本文中详细讨论它，尽管我们将在下面的 离线文件存储 一节中展示一个简单的例子。

存储简单数据 — web storage
--------------------

Web Storage API 非常容易使用 — 你只需存储简单的 键名/键值 对数据 (限制为字符串、数字等类型) 并在需要的时候检索其值。

### 基本语法

让我们来告诉你怎么做：

1. 第一步，访问 GitHub 上的 web storage blank template (在新标签页打开此模板)。
2. 打开你浏览器开发者工具的 JavaScript 控制台。
3. 你所有的 web storage 数据都包含在浏览器内两个类似于对象的结构中： `sessionStorage` 和 `localStorage`。第一种方法，只要浏览器开着，数据就会一直保存 (关闭浏览器时数据会丢失) ，而第二种会一直保存数据，甚至到浏览器关闭又开启后也是这样。我们将在本文中使用第二种方法，因为它通常更有用。
4. `Storage.setItem()` 方法允许你在存储中保存一个数据项——它接受两个参数：数据项的名字及其值。试着把它输入到你的 JavaScript 控制台（如果你愿意的话，可以把它的值改为你自己的名字！）

```js
localStorage.setItem("name", "Chris");
```

5. `Storage.getItem()` 方法接受一个参数——你想要检索的数据项的名称——并返回数据项的值。现在将这些代码输入到你的 JavaScript 控制台：

```js
var myName = localStorage.getItem("name");
myName;
```

在输入第二行时，你应该会看到 `myName`变量现在包含`name`数据项的值。
6. `Storage.removeItem()` 方法接受一个参数——你想要删除的数据项的名称——并从 web storage 中删除该数据项。在你的 JavaScript 控制台中输入以下几行：

```js
localStorage.removeItem("name");
var myName = localStorage.getItem("name");
myName;
```

第三行现在应该返回 `null` — `name` 项已经不存在于 web storage 中。
### 数据会一直存在！

web storage 的一个关键特性是，数据在不同页面加载时都存在（甚至是当浏览器关闭后，对 localStorage 的而言）。让我们来看看这个：

1. 再次打开我们的 Web Storage 空白模板，但是这次你要在不同的浏览器中打开这个教程！这样可以更容易处理。
2. 在浏览器的 JavaScript 控制台中输入这几行：

```js
localStorage.setItem("name", "Chris");
var myName = localStorage.getItem("name");
myName;
```

你应该看到 name 数据项返回。
3. 现在关掉浏览器再把它打开。
4. 再次输入下面几行：

```js
var myName = localStorage.getItem("name");
myName;
```

你应该看到，尽管浏览器已经关闭，然后再次打开，但仍然可以使用该值。
### 为每个域名分离储存

每个域都有一个单独的数据存储区 (每个单独的网址都在浏览器中加载). 你 会看到，如果你加载两个网站（例如 google.com 和 amazon.com）并尝试将某个项目存储在一个网站上，该数据项将无法从另一个网站获取。

这是有道理的 - 你可以想象如果网站能够查看彼此的数据，就会出现安全问题！

### 更复杂的例子

让我们通过编写一个简单的工作示例来应用这些新发现的知识，让你了解如何使用网络存储。我们的示例将允许你输入一个名称，然后该页面将刷新，以提供个性化问候。这种状态也会页面/浏览器重新加载期间保持，因为这个名称存储在 Web Storage 中。

你可以在 personal-greeting.html 中找到示例文件——这包含一个具有标题，内容和页脚，以及用于输入你的姓名的表单的简单网站。

![](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage/web-storage-demo.png)

让我们来构建示例，以便了解它的工作原理。

1. 首先，在你的计算机上的新目录中创建一个 personal-greeting.html 文件的副本。
2. 接下来，请注意我们的 HTML 如何引用一个名为`index.js`的 JavaScript 文件（请参见第 40 行）。我们需要创建它并将 JavaScript 代码写入其中。在与 HTML 文件相同的目录中创建一个`index.js`文件。
3. 我们首先创建对所有需要在此示例中操作的 HTML 功能的引用 - 我们将它们全部创建为常量，因为这些引用在应用程序的生命周期中不需要更改。将以下几行添加到你的 JavaScript 文件中：

```js
// 创建所需的常量
const rememberDiv = document.querySelector(".remember");
const forgetDiv = document.querySelector(".forget");
const form = document.querySelector("form");
const nameInput = document.querySelector("#entername");
const submitBtn = document.querySelector("#submitname");
const forgetBtn = document.querySelector("#forgetname");

const h1 = document.querySelector("h1");
const personalGreeting = document.querySelector(".personal-greeting");
```

4. 接下来，我们需要包含一个小小的事件监听器，以在按下提交按钮时阻止实际的提交表单动作自身，因为这不是我们想要的行为。在你之前的代码下添加此代码段：在你之前的代码后添加这段代码：

```js
// 当按钮按下时阻止表单提交
form.addEventListener("submit", function (e) {
  e.preventDefault();
});
```

5. 现在我们需要添加一个事件监听器，当单击“Say hello”按钮时，它的处理函数将会运行。这些注释详细解释了每一处都做了什么，但实际上我们在这里获取用户输入到文本输入框中的名字并使用`setItem()`将它保存在网络存储中，然后运行一个名为`nameDisplayCheck()`的函数来处理实际的网站文本的更新。将此添加到代码的底部：

```js
// run function when the 'Say hello' button is clicked
submitBtn.addEventListener("click", function () {
  // store the entered name in web storage
  localStorage.setItem("name", nameInput.value);
  // run nameDisplayCheck() to sort out displaying the
  // personalized greetings and updating the form display
  nameDisplayCheck();
});
```

6. 此时，我们还需要一个事件处理程序，以便在单击“Forget”按钮时运行一个函数——且仅在单击“Say hello”按钮（两种表单状态来回切换）后才显示。在这个功能中，我们使用`removeItem()`从网络存储中删除项目`name`，然后再次运行`nameDisplayCheck()`以更新显示。将其添加到底部：

```js
// run function when the 'Forget' button is clicked
forgetBtn.addEventListener("click", function () {
  // Remove the stored name from web storage
  localStorage.removeItem("name");
  // run nameDisplayCheck() to sort out displaying the
  // generic greeting again and updating the form display
  nameDisplayCheck();
});
```

7. 现在是时候定义`nameDisplayCheck()`函数本身了。在这里，我们通过使用`localStorage.getItem('name')`作为测试条件来检查 name 数据项是否已经存储在 Web Storage 中。如果它已被存储，则该调用的返回值为`true`; 如果没有，它会是`false`。如果是`true`，我们会显示个性化问候语，显示表格的“forget”部分，并隐藏表格的“Say hello”部分。如果是`false`，我们会显示一个通用问候语，并做相反的事。再次将下面的代码添到底部：

```js
// define the nameDisplayCheck() function
function nameDisplayCheck() {
  // check whether the 'name' data item is stored in web Storage
  if (localStorage.getItem("name")) {
    // If it is, display personalized greeting
    let name = localStorage.getItem("name");
    h1.textContent = "Welcome, " + name;
    personalGreeting.textContent =
      "Welcome to our website, " +
      name +
      "! We hope you have fun while you are here.";
    // hide the 'remember' part of the form and show the 'forget' part
    forgetDiv.style.display = "block";
    rememberDiv.style.display = "none";
  } else {
    // if not, display generic greeting
    h1.textContent = "Welcome to our website ";
    personalGreeting.textContent =
      "Welcome to our website. We hope you have fun while you are here.";
    // hide the 'forget' part of the form and show the 'remember' part
    forgetDiv.style.display = "none";
    rememberDiv.style.display = "block";
  }
}
```

8. 最后但同样重要的是，我们需要在每次加载页面时运行`nameDisplayCheck()`函数。如果我们不这样做，那么个性化问候不会在页面重新加载后保持。将以下代码添加到代码的底部：

```js
document.body.onload = nameDisplayCheck;
```

你的例子完成了 - 做得好！现在剩下的就是保存你的代码并在浏览器中测试你的 HTML 页面。你可以在这里看到我们的完成版本并在线运行。

**备注：** 在 Using the Web Storage API 中还有一个稍微复杂点儿的示例。

**备注：** 在完成版本的源代码中， `<script src="index.js" defer></script>` 一行里， `defer` 属性指明在页面加载完成之前，`<script>`元素的内容不会执行。

存储复杂数据 — IndexedDB
------------------

IndexedDB API（有时简称 IDB）是可以在浏览器中访问的一个完整的数据库系统，在这里，你可以存储复杂的关系数据。其种类不限于像字符串和数字这样的简单值。你可以在一个 IndexedDB 中存储视频，图像和许多其他的内容。

但是，这确实是有代价的：使用 IndexedDB 要比 Web Storage API 复杂得多。在本节中，我们仅仅只能浅尝辄止地一提它的能力，不过我们会给你足够基础知识以帮助你开始。

### 通过一个笔记存储示例演示

在这里，我们将向你介绍一个示例，该示例允许你在浏览器中存储笔记并随时查看和删除它们，在我们进行时，我们将解释 IDB 的最基本部分并让你自己构建注释。

这个应用看起来像这样：

![](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage/idb-demo.png)

每个笔记都有一个标题和一些正文，每个都可以单独编辑。我们将在下面通过的 JavaScript 代码提供详细的注释，以帮助你了解正在发生的事情。

### 开始

1、首先，将 `index.html`, `style.css`, 和 `index-start.js` 文件的本地副本放入本地计算机上的新目录中。

2、浏览这些文件。你将看到 HTML 非常简单：具有页眉和页脚的网站，以及包含显示注释的位置的主内容区域，以及用于在数据库中输入新注释的表单。CSS 提供了一些简单的样式，使其更清晰。JavaScript 文件包含五个声明的常量，其中包含对将显示注释的 `<ul>` 元素的引用，标题和正文 `<input>` 元素，`<form>`本身，以及`<button>`。

3、将你的 JavaScript 文件重命名为 `index.js` 。你现在可以开始向其添加代码了。

### 数据库初始设置

现在让我们来看看为了建立数据库必须首先要做什么。

1. 在常量声明下，加入这几行：

```js
// Create an instance of a db object for us to store the open database in
let db;
```

这里我们声明了一个叫 `db` 的变量 — 这将在之后被用来存储一个代表数据库的对象。我们将在几个地方使用它，所以我们为了方便使用而在这里把它声明为全局的。
2. 接着，在你的代码最后添加如下代码：

```js
window.onload = function () {};
```

我们将把所有的后续代码写在这个 `window.onload` 事件处理函数内，这个函数将在 window 的 `load` 事件被触发时调用，为了确保我们没有在应用完整加载前试图使用 IndexedDB 功能（如果我们不这么做，它会失败）。
3. 在`window.onload`处理程序内，添加以下内容：

```js
// Open our database; it is created if it doesn't already exist
// (see onupgradeneeded below)
let request = window.indexedDB.open("notes", 1);
```

此行创建一个 `request` 变量，目的是打开 `notes`数据库的 `1`版本。如果`notes`数据库不存在，则后续代码将为你创建。你将在 IndexedDB 中经常看到此请求模式。数据库操作需要时间。你不希望在等待结果时挂起浏览器，因此数据库操作是异步的，这意味着它们不会立即发生，而是在将来的某个时刻发生，并且在完成后会收到通知。
 要在 IndexedDB 中处理此问题，你需要创建一个请求对象（可以随意命名 - 命名为`request`，可以表明它的用途）。然后，在请求完成或者失败时，使用事件处理程序来运行代码，你将在下面看到这些代码。

**备注：** 版本号很重要。如果要升级数据库（例如：更改表结构），则必须使用增加的版本号或者`onupgradeneeded`处理程序内指定的不同模式（请参阅下文）等再次运行代码。在这个简单教程中，我们不讨论数据库升级。

    1. 在之前添加的事件处理程序下方添加以下代码 - 在`window.onload`处理程序内：
     js
    ```js
    // onerror handler signifies that the database didn't open successfully
    request.onerror = function () {
      console.log("Database failed to open");
    };

    // onsuccess handler signifies that the database opened successfully
    request.onsuccess = function () {
      console.log("Database opened successfully");

      // Store the opened database object in the db variable. This is used a lot below
      db = request.result;

      // Run the displayData() function to display the notes already in the IDB
      displayData();
    };

    ```
    如果系统返回：请求失败，`request.onerror` (en-US)将会运行。这将允许你对这个问题做出响应。在我们的简单示例中，只是将消息打印到 JavaScript 控制台。
     如果系统返回：请求成功，表明成功打开数据库，`request.onsuccess` (en-US)将会运行。如果是这种情况，则表示已打开数据库的对象在`request.result` (en-US)属性中变为可用，从而允许我们操作数据库。我们将它存储在`db`我们之前创建的变量中供以后使用。我们还运行了一个名为 `displayData()` 的函数，它在 `<ul>` 内显示数据库中的数据。我们现在运行它，以便在页面加载时立即显示已经在数据库中的笔记。你将在后面看到 `displayData()` 的定义。
4. 最后，对于本节，我们可能会添加最重要的事件处理程序来设置数据库：`request.onupgradeneeded` (en-US)。如果尚未设置数据库，或者使用比现有存储数据库更大的版本号打开数据库（执行升级时），则运行此处理程序。在上一个处理程序下面添加以下代码：

```js
// Setup the database tables if this has not already been done
request.onupgradeneeded = function (e) {
  // Grab a reference to the opened database
  let db = e.target.result;

  // Create an objectStore to store our notes in (basically like a single table)
  // including a auto-incrementing key
  let objectStore = db.createObjectStore("notes", {
    keyPath: "id",
    autoIncrement: true,
  });

  // Define what data items the objectStore will contain
  objectStore.createIndex("title", "title", { unique: false });
  objectStore.createIndex("body", "body", { unique: false });

  console.log("Database setup complete");
};
```

这是我们定义数据库的模式（结构）的地方; 也就是说，它包含的列（或字段）集。这里我们首先从`e.target.result`（事件目标的`result`属性）中获取对现有数据库的引用，该引用是`request`对象。这相当于处理程序`db = request.result;`内部的行`onsuccess`，但我们需要在此单独执行此操作，因为`onupgradeneeded`处理程序（如果需要）将在`onsuccess`处理程序之前运行，这意味着`db`如果我们不这样做，该值将不可用。
 然后`IDBDatabase.createObjectStore()`，我们使用在打开的数据库中创建一个新的对象库。这相当于传统数据库系统中的单个表。我们给它起了名称注释，并且还指定了一个`autoIncrement`名为的关键字段`id`- 在每个新记录中，这将自动赋予增量值 - 开发人员不需要明确地设置它。作为密钥，该`id`字段将用于唯一标识记录，例如删除或显示记录时。
 我们还使用以下`IDBObjectStore.createIndex()` (en-US)方法创建另外两个索引（字段）:( `title`每个音符将包含一个标题），以及`body`（包含音符的正文）。

因此，通过设置这个简单的数据库模式，当我们开始向数据库添加记录时，每个记录都会沿着这些行表示为一个对象：

```js
{
  title: "Buy milk",
  body: "Need both cows milk and soya.",
  id: 8
}
```

### 添加数据到数据库

现在让我们看一下如何将记录添加到数据库中。这将使用我们页面上的表单完成。

在你之前的事件处理程序下面，添加以下一行，它设置了一个 `submit` 事件处理程序，当表单被提交时（当提交 `<button>` 元素被按下导致表单成功提交），运行一个叫做 `addData()` 的函数：

```js
// Create an onsubmit handler so that when the form is submitted the addData() function is run
form.onsubmit = addData;
```

现在让我们定义一下这个`addData()`功能。在上一行下面添加：

```js
// Define the addData() function
function addData(e) {
  // prevent default - we don't want the form to submit in the conventional way
  e.preventDefault();

  // grab the values entered into the form fields and store them in an object ready for being inserted into the DB
  let newItem = { title: titleInput.value, body: bodyInput.value };

  // open a read/write db transaction, ready for adding the data
  let transaction = db.transaction(["notes"], "readwrite");

  // call an object store that's already been added to the database
  let objectStore = transaction.objectStore("notes");

  // Make a request to add our newItem object to the object store
  var request = objectStore.add(newItem);
  request.onsuccess = function () {
    // Clear the form, ready for adding the next entry
    titleInput.value = "";
    bodyInput.value = "";
  };

  // Report on the success of the transaction completing, when everything is done
  transaction.oncomplete = function () {
    console.log("Transaction completed: database modification finished.");

    // update the display of data to show the newly added item, by running displayData() again.
    displayData();
  };

  transaction.onerror = function () {
    console.log("Transaction not opened due to error");
  };
}
```

这很复杂；要打破它，我们：

* 在事件对象上运行 `Event.preventDefault()` 以停止以传统方式实际提交的表单（这将导致页面刷新并破坏体验）。
* 创建一个表示要输入数据库的记录的对象，并使用表单输入中的值填充它。请注意，我们不必明确包含一个 `id` 值——正如我们提前详细说明的那样，这是自动填充的。
* 使用 `IDBDatabase.transaction()` (en-US) 方法打开 `notes` 对象存储的 `readwrite` 事务。此事务对象允许我们访问对象存储，以便我们可以对其执行某些操作，例如添加新记录。
* 使用 `IDBTransaction.objectStore()` (en-US) 方法访问对象库，将结果保存在 `objectStore` 变量中。
* 使用 `IDBObjectStore.add()` 添加新记录到数据库。这创建了一个请求对象，与我们之前看到的方式相同。
* 在生命周期的关键点为 `request` 以及 `transaction` 对象添加事件处理程序以运行代码。请求成功后，我们会清除表单输入，以便输入下一个注释。交易完成后，我们 `displayData()` 再次运行该功能以更新页面上的注释显示。
### 显示数据

我们已经在代码中引用了 `displayData()` 两次，所以我们可能更好地定义它。将其添加到你的代码中，位于上一个函数定义之下：

```js
// Define the displayData() function
function displayData() {
  // Here we empty the contents of the list element each time the display is updated
  // If you ddn't do this, you'd get duplicates listed each time a new note is added
  while (list.firstChild) {
    list.removeChild(list.firstChild);
  }

  // Open our object store and then get a cursor - which iterates through all the
  // different data items in the store
  let objectStore = db.transaction("notes").objectStore("notes");
  objectStore.openCursor().onsuccess = function (e) {
    // Get a reference to the cursor
    let cursor = e.target.result;

    // If there is still another data item to iterate through, keep running this code
    if (cursor) {
      // Create a list item, h3, and p to put each data item inside when displaying it
      // structure the HTML fragment, and append it inside the list
      let listItem = document.createElement("li");
      let h3 = document.createElement("h3");
      let para = document.createElement("p");

      listItem.appendChild(h3);
      listItem.appendChild(para);
      list.appendChild(listItem);

      // Put the data from the cursor inside the h3 and para
      h3.textContent = cursor.value.title;
      para.textContent = cursor.value.body;

      // Store the ID of the data item inside an attribute on the listItem, so we know
      // which item it corresponds to. This will be useful later when we want to delete items
      listItem.setAttribute("data-note-id", cursor.value.id);

      // Create a button and place it inside each listItem
      let deleteBtn = document.createElement("button");
      listItem.appendChild(deleteBtn);
      deleteBtn.textContent = "Delete";

      // Set an event handler so that when the button is clicked, the deleteItem()
      // function is run
      deleteBtn.onclick = deleteItem;

      // Iterate to the next item in the cursor
      cursor.continue();
    } else {
      // Again, if list item is empty, display a 'No notes stored' message
      if (!list.firstChild) {
        let listItem = document.createElement("li");
        listItem.textContent = "No notes stored.";
        list.appendChild(listItem);
      }
      // if there are no more cursor items to iterate through, say so
      console.log("Notes all displayed");
    }
  };
}
```

再次，让我们打破这个：

* 首先，我们清空 `<ul>` 元素的内容，然后填充更新的内容。如果你不这样做，那么每次更新时都会添加大量重复内容。
* 接下来，我们`notes`使用`IDBDatabase.transaction()` (en-US)和`IDBTransaction.objectStore()` (en-US)我们一样得到对象存储的引用`addData()`，除了这里我们将它们链接在一行中。
* 下一步是使用`IDBObjectStore.openCursor()`方法打开对游标的请求 - 这是一个可用于迭代对象存储中的记录的构造。我们将一个`onsuccess`处理程序链接到该行的末尾以使代码更简洁 - 当成功返回游标时，运行处理程序。
* 我们`IDBCursor`使用 let 获取对游标本身（对象）的引用`cursor = e.target.result`。
* 接下来，我们检查光标是否包含来自数据存储区（`if(cursor){ ... }`）的记录 - 如果是这样，我们创建一个 DOM 片段，用记录中的数据填充它，然后将其插入页面（`<ul>`元素内部）。我们还包括一个删除按钮，当单击该按钮时，将通过运行该`deleteItem()`功能删除该注释，我们将在下一节中查看。
* 在`if`块结束时，我们使用该`IDBCursor.continue()` (en-US)方法将光标前进到数据存储区中的下一条记录，然后`if`再次运行块的内容。如果有另一个要迭代的记录，这会导致它被插入到页面中，然后`continue()`再次运行，依此类推。
* 当没有更多记录要迭代时，`cursor`将返回`undefined`，因此`else`块将运行而不是`if`块。此块检查是否有任何注释被插入`<ul>`- 如果没有，它会插入一条消息，说没有存储注释。
### 删除一条笔记

如上所述，当按下笔记的删除按钮时，笔记将被删除。这是通过`deleteItem()`函数实现的，如下所示：

```js
// Define the deleteItem() function
function deleteItem(e) {
  // retrieve the name of the task we want to delete. We need
  // to convert it to a number before trying it use it with IDB; IDB key
  // values are type-sensitive.
  let noteId = Number(e.target.parentNode.getAttribute("data-note-id"));

  // open a database transaction and delete the task, finding it using the id we retrieved above
  let transaction = db.transaction(["notes"], "readwrite");
  let objectStore = transaction.objectStore("notes");
  let request = objectStore.delete(noteId);

  // report that the data item has been deleted
  transaction.oncomplete = function () {
    // delete the parent of the button
    // which is the list item, so it is no longer displayed
    e.target.parentNode.parentNode.removeChild(e.target.parentNode);
    console.log("Note " + noteId + " deleted.");

    // Again, if list item is empty, display a 'No notes stored' message
    if (!list.firstChild) {
      let listItem = document.createElement("li");
      listItem.textContent = "No notes stored.";
      list.appendChild(listItem);
    }
  };
}
```

* 第一部分可以使用一些解释 - 我们检索要删除`Number(e.target.parentNode.getAttribute('data-note-id'))`的记录的 ID - 回想一下记录的 ID 是在第一次显示时保存在`data-note-id`属性中的`<li>`。但是，我们需要通过全局内置的Number（）对象传递属性，因为它当前是一个字符串，否则将无法被数据库识别。
* 然后，我们使用我们之前看到的相同模式获取对对象存储的引用，并使用该`IDBObjectStore.delete()` (en-US)方法从数据库中删除记录，并将 ID 传递给它。
* 当数据库事务完成后，我们`<li>`从 DOM 中删除注释，然后再次检查以查看它是否`<ul>`为空，并根据需要插入注释。

就是这样了！你的例子现在应该有效。

如果你遇到问题，请随时查看我们的实例（请参阅源代码）。

### 通过 IndexedDB 存储复杂数据

如上所述，IndexedDB 可用于存储不仅仅是简单的文本字符串。你可以存储任何你想要的东西，包括复杂的对象，如视频或图像 blob。并且它比任何其他类型的数据更难实现。

为了演示如何操作，我们编写了另一个名为IndexedDB 视频存储的示例（请参阅此处也可以在此处运行）。首次运行示例时，它会从网络下载所有视频，将它们存储在 IndexedDB 数据库中，然后在 UI 内部 `<video>` 元素中显示视频。第二次运行它时，它会在数据库中找到视频并从那里获取它们而不是显示它们 - 这使得后续加载更快，占用空间更少。

让我们来看看这个例子中最有趣的部分。我们不会全部看 - 它的很多内容与上一个示例类似，代码注释得很好。

1. 对于这个简单的例子，我们已经存储了视频的名称以获取数组 opf 对象：

```js
const videos = [
  { name: "crystal" },
  { name: "elf" },
  { name: "frog" },
  { name: "monster" },
  { name: "pig" },
  { name: "rabbit" },
];
```

2. 首先，一旦数据库成功打开，我们就运行一个`init()`函数。这会遍历不同的视频名称，尝试加载由`videos`数据库中的每个名称标识的记录。
 如果在数据库中找到每个视频（通过查看`request.result`评估是否容易检查`true`- 如果记录不存在，那么`undefined`），视频文件（存储为 blob）和视频名称将直接传递给`displayVideo()`函数以放置它们在用户界面中。如果没有，视频名称将传递给`fetchVideoFromNetwork()`函数...你猜对了 - 从网络中获取视频。

```js
function init() {
  // Loop through the video names one by one
  for (let i = 0; i < videos.length; i++) {
    // Open transaction, get object store, and get() each video by name
    let objectStore = db.transaction("videos").objectStore("videos");
    let request = objectStore.get(videos[i].name);
    request.onsuccess = function () {
      // If the result exists in the database (is not undefined)
      if (request.result) {
        // Grab the videos from IDB and display them using displayVideo()
        console.log("taking videos from IDB");
        displayVideo(
          request.result.mp4,
          request.result.webm,
          request.result.name,
        );
      } else {
        // Fetch the videos from the network
        fetchVideoFromNetwork(videos[i]);
      }
    };
  }
}
```

3. 以下片段是从内部`fetchVideoFromNetwork()`获取的 - 这里我们使用两个单独的`fetch()`请求获取视频的 MP4 和 WebM 版本。然后，我们使用该`Body.blob()`方法将每个响应的主体提取为 blob，为我们提供可以在以后存储和显示的视频的对象表示。
 我们在这里遇到了一个问题 - 这两个请求都是异步的，但我们只想在两个 promises 都满足时尝试显示或存储视频。幸运的是，有一种处理这种问题的内置方法 - `Promise.all()`。这需要一个参数 - 引用你要检查放置在数组中的履行的所有单个承诺 - 并且本身是基于承诺的。
 当所有这些承诺都履行完毕时，`all()`承诺将通过包含所有个人履行价值的数组来实现。在`all()`块中，你可以看到我们`displayVideo()`之前调用函数，就像在 UI 中显示视频一样，然后我们也调用`storeVideo()`函数将这些视频存储在数据库中。

```js
let mp4Blob = fetch("videos/" + video.name + ".mp4").then((response) =>
  response.blob(),
);
let webmBlob = fetch("videos/" + video.name + ".webm").then((response) =>
  response.blob(),
);

// Only run the next code when both promises have fulfilled
Promise.all([mp4Blob, webmBlob]).then(function (values) {
  // display the video fetched from the network with displayVideo()
  displayVideo(values[0], values[1], video.name);
  // store it in the IDB using storeVideo()
  storeVideo(values[0], values[1], video.name);
});
```

4. 我们`storeVideo()`先来看看吧。这与你在上一个示例中看到的用于向数据库添加数据的模式非常相似 - 我们打开一个`readwrite`事务并获取对象存储引用`videos`，创建一个表示要添加到数据库的记录的对象，然后使用它添加它`IDBObjectStore.add()`。

```js
function storeVideo(mp4Blob, webmBlob, name) {
  // Open transaction, get object store; make it a readwrite so we can write to the IDB
  let objectStore = db.transaction(['videos'], 'readwrite').objectStore('videos');
  // Create a record to add to the IDB
  let record = {
    mp4 : mp4Blob,
    webm : webmBlob,
    name : name
  }

  // Add the record to the IDB using add()
  let request = objectStore.add(record);

  ...

};
```

5. 最后但并非最不重要的是，我们`displayVideo()`创建了在 UI 中插入视频然后将它们附加到页面所需的 DOM 元素。最有趣的部分如下所示 - 要在`<video>`元素中实际显示我们的视频 blob，我们需要使用该`URL.createObjectURL()`方法创建对象 URL（指向存储在内存中的视频 blob 的内部 URL）。完成后，我们可以将对象 URL 设置为 `<source>` 元素`src`属性的值，并且它可以正常工作。

```js
function displayVideo(mp4Blob, webmBlob, title) {
  // Create object URLs out of the blobs
  let mp4URL = URL.createObjectURL(mp4Blob);
  let webmURL = URL.createObjectURL(webmBlob);

  ...

  let video = document.createElement('video');
  video.controls = true;
  let source1 = document.createElement('source');
  source1.src = mp4URL;
  source1.type = 'video/mp4';
  let source2 = document.createElement('source');
  source2.src = webmURL;
  source2.type = 'video/webm';

  ...
}
```

离线文件存储
------

上面的示例已经说明了如何创建一个将大型资产存储在 IndexedDB 数据库中的应用程序，从而无需多次下载它们。这已经是对用户体验的一个很大的改进，但仍然有一件事 - 每次访问网站时仍然需要下载主要的 HTML，CSS 和 JavaScript 文件，这意味着当没有网络连接时，它将无法工作。

![](/zh-CN/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage/ff-offline.png)

这就是服务工作者和密切相关的Cache API 的用武之地。

服务工作者是一个 JavaScript 文件，简单地说，它是在浏览器访问时针对特定来源（网站或某个域的网站的一部分）进行注册的。注册后，它可以控制该来源的可用页面。它通过坐在加载的页面和网络之间以及拦截针对该来源的网络请求来实现这一点。

当它拦截一个请求时，它可以做任何你想做的事情（参见用例思路），但经典的例子是离线保存网络响应，然后提供响应请求而不是来自网络的响应。实际上，它允许你使网站完全脱机工作。

Cache API 是另一种客户端存储机制，略有不同 - 它旨在保存 HTTP 响应，因此与服务工作者一起工作得非常好。

**备注：** 现在大多数现代浏览器都支持服务工作者和缓存。在撰写本文时，Safari 仍在忙着实施它，但它应该很快就会存在。

### 一个 service worker 例子

让我们看一个例子，让你对这可能是什么样子有所了解。我们已经创建了另一个版本的视频存储示例，我们在上一节中看到了 - 这个功能完全相同，只是它还通过服务工作者将 Cache，CSS 和 JavaScript 保存在 Cache API 中，允许示例脱机运行！

请参阅 IndexedDB 视频存储，其中 service worker 在运行，并且还可以查看源代码。

#### 注册服务工作者

首先要注意的是，在主 JavaScript 文件中放置了一些额外的代码（请参阅index.js）。首先，我们进行特征检测测试，以查看`serviceWorker`该`Navigator`对象中是否有该成员。如果返回 true，那么我们知道至少支持服务工作者的基础知识。在这里，我们使用该`ServiceWorkerContainer.register()`方法将`sw.js`文件中包含的服务工作者注册到它所驻留的源，因此它可以控制与它或子目录相同的目录中的页面。当其承诺履行时，服务人员被视为已注册。

```js
// Register service worker to control making site work offline

if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register(
      "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js",
    )
    .then(function () {
      console.log("Service Worker Registered");
    });
}
```

**备注：** `sw.js`文件的给定路径是相对于站点源的，而不是包含代码的 JavaScript 文件。服务人员在`https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js`。原点是`https://mdn.github.io`，因此给定的路径必须是`/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js`。如果你想在自己的服务器上托管此示例，则必须相应地更改此示例。这是相当令人困惑的，但出于安全原因，它必须以这种方式工作。

#### 安装 service worker

下次访问服务工作者控制下的任何页面时（例如，重新加载示例时），将针对该页面安装服务工作者，这意味着它将开始控制它。发生这种情况时，`install`会向服务工作人员发起一个事件; 你可以在服务工作者本身内编写代码来响应安装。

让我们看一下sw.js文件（服务工作者）中的一个例子。你将看到安装侦听器已注册`self`。此`self`关键字是一种从服务工作文件内部引用服务工作者的全局范围的方法。

在`install` 处理程序内部，我们使用`ExtendableEvent.waitUntil()`事件对象上可用的方法来表示浏览器不应该完成服务工作者的安装，直到其中的 promise 成功完成。

这是我们在运行中看到 Cache API 的地方。我们使用该`CacheStorage.open()`方法打开一个可以存储响应的新缓存对象（类似于 IndexedDB 对象存储）。此承诺通过`Cache`表示`video-store`缓存的对象来实现。然后，我们使用该`Cache.addAll()`方法获取一系列资产并将其响应添加到缓存中。

```js
self.addEventListener("install", function (e) {
  e.waitUntil(
    caches.open("video-store").then(function (cache) {
      return cache.addAll([
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/",
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html",
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js",
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css",
      ]);
    }),
  );
});
```

这就是现在，安装完成。

#### 响应未来的请求

在我们的 HTML 页面上注册并安装了服务工作者，并且所有相关资产都添加到我们的缓存中，我们几乎准备好了。还有一件事要做，写一些代码来响应进一步的网络请求。

这就是第二位代码的`sw.js`作用。我们向服务工作者全局范围添加另一个侦听器，该范围在`fetch`引发事件时运行处理函数。只要浏览器在服务工作者注册的目录中请求资产，就会发生这种情况。

在处理程序内部，我们首先记录所请求资产的 URL。然后，我们使用该`FetchEvent.respondWith()`方法为请求提供自定义响应。

在这个块中，我们`CacheStorage.match()`用来检查是否可以在任何缓存中找到匹配的请求（即匹配 URL）。如果未找到匹配，或者`undefined`如果未找到匹配，则此承诺将满足匹配的响应。

如果找到匹配项，我们只需将其作为自定义响应返回。如果没有，我们从网络中获取（）响应并返回该响应。

```js
self.addEventListener("fetch", function (e) {
  console.log(e.request.url);
  e.respondWith(
    caches.match(e.request).then(function (response) {
      return response || fetch(e.request);
    }),
  );
});
```

这就是我们简单的服务工作者。你可以使用它们进行更多的负载 - 有关详细信息，请参阅服务工作者手册。感谢 Paul Kinlan 在他的文章中添加服务工作者和离线到你的 Web 应用程序，这启发了这个简单的例子。

#### 测试离线示例

要测试我们的服务工作者示例，你需要加载它几次以确保它已安装。完成后，你可以：

* 尝试拔掉网络连接/关闭 Wifi。
* 如果你使用的是 Firefox，请选择*文件>脱机工作*。
* 转到 devtools，然后选择*Application> Service Workers*，如果你使用的是 Chrome，请选中*Offline*选中。

如果再次刷新示例页面，你仍应该看到它加载得很好。所有内容都是脱机存储的 - 缓存中的页面资源以及 IndexedDB 数据库中的视频。

总结
--

现在就是这些。我们希望你能感觉到我们对客户端存储技术的介绍很有用。

参见
--

* 网页存储 API
* IndexedDB API
* Cookies
* Service worker API
* 上一页
* Overview: Client-side web APIs