变形 Transformations - Web API 接口参考

变形 Transformations
==================

* « 上一页
* 下一页 »

在本教程前面的部分中，我们已经了解了 Canvas 网格和坐标空间。到目前为止，我们只是根据我们的需要使用默认的网格，改变整个画布的大小。变形是一种更强大的方法，可以将原点移动到另一点、对网格进行旋转和缩放。

状态的保存和恢复
--------

在了解变形之前，我先介绍两个在你开始绘制复杂图形时必不可少的方法。

`save()`

保存画布 (canvas) 的所有状态

`restore()`

save 和 restore 方法是用来保存和恢复 canvas 状态的，都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。

Canvas 状态存储在栈中，每当`save()`方法被调用后，当前的状态就被推送到栈中保存。一个绘画状态包括：

* 当前应用的变形（即移动，旋转和缩放，见下）
* 以及下面这些属性：`strokeStyle`, `fillStyle`, `globalAlpha`, `lineWidth`, `lineCap`, `lineJoin`, `miterLimit`, `lineDashOffset`, `shadowOffsetX`, `shadowOffsetY`, `shadowBlur`, `shadowColor`, `globalCompositeOperation`, `font`, `textAlign`, `textBaseline`, `direction`, `imageSmoothingEnabled`
* 当前的裁切路径（clipping path），会在下一节介绍

你可以调用任意多次 `save`方法。每一次调用 `restore` 方法，上一个保存的状态就从栈中弹出，所有设定都恢复。

### `save` 和 `restore` 的应用例子

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Transformations/canvas_savestate.png)我们尝试用这个连续矩形的例子来描述 canvas 的状态栈是如何工作的。

第一步是用默认设置画一个大四方形，然后保存一下状态。改变填充颜色画第二个小一点的蓝色四方形，然后再保存一下状态。再次改变填充颜色绘制更小一点的半透明的白色四方形。

到目前为止所做的动作和前面章节的都很类似。不过一旦我们调用 `restore`，状态栈中最后的状态会弹出，并恢复所有设置。如果不是之前用 `save` 保存了状态，那么我们就需要手动改变设置来回到前一个状态，这个对于两三个属性的时候还是适用的，一旦多了，我们的代码将会猛涨。

当第二次调用 `restore` 时，已经恢复到最初的状态，因此最后是再一次绘制出一个黑色的四方形。

| Screenshot | Live sample |
| --- | --- |
|  |  |

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  ctx.fillRect(0, 0, 150, 150); // 使用默认设置绘制一个矩形
  ctx.save(); // 保存默认状态

  ctx.fillStyle = "#09F"; // 在原有配置基础上对颜色做改变
  ctx.fillRect(15, 15, 120, 120); // 使用新的设置绘制一个矩形

  ctx.save(); // 保存当前状态
  ctx.fillStyle = "#FFF"; // 再次改变颜色配置
  ctx.globalAlpha = 0.5;
  ctx.fillRect(30, 30, 90, 90); // 使用新的配置绘制一个矩形

  ctx.restore(); // 重新加载之前的颜色状态
  ctx.fillRect(45, 45, 60, 60); // 使用上一次的配置绘制一个矩形

  ctx.restore(); // 加载默认颜色配置
  ctx.fillRect(60, 60, 30, 30); // 使用加载的配置绘制一个矩形
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

移动 Translating
--------------

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Transformations/canvas_grid_translate.png)

我们先介绍 `translate`方法，它用来移动 canvas 和它的原点到一个不同的位置。

`translate(x, y)`

`translate`方法接受两个参数。\*x \*是左右偏移量，*y* 是上下偏移量，如右图所示。

在做变形之前先保存状态是一个良好的习惯。大多数情况下，调用 restore 方法比手动恢复原先的状态要简单得多。又，如果你是在一个循环中做位移但没有保存和恢复 canvas 的状态，很可能到最后会发现怎么有些东西不见了，那是因为它很可能已经超出 canvas 范围以外了。

### `translate` 的例子

这个例子显示了一些移动 canvas 原点的好处。如果不使用 `translate`方法，那么所有矩形都将被绘制在相同的位置（0,0）。`translate`方法同时让我们可以任意放置这些图案，而不需要在 `fillRect()` 方法中手工调整坐标值，既好理解也方便使用。

我在 `draw`方法中调用 `fillRect()` 方法 9 次，用了 2 层循环。每一次循环，先移动 canvas，画螺旋图案，然后恢复到原始状态。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  for (var i = 0; i < 3; i++) {
    for (var j = 0; j < 3; j++) {
      ctx.save();
      ctx.fillStyle = "rgb(" + 51 \* i + ", " + (255 - 51 \* i) + ", 255)";
      ctx.translate(10 + j \* 50, 10 + i \* 50);
      ctx.fillRect(0, 0, 25, 25);
      ctx.restore();
    }
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

旋转 Rotating
-----------

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Transformations/canvas_grid_rotate.png)

第二个介绍 `rotate`方法，它用于以原点为中心旋转 canvas。

`rotate(angle)`

这个方法只接受一个参数：旋转的角度 (angle)，它是顺时针方向的，以弧度为单位的值。

旋转的中心点始终是 canvas 的原点，如果要改变它，我们需要用到 `translate`方法。

### `rotate` 的例子

在这个例子里，我们将会使用 `rotate()` 方法来画圆并构成圆形图案。当然你也可以分别计算出 *x* 和 *y* 坐标（`x = r*Math.cos(a); y = r*Math.sin(a)`）。这里无论用什么方法都无所谓的，因为我们画的是圆。计算坐标的结果只是旋转圆心位置，而不是圆本身。即使用 `rotate`旋转两者，那些圆看上去还是一样的，不管它们绕中心旋转有多远。

这里我们又用到了两层循环。第一层循环决定环的数量，第二层循环决定每环有多少个点。每环开始之前，我都保存一下 canvas 的状态，这样恢复起来方便。每次画圆点，我都以一定夹角来旋转 canvas，而这个夹角则是由环上的圆点数目的决定的。最里层的环有 6 个圆点，这样，每次旋转的夹角就是 360/6 = 60 度。往外每一环的圆点数目是里面一环的 2 倍，那么每次旋转的夹角随之减半。

```js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");

  // left rectangles, rotate from canvas origin
  ctx.save();
  // blue rect
  ctx.fillStyle = "#0095DD";
  ctx.fillRect(30, 30, 100, 100);
  ctx.rotate((Math.PI / 180) \* 25);
  // grey rect
  ctx.fillStyle = "#4D4E53";
  ctx.fillRect(30, 30, 100, 100);
  ctx.restore();

  // right rectangles, rotate from rectangle center
  // draw blue rect
  ctx.fillStyle = "#0095DD";
  ctx.fillRect(150, 30, 100, 100);

  ctx.translate(200, 80); // translate to rectangle center
  // x = x + 0.5 \* width
  // y = y + 0.5 \* height
  ctx.rotate((Math.PI / 180) \* 25); // rotate
  ctx.translate(-200, -80); // translate back

  // draw grey rect
  ctx.fillStyle = "#4D4E53";
  ctx.fillRect(150, 30, 100, 100);
}
```

```
<canvas id="canvas" width="300" height="200"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

缩放 Scaling
----------

接着是缩放。我们用它来增减图形在 canvas 中的像素数目，对形状，位图进行缩小或者放大。

**`scale(x, y)`**

`scale`方法可以缩放画布的水平和垂直的单位。两个参数都是实数，可以为负数，x 为水平缩放因子，y 为垂直缩放因子，如果比 1 小，会缩小图形，如果比 1 大会放大图形。默认值为 1，为实际大小。

画布初始情况下，是以左上角坐标为原点的第一象限。如果参数为负实数，相当于以 x 或 y 轴作为对称轴镜像反转（例如，使用`translate(0,canvas.height); scale(1,-1);` 以 y 轴作为对称轴镜像反转，就可得到著名的笛卡尔坐标系，左下角为原点）。

默认情况下，canvas 的 1 个单位为 1 个像素。举例说，如果我们设置缩放因子是 0.5，1 个单位就变成对应 0.5 个像素，这样绘制出来的形状就会是原先的一半。同理，设置为 2.0 时，1 个单位就对应变成了 2 像素，绘制的结果就是图形放大了 2 倍。

### `scale` 的例子

这最后的例子里，我们用不同的缩放方式来画两个图形。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // draw a simple rectangle, but scale it.
  ctx.save();
  ctx.scale(10, 3);
  ctx.fillRect(1, 10, 10, 10);
  ctx.restore();

  // mirror horizontally
  ctx.scale(-1, 1);
  ctx.font = "48px serif";
  ctx.fillText("MDN", -135, 120);
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

变形 Transforms
-------------

最后一个方法允许对变形矩阵直接修改。

`transform(a, b, c, d, e, f)`

这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵，如下面的矩阵所示：

[

a

c

e

b

d

f

0

0

1

]

\left[ \begin{array}{ccc} a & c & e \ b & d & f \ 0 & 0 & 1 \end{array} \right]

如果任意一个参数是`Infinity`，变形矩阵也必须被标记为无限大，否则会抛出异常。

这个函数的参数各自代表如下：

`a (m11)`

水平方向的缩放

`b(m12)`

竖直方向的倾斜偏移

`c(m21)`

水平方向的倾斜偏移

`d(m22)`

竖直方向的缩放

`e(dx)`

水平方向的移动

`f(dy)`

竖直方向的移动

`setTransform(a, b, c, d, e, f)`

这个方法会将当前的变形矩阵重置为单位矩阵，然后用相同的参数调用 `transform`方法。如果任意一个参数是无限大，那么变形矩阵也必须被标记为无限大，否则会抛出异常。从根本上来说，该方法是取消了当前变形，然后设置为指定的变形，一步完成。

`resetTransform()`

重置当前变形为单位矩阵，它和调用以下语句是一样的：`ctx.setTransform(1, 0, 0, 1, 0, 0);`

### `transform`/`setTransform` 的例子

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  var sin = Math.sin(Math.PI / 6);
  var cos = Math.cos(Math.PI / 6);
  ctx.translate(100, 100);
  var c = 0;
  for (var i = 0; i <= 12; i++) {
    c = Math.floor((255 / 12) \* i);
    ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
    ctx.fillRect(0, 0, 100, 10);
    ctx.transform(cos, sin, -sin, cos, 0, 0);
  }

  ctx.setTransform(-1, 0, 0, 1, 100, 100);
  ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
  ctx.fillRect(0, 50, 100, 100);
}
```

```
<canvas id="canvas" width="200" height="250"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

* « 上一页
* 下一页 »


canvas 的优化 - Web API 接口参考

canvas 的优化
==========

* « 上一页
* 下一页 »

`<canvas>`元素是众多广泛使用的网络 2D 图像渲染标准之一。它被广泛用于游戏及复杂的图像可视化中。然而，随着网站和应用将 canvas 画布推至极限，性能开始成为问题。此文目标是给使用 canvas 画布元素的优化带来建议，去保证你的网站或者应用表现卓越。

性能贴士
----

下面是一些改善性能的建议

### 在离屏 canvas 上预渲染相似的图形或重复的对象

如果发现自己在每个动画帧上重复了一些相同的绘制操作，请考虑将其分流到屏幕外的画布上。然后，你可以根据需要频繁地将屏幕外图像渲染到主画布上，而不必首先重复生成该图像的步骤。

```js
myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");

myEntity.render(myEntity.offscreenContext);
```

### 避免浮点数的坐标点，用整数取而代之

当你画一个没有整数坐标点的对象时会发生子像素渲染。

```js
ctx.drawImage(myImage, 0.3, 0.5);
```

浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况，请保证在你调用`drawImage()`函数时，用`Math.floor()`函数对所有的坐标点取整。

### 不要在用`drawImage`时缩放图像

在离屏 canvas 中缓存图片的不同尺寸，而不要用`drawImage()`去缩放它们。

### 使用多层画布去画一个复杂的场景

在你的应用程序中，你可能会发现某些对象需要经常移动或更改，而其他对象则保持相对静态。在这种情况下，可能的优化是使用多个`<canvas>`元素对你的项目进行分层。

例如，假设你有一个游戏，其 UI 位于顶部，中间是游戏性动作，底部是静态背景。在这种情况下，你可以将游戏分成三个`<canvas>`层。UI 将仅在用户输入时发生变化，游戏层随每个新框架发生变化，并且背景通常保持不变。

```html
<div id="stage">
  <canvas id="ui-layer" width="480" height="320"></canvas>
  <canvas id="game-layer" width="480" height="320"></canvas>
  <canvas id="background-layer" width="480" height="320"></canvas>
</div>

<style>
 #stage {
 width: 480px;
 height: 320px;
 position: relative;
 border: 2px solid black;
 }
 canvas {
 position: absolute;
 }
 #ui-layer {
 z-index: 3;
 }
 #game-layer {
 z-index: 2;
 }
 #background-layer {
 z-index: 1;
 }
</style>
```

### 用 CSS 设置大的背景图

如果像大多数游戏那样，你有一张静态的背景图，用一个静态的`<div>`元素，结合`background` 特性，以及将它置于画布元素之后。这么做可以避免在每一帧在画布上绘制大图。

### 用 CSS transforms 特性缩放画布

CSS transforms 使用 GPU，因此速度更快。最好的情况是不直接缩放画布，或者具有较小的画布并按比例放大，而不是较大的画布并按比例缩小。

```js
var scaleX = window.innerWidth / canvas.width;
var scaleY = window.innerHeight / canvas.height;

var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);

stage.style.transformOrigin = "0 0"; //scale from top left
stage.style.transform = "scale(" + scaleToFit + ")";
```

### 关闭透明度

如果你的游戏使用画布而且不需要透明，当使用 `HTMLCanvasElement.getContext()` 创建一个绘图上下文时把 `alpha` 选项设置为 `false` 。这个选项可以帮助浏览器进行内部优化。

```js
var ctx = canvas.getContext("2d", { alpha: false });
```

### 更多的贴士

* 将画布的函数调用集合到一起（例如，画一条折线，而不要画多条分开的直线）
* 避免不必要的画布状态改变
* 渲染画布中的不同点，而非整个新状态
* 尽可能避免 `shadowBlur`特性
* 尽可能避免text rendering
* 尝试不同的方法来清除画布 (`clearRect()` vs. `fillRect()` vs. 调整 canvas 大小)
* 有动画，请使用`window.requestAnimationFrame()` 而非`window.setInterval()`
* 请谨慎使用大型物理库
参见
--

* Improving HTML5 Canvas Performance – HTML5 Rocks
* Optimizing your JavaScript game for Firefox OS – Mozilla Hacks
* « 上一页
* 下一页 »


使用图像 - Web API 接口参考

使用图像
====

* « 上一页
* 下一页 »

canvas 更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景，以及游戏界面（Sprites）等等。浏览器支持的任意格式的外部图片都可以使用，比如 PNG、GIF 或者 JPEG。你甚至可以将同一个页面中其他 canvas 元素生成的图片作为图片源。

引入图像到 canvas 里需要以下两步基本操作：

1. 获得一个指向`HTMLImageElement`的对象或者另一个 canvas 元素的引用作为源，也可以通过提供一个 URL 的方式来使用图片（参见例子）
2. 使用`drawImage()`函数将图片绘制到画布上

我们来看看具体是怎么做的。

获得需要绘制的图片
---------

canvas 的 API 可以使用下面这些类型中的一种作为图片的源：

**`HTMLImageElement`**

这些图片是由 `Image()` 函数构造出来的，或者任何的 `<img>` 元素

**`HTMLVideoElement`**

用一个 HTML 的 `<video>`元素作为你的图片源，可以从视频中抓取当前帧作为一个图像

**`HTMLCanvasElement`**

可以使用另一个 `<canvas>` 元素作为你的图片源。

**`ImageBitmap`**

这是一个高性能的位图，可以低延迟地绘制，它可以从上述的所有源以及其他几种源中生成。

这些源统一由 `CanvasImageSource`类型来引用。

有几种方式可以获取到我们需要在 canvas 上使用的图片。

### 使用相同页面内的图片

我们可以通过下列方法的一种来获得与 canvas 相同页面内的图片的引用：

* `document.images`集合
* `document.getElementsByTagName()`方法
* 如果你知道你想使用的指定图片的 ID，你可以用`document.getElementById()`获得这个图片
### 使用其他域名下的图片

在 `HTMLImageElement`上使用crossOrigin (en-US)属性，你可以请求加载其他域名上的图片。如果图片的服务器允许跨域访问这个图片，那么你可以使用这个图片而不污染 canvas，否则，使用这个图片将会污染 canvas。

### 使用其他 canvas 元素

和引用页面内的图片类似地，用 `document.getElementsByTagName` 或 `document.getElementById` 方法来获取其他 canvas 元素。但你引入的应该是已经准备好的 canvas。

一个常用的应用就是将第二个 canvas 作为另一个大的 canvas 的缩略图。

### 由零开始创建图像

或者我们可以用脚本创建一个新的 `HTMLImageElement` 对象。要实现这个方法，我们可以使用很方便的 `Image()` 构造函数。

```js
var img = new Image(); // 创建一个<img>元素
img.src = "myImage.png"; // 设置图片源地址
```

当脚本执行后，图片开始装载。

若调用 `drawImage` 时，图片没装载完，那什么都不会发生（在一些旧的浏览器中可能会抛出异常）。因此你应该用 load 事件来保证不会在加载完毕之前使用这个图片：

```js
var img = new Image(); // 创建 img 元素
img.onload = function () {
  // 执行 drawImage 语句
};
img.src = "myImage.png"; // 设置图片源地址
```

如果你只用到一张图片的话，这已经够了。但一旦需要不止一张图片，那就需要更加复杂的处理方法，但图片预加载策略超出本教程的范围。

### 通过 data: url 方式嵌入图像

我们还可以通过 data:url 方式来引用图像。Data urls 允许用一串 Base64 编码的字符串的方式来定义一个图片。

```js
img.src =
  "data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==";
```

其优点就是图片内容即时可用，无须再到服务器兜一圈。（还有一个优点是，可以将 CSS，JavaScript，HTML 和 图片全部封装在一起，迁移起来十分方便。）缺点就是图像没法缓存，图片大的话内嵌的 url 数据会相当的长：

### 使用视频帧

你还可以使用`<video>` 中的视频帧（即便视频是不可见的）。例如，如果你有一个 ID 为“myvideo”的`<video>` 元素，你可以这样做：

```js
function getMyVideo() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    return document.getElementById("myvideo");
  }
}
```

它将为这个视频返回`HTMLVideoElement`对象，正如我们前面提到的，它可以作为我们的 Canvas 图片源。

绘制图片
----

一旦获得了源图对象，我们就可以使用 `drawImage` 方法将它渲染到 canvas 里。`drawImage` 方法有三种形态，下面是最基础的一种。

**`drawImage(image, x, y)`**

其中 `image` 是 `image` 或者 `canvas` 对象，`x` 和 `y` 是其在目标 `canvas` 里的起始坐标。

**备注：** SVG 图像必须在 <svg> 根指定元素的宽度和高度。

### 示例：一个简单的线图

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images/canvas_backdrop.png)

下面一个例子我用一个外部图像作为一线性图的背景。用背景图我们就不需要绘制复杂的背景，省下不少代码。这里只用到一个 image 对象，于是就在它的 `onload` 事件响应函数中触发绘制动作。`drawImage` 方法将背景图放置在 canvas 的左上角 (0,0) 处。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="180" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  var img = new Image();
  img.onload = function () {
    ctx.drawImage(img, 0, 0);
    ctx.beginPath();
    ctx.moveTo(30, 96);
    ctx.lineTo(70, 66);
    ctx.lineTo(103, 76);
    ctx.lineTo(170, 15);
    ctx.stroke();
  };
  img.src = "backdrop.png";
}
```

结果看起来是这样的：

| Screenshot | Live sample |
| --- | --- |
|  |  |

缩放 Scaling
----------

`drawImage` 方法的又一变种是增加了两个用于控制图像在 canvas 中缩放的参数。

`drawImage(image, x, y, width, height)`

这个方法多了 2 个参数：`width` 和 `height，`这两个参数用来控制 当向 canvas 画入时应该缩放的大小

### 示例：平铺图像

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images/canvas_scale_image.png)

在这个例子里，我会用一张图片像背景一样在 canvas 中以重复平铺开来。实现起来也很简单，只需要循环铺开经过缩放的图片即可。见下面的代码，第一层 `for` 循环是做行重复，第二层是做列重复的。图像大小被缩放至原来的三分之一，50x38 px。这种方法可以用来很好的达到背景图案的效果，在下面的教程中会看到。

**备注：** 图像可能会因为大幅度的缩放而变得起杂点或者模糊。如果你的图像里面有文字，那么最好还是不要进行缩放，因为那样处理之后很可能图像里的文字就会变得无法辨认了。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  var img = new Image();
  img.onload = function () {
    for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 3; j++) {
        ctx.drawImage(img, j \* 50, i \* 38, 50, 38);
      }
    }
  };
  img.src = "rhino.jpg";
}
```

结果看起来像这样：

| Screenshot | Live sample |
| --- | --- |
|  |  |

切片 Slicing
----------

`drawImage` 方法的第三个也是最后一个变种有 8 个新参数，用于控制做切片显示的。

`drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)`

第一个参数和其他的是相同的，都是一个图像或者另一个 canvas 的引用。其他 8 个参数最好是参照右边的图解，前 4 个是定义图像源的切片位置和大小，后 4 个则是定义切片的目标显示位置和大小。

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images/canvas_drawimage.jpg)

切片是个做图像合成的强大工具。假设有一张包含了所有元素的图像，那么你可以用这个方法来合成一个完整图像。例如，你想画一张图表，而手上有一个包含所有必需的文字的 PNG 文件，那么你可以很轻易的根据实际数据的需要来改变最终显示的图表。这方法的另一个好处就是你不需要单独装载每一个图像。

### 示例：相框

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images/canvas_drawimage2.jpg)

在这个例子里面我用到上面已经用过的犀牛图像，不过这次我要给犀牛头做个切片特写，然后合成到一个相框里面去。相框带有阴影效果，是一个以 24-bit PNG 格式保存的图像。因为 24-bit PNG 图像带有一个完整的 8-bit alpha 通道，与 GIF 和 8-bit PNG 不同，我可以将它放成背景而不必担心底色的问题。

我用一个与上面用到的不同的方法来装载图像，直接将图像插入到 HTML 里面，然后通过 CSS 隐藏（`display:none`）它。两个图像我都赋了 `id` ，方便后面使用。看下面的脚本，相当简单，首先对犀牛头做好切片（第一次`drawImage`）放在 canvas 上，然后再上面套个相框（第二次`drawImage`）。

```html
<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
    <div style="display:none;">
      <img id="source" src="rhino.jpg" width="300" height="227" />
      <img id="frame" src="canvas\_picture\_frame.png" width="132" height="150" />
    </div>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d");

  // Draw slice
  ctx.drawImage(
    document.getElementById("source"),
    33,
    71,
    104,
    124,
    21,
    20,
    87,
    104,
  );

  // Draw frame
  ctx.drawImage(document.getElementById("frame"), 0, 0);
}
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

画廊示例
----

我这一章最后的示例是弄一个小画廊。画廊由挂着几张画作的格子组成。当页面装载好之后，为每张画创建一个 canvas 元素并用加上画框然后插入到画廊中去。

在我这个例子里面，所有“画”都是固定宽高的，画框也是。你可以做些改进，通过脚本用画的宽高来准确控制围绕它的画框的大小。

下面的代码应该是蛮简单易懂的了。就是遍历图像对象数组，依次创建新的 canvas 元素并添加进去。可能唯一需要注意的，对于那些并不熟悉 DOM 的朋友来说，是 `Node.insertBefore` 方法的用法。`insertBefore` 是父节点（单元格）的方法，用于将新节点（canvas 元素）插入到我们想要插入的节点之前。

```html
<html>
  <body onload="draw();">
    <table>
      <tr>
        <td><img src="gallery\_1.jpg" /></td>
        <td><img src="gallery\_2.jpg" /></td>
        <td><img src="gallery\_3.jpg" /></td>
        <td><img src="gallery\_4.jpg" /></td>
      </tr>
      <tr>
        <td><img src="gallery\_5.jpg" /></td>
        <td><img src="gallery\_6.jpg" /></td>
        <td><img src="gallery\_7.jpg" /></td>
        <td><img src="gallery\_8.jpg" /></td>
      </tr>
    </table>
    <img id="frame" src="canvas\_picture\_frame.png" width="132" height="150" />
  </body>
</html>
```

```css
body {
  background: 0 -100px repeat-x url(bg\_gallery.png) #4f191a;
  margin: 10px;
}

img {
  display: none;
}

table {
  margin: 0 auto;
}

td {
  padding: 15px;
}
```

```js
function draw() {
  // Loop through all images
  for (i = 0; i < document.images.length; i++) {
    // Don't add a canvas for the frame image
    if (document.images[i].getAttribute("id") != "frame") {
      // Create canvas element
      canvas = document.createElement("CANVAS");
      canvas.setAttribute("width", 132);
      canvas.setAttribute("height", 150);

      // Insert before the image
      document.images[i].parentNode.insertBefore(canvas, document.images[i]);

      ctx = canvas.getContext("2d");

      // Draw image to canvas
      ctx.drawImage(document.images[i], 15, 20);

      // Add frame
      ctx.drawImage(document.getElementById("frame"), 0, 0);
    }
  }
}
```

控制图像的缩放行为
---------

如同前文所述，过度缩放图像可能会导致图像模糊或像素化。你可以通过使用绘图环境的`imageSmoothingEnabled`属性来控制是否在缩放图像时使用平滑算法。默认值为`true`，即启用平滑缩放。你也可以像这样禁用此功能：

```js
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
```

* « 上一页
* 下一页 »


Coordinates.latitude - Web API 接口参考

Coordinates.latitude
====================

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

**`Coordinates.latitude`** 只读属性，十进制双精度浮点数来表示纬度坐标

语法
--
```

lat = coordinates.latitude
```

规范
--

| Specification |
| --- |
| Geolocation API # latitude-longitude-and-accuracy-attributes |

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

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

参见
--

* Using geolocation
* The `Coordinates` interface it belongs to.


DocumentType：after() 方法 - Web API 接口参考

DocumentType：after() 方法
=======================

**`DocumentType.after()`** 方法在 `DocumentType` 的父级的子列表中插入一组 `Node` 对象或字符串，紧跟在 `DocumentType` 之后。字符串将作为等效的 `Text` (en-US) 节点插入。

语法
--

```js
after(param1)
after(param1, param2)
after(param1, param2, /\* … ,\*/ paramN)
```

### 参数

`param1`, …, `paramN`

要插入的一组 `Node` 对象或字符串。

### 返回值

无 (`undefined`)。

### 异常

`HierarchyRequestError` `DOMException`

当无法将节点插入到层次结构的指定点时抛出。

示例
--

```js
let docType = document.implementation.createDocumentType("html", "", "");
let myDoc = document.implementation.createDocument("", "", docType);

docType.after(document.createElement("html"));

myDoc.childNodes;
// 节点列表 [<!DOCTYPE html>, <html>]
```

规范
--

| Specification |
| --- |
| DOM Standard # ref-for-dom-childnode-after① |

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

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

参见
--

* `DocumentType.before()` (en-US)


Option() - Web API 接口参考

Option()
========

用于创建`HTMLOptionElement`的构造函数。

句法
--
```

var optionElementReference = new Option(text, value, defaultSelected, selected);
```

### 参数

文字可选

表示元素内容的`DOMString`，即显示的文本。如果没有指定，则使用默认值""(空字符串)。

值可选

表示`HTMLOptionElement`的值的`DOMString`，即`value`等价的`<option>` 的属性。如果未指定，则将文本的值用作值，例如，将表单提交给服务器时，相关联的`<select>`元素的值。

defaultSelected 可选

设置`selected`属性值的`Boolean` (en-US)，也就是说这个`<option>`是默认值当第一次加载页面时，在`<select>`元素中选择。如果没有指定，`false`则使用默认值。请注意，`true` 如果选项尚未被选中，则该值不会将选项设置为选中状态。

选中可选

A `Boolean` (en-US)设置选项的选择状态; 默认是`false`(未选中)。如果省略，即使`defaultSelected`参数是`true`，该选项没有被选中。

规范
--

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


ContentVisibilityAutoStateChangeEvent：skipped 属性 - Web API 接口参考

ContentVisibilityAutoStateChangeEvent：skipped 属性
================================================

Limited availability
--------------------

This feature is not Baseline because it does not work in some of the most widely-used browsers.

* Learn more
* See full compatibility
* Report feedback
若用户代理跳过元素内容，则 `ContentVisibilityAutoStateChangeEvent` 接口的只读属性 `skipped` 返回 `true`，否则返回 `false`。

值
-

布尔值。若用户代理跳过元素内容则返回 `true`，否则返回 `false`。

示例
--

```js
const canvasElem = document.querySelector("canvas");

canvasElem.addEventListener("contentvisibilityautostatechange", stateChanged);
canvasElem.style.contentVisibility = "auto";

function stateChanged(event) {
  if (event.skipped) {
    stopCanvasUpdates(canvasElem);
  } else {
    startCanvasUpdates(canvasElem);
  }
}

// 在画布需要开始更新时调用此方法。
function startCanvasUpdates(canvas) {
  // …
}

// 在画布需要停止更新时调用此方法。
function stopCanvasUpdates(canvas) {
  // …
}
```

规范
--

| Specification |
| --- |
| CSS Containment Module Level 2 # dom-contentvisibilityautostatechangeevent-skipped |

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

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

参见
--

* `contentvisibilityautostatechange` 事件
* CSS 局限
* `content-visibility` 属性
* `contain` 属性


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

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

Limited availability
--------------------

This feature is not Baseline because it does not work in some of the most widely-used browsers.

* Learn more
* See full compatibility
* Report feedback
构造函数 **`ContentVisibilityAutoStateChangeEvent()`** 创建新的 `ContentVisibilityAutoStateChangeEvent` 对象实例。

语法
--

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

### 参数

`type`

表示事件类型的字符串。对于 `ContentVisibilityAutoStateChangeEvent`，此参数恒为 `event`。

`options` 可选

包含下列属性的对象：

`skipped`

布尔值，若用户代理跳过元素内容则设置为 `true`，否则为 `false`。

示例
--

开发者不会手动使用此构造函数。当触发 `contentvisibilityautostatechange` 事件导致调用事件处理器时将构造新的 `ContentVisibilityAutoStateChangeEvent` 对象。

```js
canvasElem.addEventListener("contentvisibilityautostatechange", (event) => {
  // …
});
```

规范
--

| Specification |
| --- |
| CSS Containment Module Level 2 # dom-contentvisibilityautostatechangeevent-contentvisibilityautostatechangeevent |

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

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

参见
--

* `contentvisibilityautostatechange` 事件
* CSS 局限
* `content-visibility` 属性
* `contain` 属性


InputEvent.inputType - Web API 接口参考

InputEvent.inputType
====================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
`InputEvent` 接口中的只读属性 **`inputType`** 返回对可编辑内容所做更改的类型。可能的更改包括插入、删除和格式化文本。

语法
--

```js
var aString = inputEvent.inputType;
```

### 返回值

一个包含所做输入的类型的字符串。有许多可能的值，例如 `insertText`、`deleteContentBackward`、`insertFromPaste` 和 `formatBold`。有关可用输入类型的完整列表，请参阅 Input Events Level 1 规范的属性部分。

示例
--

此示例记录一个可编辑的 `<div>` 所触发的 input 事件的 `inputType`。

### HTML

```html
<p id="log">Input type:</p>
<div
 contenteditable="true"
 style="margin: 20px;padding: 20px;border:2px dashed red;">
  <p>
    Some sample text. Try inserting line breaks, or deleting text in different
    ways, or pasting different content in.
  </p>
  <hr />
  <ul>
    <li>A sample</li>
    <li>bulleted</li>
    <li>list.</li>
  </ul>
  <p>Another paragraph.</p>
</div>
```

### JavaScript

```js
const log = document.getElementById("log");
const editable = document.querySelector("div[contenteditable]");
editable.addEventListener("input", logInputType);

function logInputType(event) {
  log.textContent = `Input type: ${event.inputType}`;
}
```

### 结果

尝试编辑 `<div>` 中的文本，并看看发生了什么事。

**备注：** 有关更详细的示例，请参见 Masayuki Nakano 的 InputEvent 测试套件。

规范
--

| Specification |
| --- |
| UI Events # dom-inputevent-inputtype |

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

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


InputEvent.dataTransfer - Web API 接口参考

InputEvent.dataTransfer
=======================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

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

`InputEvent` 接口中的只读属性 **`dataTransfer`** 返回一个 `DataTransfer` 对象，该对象包含有关要添加到可编辑内容，或从可编辑内容中删除的富文本或纯文本数据的信息。

语法
--
```

var dataTransfer = inputEvent.dataTransfer
```

### 返回值

一个 `DataTransfer` 对象。

示例
--

在下面的简单示例中，我们在 input 事件上设置了一个事件监听器，以便在将任何内容粘贴到 `<p>` 元素时，通过 `InputEvent.dataTransfer.getData()` 方法检索其 HTML 源代码，并在输入框下面的段落中报告。

尝试复制并粘贴提供的部分内容以查看效果。注意，部分浏览器对其支持不佳。

```html
<p><span style="font-weight: bold; color: blue">Whoa, bold blue text!</span></p>
<p>
  <span style="font-weight: italic; color: red"
 >Exciting: italic red text!</span
 >
</p>
<p>Boring normal text ;-(</p>

<hr />

<p contenteditable="true">
  Go on, try pasting some content into this editable paragraph and see what
  happens!
</p>

<p class="result"></p>
```

```js
var editable = document.querySelector("p[contenteditable]");
var result = document.querySelector(".result");
var dataTransferObj;

editable.addEventListener("input", (e) => {
  result.textContent = e.dataTransfer.getData("text/html");
});
```

规范
--

| Specification |
| --- |
| Input Events Level 2 # dom-inputevent-datatransfer |

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

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


InputEvent：isComposing 属性 - Web API 接口参考

InputEvent：isComposing 属性
=========================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
**`InputEvent.isComposing`** 只读属性返回一个布尔值，表示事件是否是在 `compositionstart` 之后且在 `compositionend` 之前触发的。

返回值
---

一个布尔值。

示例
--

```js
const inputEvent = new InputEvent("syntheticInput", false);
console.log(inputEvent.isComposing); // 返回 false
```

规范
--

| Specification |
| --- |
| UI Events # dom-inputevent-iscomposing |

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

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

参见
--

* `compositionstart` 和 `compositionend`
* `InputEvent`


InputEvent() - Web API 接口参考

InputEvent()
============

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

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

构造函数 **`InputEvent()`** 返回一个新创建的 `InputEvent` 对象。

语法
--
```

 event = new InputEvent(typeArg, inputEventInit);
```

### 参数

*typeArg*

一个 `DOMString` ，表示事件的名称。

*inputEventInit*可选

一个 `InputEventInit` 字典，有以下字段：

* `inputType`（可选），指定可编辑内容更改类型的字符串，例如插入、删除或格式化文本。
* `data`（可选），包含要插入的字符的字符串。如果更改未插入文本（例如删除字符时），则其可能为空字符串。
* `dataTransfer`（可选），一个 `DataTransfer` 对象，其中包含有关添加到可编辑内容，或从可编辑内容中删除的富文本或纯文本数据的信息。
* `isComposing`（可选），一个布尔值，指示事件是组合会话的一部分，这意味着它在 `compositionstart` 事件之后，但在 `compositionend` 事件之前。默认值为 `false`。
* `ranges`（可选），一个静态 `Range` 数组，如果输入事件没有被取消，它将受到对 DOM 的更改的影响。

`InputEventInit` 字典也接受来自 `UIEventInit` 以及 `EventInit` 字典的值。

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

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

参考
--

* `InputEvent`，它构造的对象所属的接口。


InputEvent.data - Web API 接口参考

InputEvent.data
===============

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

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

**备注：** 请注意，**`data`** 属性在使用键盘输入时会返回输入的字符内容，但在粘贴、拖动时可能会返回 `null`，这取决于浏览器。浏览器也可能把一些数据保存在 `InputEvent.dataTransfer`，而不是该 **`data`** 属性中。

`InputEvent` 接口中的只读属性 **`data`** 返回含有插入字符数据的 `DOMString`。如果更改未插入文本（例如删除字符时），则其可能为空字符串。

语法
--
```

var string = inputEvent.data;
```

### 返回值

一个 `DOMString`。

示例
--

在下面的简单示例中，我们在 input 事件上设置了一个事件监听器，以便在对 `<input>` 元素的内容进行任何更改时（通过键入或粘贴），通过 **`InputEvent.data`** 属性检索添加的文本，并在 `<input>` 下面的段落中报告。

```html
<p>Some text to copy and paste.</p>

<input type="text" />

<p class="result"></p>
```

```js
var editable = document.querySelector("input");
var result = document.querySelector(".result");

editable.addEventListener("input", (e) => {
  result.textContent = "Inputted text: " + e.data;
});
```

规范
--

| Specification |
| --- |
| Input Events Level 2 # dfn-data |

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

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


Checking when a deadline is due - Web API 接口参考

Checking when a deadline is due
===============================

在本文中，我们将看一个复杂的示例，该示例涉及根据 IndexedDB 存储的截止日期检查当前时间和日期。这里的主要复杂因素是检查存储的截止日期信息（月，小时，日等）与 Date 对象的当前时间和日期。

![A screenshot of the sample app. A red main title saying To do app, a test to-do item, and a red form for users to enter new tasks](/zh-CN/docs/Web/API/IndexedDB_API/Checking_when_a_deadline_is_due/to-do-app.png)

The main example application we will be referring to in this article is **To-do list notifications**, a simple to-do list application that stores task titles and deadline times and dates via IndexedDB, and then provides users with notifications when deadline dates are reached, via the Notification, and Vibration APIs. You can download the To-do list notifications app from github and play around with the source code, or view the app running live.

基本问题
----

在待办事项应用程序中，我们希望首先以显示时机器可读和人类可理解的格式记录时间和日期信息，然后检查每个时间和日期是否在当前时刻发生。基本上，我们想要检查现在的时间和日期，然后检查每个存储的事件，看看他们的截止日期是否与当前时间和日期相匹配。如果他们这样做，我们希望通过某种通知让用户知道。

This would be easy if we were just comparing two `Date` objects, but of course humans don't want to enter deadline information in the same format JavaScript understands. Human-readable dates are quite different, with a number of different representations.

### Recording the date information

为了在移动设备上提供合理的用户体验，并减少歧义，我决定创建一个 HTML 表单：

![The form of the to-do app, containing fields to fill in a task title, and minute, hour, day, month and year values for the deadline.](/zh-CN/docs/Web/API/IndexedDB_API/Checking_when_a_deadline_is_due/to-do-app-form2.png)

* 用于输入待办事项列表标题的文本输入。这是用户输入中最不可避免的一点。
* Number inputs for the hour and minute parts of the deadline. On browsers that support `type="number"`, you get a nice little up and down arrow number picker. On mobile platforms you tend to get a numeric keypad for entering data, which is helpful. On others you just get a standard text input, which is okay.
* `<select>` 是用于输入截止日期的日期，月份和年份的元素。因为这些值对于用户来说是最模糊的（7，星期日，太阳？04,4，4 月，4 月？2013，'13，13？），我认为最好的解决方案是给他们一个选择，这也为移动用户节省了恼人的打字。日期记录为月份的数字天数，月份记录为完整月份名称，年份记录为完整的四位数年份数字

当我们点击 submit 按钮是，将会运行函数 `addData()` , 示例：

```js
function addData(e) {
  e.preventDefault();

  if(title.value == '' || hours.value == null || minutes.value == null || day.value == '' || month.value == '' || year.value == null) {
    note.innerHTML += '<li>Data not submitted — form incomplete.</li>';
    return;
  }
```

In this segment, we check to see if the form fields have all been filled in. If not, we drop a message into our developer notifications pane (see the bottom left of the app UI) to tell the user what is going on, and exit out of the function. This step is mainly for browsers that don't support HTML form validation (I have used the `required` attribute in my HTML to force validation, in those that do.)

```js
   else {
    var newItem = [
      {
        taskTitle: title.value,
        hours    : hours.value,
        minutes  : minutes.value,
        day      : day.value,
        month    : month.value,
        year     : year.value,
        notified : "no"
      }
    ];

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

    // report on the success of opening the transaction
    transaction.oncomplete = function(event) {
      note.innerHTML += '<li>Transaction opened for task addition.</li>';
    };

    transaction.onerror = function(event) {
      note.innerHTML += '<li>Transaction not opened due to error. Duplicate items not allowed.</li>';
    };

    // create an object store on the transaction
    var objectStore = transaction.objectStore("toDoList");

    // add our newItem object to the object store
    var request = objectStore.add(newItem[0]);
```

In this section we create an object called `newItem` that stores the data in the format required to insert it into the database. The next few lines open the database transaction and provide messages to notify the user if this was successful or failed.Then an `objectStore` is created into which the new item is added. The `notified` property of the data object indicates that the to-do list item's deadline has not yet come up and been notified - more on this later!

**备注：** The `db` variable stores a reference to the IndexedDB database instance; we can then use various properties of this variable to manipulate the data.

```js
    request.onsuccess = function(event) {

      note.innerHTML += '<li>New item added to database.</li>';

      title.value = '';
      hours.value = null;
      minutes.value = null;
      day.value = 01;
      month.value = 'January';
      year.value = 2020;
    };
  }
```

下一节将创建一条日志消息，说明新项目添加成功，并重置表单，以便为下一个任务输入做好准备。

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

Last of all, we run the `displayData()` function, which updates the display of data in the app to show the new task that was just entered.

### Checking whether a deadline has been reached

At this point our data is in the database; now we want to check whether any of the the deadlines have been reached. This is done by our `checkDeadlines()` function:

```js
function checkDeadlines() {
  var now = new Date();
```

First we grab the current date and time by creating a blank `Date` object. Easy huh? It's about to get a bit more complex.

```js
var minuteCheck = now.getMinutes();
var hourCheck = now.getHours();
var dayCheck = now.getDate();
var monthCheck = now.getMonth();
var yearCheck = now.getFullYear();
```

The `Date` object has a number of methods to extract various parts of the date and time inside it. Here we fetch the current minutes (gives an easy numerical value), hours (gives an easy numerical value), day of the month (`getDate()` is needed for this, as `getDay()` returns the day of the week, 1-7), month (returns a number from 0-11, see below), and year (`getFullYear()` is needed; `getYear()` is deprecated, and returns a weird value that is not much use to anyone!)

```js
   var objectStore = db.transaction(['toDoList'], "readwrite").objectStore('toDoList');

  objectStore.openCursor().onsuccess = function(event) {
    var cursor = event.target.result;

    if(cursor) {
```

Next we create another IndexedDB `objectStore`, and use the `openCursor()` method to open a cursor, which is basically a way in IndexedDB to iterate through all the items in the store. We then loop through all the items in the cursor for as long as there is a valid item left in the cursor.

```js
switch (cursor.value.month) {
  case "January":
    var monthNumber = 0;
    break;
  case "February":
    var monthNumber = 1;
    break;

  // other lines removed from listing for brevity

  case "December":
    var monthNumber = 11;
    break;
  default:
    alert("Incorrect month entered in database.");
}
```

我们要做的第一件事是将我们存储在数据库中的月份名称转换为 JavaScript 将理解的月份号码。如前所述，JavaScript Date 对象将月份值创建为 0 到 11 之间的数字。

```js
if (
  +cursor.value.hours == hourCheck &&
  +cursor.value.minutes == minuteCheck &&
  +cursor.value.day == dayCheck &&
  monthNumber == monthCheck &&
  cursor.value.year == yearCheck &&
  notified == "no"
) {
  // If the numbers all do match, run the createNotification()
  // function to create a system notification
  createNotification(cursor.value.taskTitle);
}
```

With the current time and date segments that we want to check against the IndexedDB stored values all assembled, it is time to perform the checks. We want all the values to match before we show the user some kind of notification to tell them their deadline is up.

The `+` operator in this case converts numbers with leading zeros into their non leading zero equivalents, e.g. 09 -> 9. This is needed because JavaScript `Date` number values never have leading zeros, but our data might.

The `notified == "no"` check is designed to make sure you will only get one notification per to-do item. When a notification is fired for each item object, its `notification` property is set to `"yes"` so this check will not pass on the next iteration, via the following code inside the `createNotification()` function (read Using IndexedDB for an explanation):

```js
    // now we need to update the value of notified to "yes" in this particular data object, so the
    // notification won't be set off on it again

    // first open up a tranaction as usual
    var objectStore = db.transaction(['toDoList'], "readwrite").objectStore('toDoList');

    // get the to-do list object that has this title as it's title
    var request = objectStore.get(title);

    request.onsuccess = function() {
      // grab the data object returned as the result
      var data = request.result;

      // update the notified value in the object to "yes"
      data.notified = "yes";

      // create another request that inserts the item back into the database
      var requestUpdate = objectStore.put(data);

      // when this new request succeeds, run the displayData() function again to update the display
      requestUpdate.onsuccess = function() {
        displayData();
      }
```

If the checks all match, we then run the `createNotification()` function to provide a notification to the user.

```js
       cursor.continue();
    }
  }
}
```

该函数的最后一行将光标移开，这导致上述截止日期检查机制为存储在 IndexedDB 中的下一个任务运行。

### Keep on checking!

Of course, it is no use to just run the above deadline checking function once! We want to keep constantly checking all the deadlines to see if any of them are being reached. To do this, we are simply using `setInterval()` to run `checkDeadlines()` once per second:

```js
setInterval(checkDeadlines, 1000);
```


使用 IndexedDB - Web API 接口参考

使用 IndexedDB
============

IndexedDB 是一种在用户浏览器内持久化存储数据的方法。它可以让你创建具有丰富查询能力的 Web 应用，而无需考虑网络可用性，因此你的应用在在线和离线时都可以正常运行。

关于本文档
-----

本篇教程将指导你如何使用 IndexedDB 的异步 API。如果你对 IndexedDB 还不熟悉，你应该首先阅读文章：IndexedDB 的关键特性和基本术语 (en-US)。

有关 IndexedDB API 的参考手册，请参见 IndexedDB API 这篇文章及其子页面。文章内容包括 IndexedDB 使用的对象类型，以及异步 API（同步 API 已从规范中删除）的方法。

基本模式
----

IndexedDB 鼓励使用的基本模式如下所示：

1. 打开数据库。
2. 在数据库中创建一个对象存储（object store）。
3. 启动事务，并发送一个请求来执行一些数据库操作，如添加或获取数据等。
4. 通过监听正确类型的 DOM 事件以等待操作完成。
5. 对结果进行一些操作（可以在 request 对象中找到）

有了这些提纲，我们可以进行更具体的探讨。

生成和构建一个对象存储
-----------

### 打开数据库

我们像下面这样开始整个过程：

```js
// 打开我们的数据库
const request = window.indexedDB.open("MyTestDatabase", 3);
```

看到了吗？打开数据库就像任何其他操作一样——你必须进行“请求”。

open 请求不会立即打开数据库或者开始一个事务。对 `open()` 函数的调用会返回一个我们可以作为事件来处理的包含结果（result，如果成功的话）或者错误值的 `IDBOpenDBRequest` (en-US) 对象。在 IndexedDB 中的大部分异步方法做的都是同样的事情——返回一个包含结果或错误的 `IDBRequest` 对象。open 函数的结果是一个 `IDBDatabase` 对象的实例。

open 方法的二个参数是数据库的版本号。数据库的版本决定了数据库模式（schema），即数据库的对象存储（object store）以及存储结构。如果数据库不存在，`open` 操作会创建该数据库，然后触发 `onupgradeneeded` 事件，你需要在该事件的处理器中创建数据库模式。如果数据库已经存在，但你指定了一个更高的数据库版本，会直接触发 `onupgradeneeded` 事件，允许你在处理器中更新数据库模式。我们在后面的创建或更新数据库的版本和 `IDBFactory.open` 参考页中会提到更多有关这方面的内容。

**警告：** 版本号是一个 `unsigned long long` 数字，这意味着它可以是一个特别大的数字，也意味着不能使用浮点数，否则它将会被转换成不超过它的最近整数，这可能导致事务无法启动，`upgradeneeded` 事件也不会被触发。例如，不要使用 2.4 作为版本号：`const request = indexedDB.open("MyTestDatabase", 2.4); // 不要这么做，因为版本会被取整为 2`

#### 生成处理器

对于我们产生的几乎所有的请求，我们首先要做的就是添加成功和失败处理器：

```js
request.onerror = (event) => {
  // 使用 request.errorCode 来做点什么！
};
request.onsuccess = (event) => {
  // 使用 request.result 来做点什么！
};
```

`onsuccess()` 和 `onerror()` 这两个函数哪个被调用呢？如果一切顺利的话，会触发 success 事件（即一个 `type` 属性被设置成 `"success"` 的 DOM 事件），并将 `request` 设置为它的 `target`。一旦它被触发，相关 `request` 的 `onsuccess()` 函数就会被触发，使用 success 事件作为它的参数。否则，如果有任何错误的话，error 事件（即 `type` 属性被设置成 `"error"` 的 DOM 事件）会在 `request` 上被触发。这将会触发使用 error 事件作为参数的 `onerror()` 函数。

IndexedDB API 以满足尽可能地减少对错误处理的需求而设计，所以你可能不会看到有很多的错误事件（至少，不会在你已经习惯了这些 API 之后！）。然而在打开数据库的情况下，还是有一些会产生错误事件的常见情况。最有可能出现的问题是用户决定不允许你的 web 应用创建数据库。IndexedDB 的主要设计目标之一就是允许大量数据可以被存储以供离线使用。（要了解关于针对每个浏览器你可以有多少存储空间的更多内容，请参见浏览器存储限制和清理标准页面的数据存储限制）。

显然，浏览器不希望允许某些广告网络或恶意网站来污染你的计算机，所以浏览器会在任意给定的 web 应用首次尝试打开 IndexedDB 以存储数据时对用户进行提醒。用户可以选择允许访问或者拒绝访问。此外，浏览器的隐私模式（Firefox 的隐私浏览模式和 Chrome 的无痕模式，但截至 2021 年 5 月，Firefox 尚未实现此特性，所以你仍然无法在 Firefox 的隐私浏览中使用 IndexedDB）下，IndexedDB 存储仅在内存中存在至隐私会话结束。

现在，假设用户已经允许了你的要创建数据库的请求，同时你也已经收到了一个触发了 success 回调的 success 事件；然后呢？这里的请求（request）是通过调用 `indexedDB.open()` 产生的，所以 `request.result` 是一个 `IDBDatabase` 的实例，而且你肯定希望将其保存下来以供后续使用。你的代码看起来可能像这样：

```js
let db;
const request = indexedDB.open("MyTestDatabase");
request.onerror = (event) => {
  console.error("为什么不允许我的 web 应用使用 IndexedDB！");
};
request.onsuccess = (event) => {
  db = event.target.result;
};
```

#### 错误处理

如上文所述，错误事件遵循冒泡机制。错误事件都是针对产生这些错误的请求的，然后事件冒泡到事务，然后最终到达数据库对象。如果你希望避免为所有的请求都增加错误处理程序，你可以仅对数据库对象添加错误处理器，像这样：

```js
db.onerror = (event) => {
  // 针对此数据库请求的所有错误的通用错误处理器！
  console.error(`数据库错误：${event.target.errorCode}`);
};
```

在打开数据库时常见的可能出现的错误之一是 `VER_ERR`。这表明存储在磁盘上的数据库的版本*高于*你试图打开的版本。这是一种必须要被错误处理器处理的一种出错情况。

### 创建或更新数据库的版本

当你创建一个新的数据库或者增加已存在的数据库的版本号（当打开数据库时，指定一个比之前更大的版本号），会触发 `onupgradeneeded` 事件，IDBVersionChangeEvent (en-US) 对象会作为参数传递给绑定在 `request.result`（例如示例中的 `db`）上的 `onversionchange` 事件处理器。在 `upgradeneeded` 事件的处理器中，你应该创建该数据库版本需要的对象存储（object store）：

```js
// 该事件仅在最新的浏览器中实现
request.onupgradeneeded = (event) => {
  // 保存 IDBDatabase 接口
  const db = event.target.result;

  // 为数据库创建对象存储（objectStore）
  const objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};
```

在这种情况下，数据库会保留之前版本数据库的对象存储，因此你不必再次创建这些对象存储。你需要创建新的对象存储，或删除不再需要的上一版本中的对象存储。如果你需要修改一个已存在的对象存储（例如要修改 `keyPath`），你必须先删除原先的对象存储，然后使用新的选项再次创建。（注意，这样会丢失对象存储中的数据，如果你需要保存这些信息，你要在数据库版本更新前读取出来并保存在别处）。

尝试创建一个与已存在（重名）的对象存储（或删除一个不存在的对象存储）会抛出错误。

如果 `onupgradeneeded` 事件成功执行完成，打开数据库请求的 `onsuccess` 处理器会被触发。

### 构建数据库

现在让我们构建数据库。IndexedDB 使用对象存储而不是表，并且一个数据库可以包含任意数量的对象存储。每当一个值被存入一个对象存储时，它会与一个键相关联。键的提供可以有几种不同的方法，这取决于对象存储是使用键路径 (en-US) 还是键生成器 (en-US)。

下面的表格显示了几种不同的提供键的方法。

| 键路径（`keyPath`） | 键生成器（`autoIncrement`） | 描述 |
| --- | --- | --- |
| 禁用 | 禁用 | 这种对象存储可以保存任意类型的值，甚至是像数字和字符串这种原始值。每当我们想要增加一个新值的时候，必须提供一个单独的键参数。 |
| 启用 | 禁用 | 这种对象存储只能保存 JavaScript 对象。这些对象必须具有一个和键路径同名的属性。 |
| 禁用 | 启用 | 这种对象存储可以保存任意类型的值。键会为我们自动生成，或者如果你想要使用一个特定键的话你可以提供一个单独的键参数。 |
| 启用 | 启用 | 这种对象存储只能保存 JavaScript 对象。通常一个键被生成的同时，生成的键的值会被存储在对象中的一个和键路径同名的属性中。然而，如果已存在该属性，该属性的值将被用作键而不会生成一个新的键。 |

在对象存储保存了对象，而不是原始值的情况下，你可以在任何对象存储上创建索引。索引可以让你使用被存储的对象的属性的值来查找存储在对象存储中的值，而不是用对象的键来查找。

此外，索引具有对存储的数据执行简单约束的能力。通过在创建索引时设置 unique 标志，索引可以确保不会有两个具有相同索引键路径值的对象被储存。因此，例如，如果你有一个用于存储一组个人信息的对象存储，并且你想要确保不会有两个拥有同样电子邮件地址的个人信息，你可以使用一个带有 unique 标志的索引来确保这些。

这听起来可能有点混乱，但下面这个简单的示例应该可以解释这些概念。首先，我们定义一些将在示例中用到的客户数据：

```js
// 我们的客户数据看起来像这样。
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" },
];
```

当然，你不会使用人们的社会保险号（ssn）作为客户表的主键，因为不是每个人都拥有社会保险号，并且你应该存储他们的生日而不是年龄。为了方便，这里我们忽略这些不合理的设计，继续往下看。

现在让我们看看如何创建一个 IndexedDB 来存储我们的数据：

```js
const dbName = "the\_name";

const request = indexedDB.open(dbName, 2);

request.onerror = (event) => {
  // 错误处理
};
request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // 创建一个对象存储来存储我们客户的相关信息，我们将“ssn”作为键路径
  // 因为 ssn 可以保证是不重复的——或至少在启动项目的会议上我们是这样被告知的。
  const objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // 创建一个索引以通过姓名来搜索客户。名字可能会重复，所以我们不能使用 unique 索引。
  objectStore.createIndex("name", "name", { unique: false });

  // 使用邮箱建立索引，我们想确保客户的邮箱不会重复，所以我们使用 unique 索引。
  objectStore.createIndex("email", "email", { unique: true });

  // 使用事务的 oncomplete 事件确保在插入数据前对象存储已经创建完毕。
  objectStore.transaction.oncomplete = (event) => {
    // 将数据保存到新创建的对象存储中。
    const customerObjectStore = db
      .transaction("customers", "readwrite")
      .objectStore("customers");
    customerData.forEach((customer) => {
      customerObjectStore.add(customer);
    });
  };
};
```

正如前面提到的，`onupgradeneeded` 是我们唯一可以修改数据库结构的地方。在这里面，我们可以创建和删除对象存储以及创建和删除索引。

对象存储仅调用 `createObjectStore()` 就可以创建。这个方法接受存储的名称和一个参数对象。即便这个参数对象是可选的，它还是非常重要的，因为它可以让你定义重要的可选属性，并完善你希望创建的对象存储的类型。在我们的示例中，我们创建了一个名为“customers”的对象存储，并且定义了一个使得每个存储中每个对象都独一无二的 `keyPath`。这个示例中的属性是“ssn”，因为社会安全号码被确保是唯一的。被存储在 `objectStore` 中的所有对象都必须存在“ssn”。

我们还创建了一个名为“name”的查询存储对象的 `name` 属性的索引。如同 `createObjectStore()`，`createIndex()` 接受一个可选的 `options` 对象，该对象细化了我们希望创建的索引类型。新增一个不带 `name` 属性的对象也会成功，但是这个对象不会出现在“name”索引中。

我们现在可以使用存储的客户对象的 `ssn` 直接从对象存储中检索它们，或者通过使用索引来使用它们的 name 进行查询。要了解这些是如何实现的，请参见使用索引部分。

### 使用键生成器

在创建对象存储时设置 `autoIncrement` 标志会为对象存储开启键生成器。这个标志默认是不开启的。

使用键生成器，当你向对象存储新增记录时键会自动生成。对象存储生成的键往往从 1 开始，然后自动生成的新的键会在之前的键的基础上加 1。生成的键的值从来不会减小，除非数据库操作结果被回滚，比如，数据库事务被中断。因此删除一条记录，甚至清空对象存储里的所有记录都不会影响对象存储的键生成器。

我们可以使用键生成器创建一个对象存储：

```js
// 打开 indexedDB。
const request = indexedDB.open(dbName, 3);

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // 创建另一个名为“names”的对象存储，并将 autoIncrement 标志设置为真。
  const objStore = db.createObjectStore("names", { autoIncrement: true });

  // 因为“names”对象存储拥有键生成器，所以它的键会自动生成。
  // 添加的记录将类似于：
  // 键：1 => 值："Bill"
  // 键：2 => 值："Donna"
  customerData.forEach((customer) => {
    objStore.add(customer.name);
  });
};
```

更多有关键生成器的详细信息，请参阅“W3C 键生成器”。

增加、读取和删除数据
----------

你需要开启一个事务才能对你创建的数据库进行操作。事务来自于数据库对象，而且你必须指定你想让这个事务跨越哪些对象存储。一旦你处于一个事务中，你就可以访问用于保存数据的对象存储，并发出请求。接下来，你要决定是对数据库进行更改还是只需从中读取数据。事务提供了三种模式：`readonly`、`readwrite` 和 `versionchange`。

必须在 `versionchange` 事务中才能修改数据库的“模式”或结构（包括新建或删除对象存储、索引）。该事务由一个指定了 version 的 `IDBFactory.open` 方法启动。

使用 `readonly` 或 `readwrite` 模式都可以从已存在的对象存储里读取记录。但只有在 `readwrite` 事务中才能修改对象存储。你需要使用 `IDBDatabase.transaction` (en-US) 启动一个事务。该方法接受两个参数：`storeNames`（作用域，一个你想访问的对象存储的数组）、事务模式 `mode`（`readonly` 或 `readwrite`）。该方法返回一个包含 `IDBIndex.objectStore` (en-US) 方法（你可以使用它来访问对象存储）的事务对象。未指定 `mode` 时，事务默认为 `readonly` 模式。

**备注：** 从 Firfox 40 起，IndexedDB 事务放松了对持久性的保证以提高性能（参见 Webkit bug 1112702）。以前在 `readwrite` 事务中，只有当所有的数据确保被写入磁盘时才会触发 `complete` (en-US) 事件。在 Firefox 40+ 中，当操作系统被告知去写入数据后 `complete` 事件便被触发，但此时数据可能还没有真正的写入磁盘。`complete` 事件触发因此变得更快，但这样会有极小的机会发生以下情况：如果操作系统崩溃或在数据被写入磁盘前断电，那么整个事务都将丢失。由于这种灾难事件是罕见的，大多数使用者并不需要过分担心。如果由于某些原因你必须确保数据的持久性（例如你要保存一个无法再次计算的关键数据），你可以使用实验性（非标准）的 `readwriteflush` 模式来创建事务以强制 `complete` 事件在数据写入磁盘后触发（参见 `IDBDatabase.transaction` (en-US)）。

你可以通过使用合适的作用域和模式来加速数据库访问，这有两个提示：

* 定义作用域时，仅指定你需要用到的对象存储。这样，你可以同时运行多个不含互相重叠作用域的事务。
* 只在必要时指定 `readwrite` 事务。你可以同时执行多个 `readonly` 事务，哪怕它们的作用域有重叠；但对于在一个对象存储上你只能运行一个 `readwrite` 事务。参见 IndexedDB 的关键特性和基本术语 (en-US)中事务 (en-US)的定义，以了解更多信息。
### 向数据库中增加数据

如果你刚刚创建了一个数据库，你可能想往里面写入一点数据。看起来会像下面这样：

```js
const transaction = db.transaction(["customers"], "readwrite");
// 注意：旧的实验性接口实现使用了已弃用的常量 IDBTransaction.READ\_WRITE 而不是 "readwrite"。
// 如果你想支持这样的实现，你可以这样写：
// const transaction = db.transaction(["customers"], IDBTransaction.READ\_WRITE);
```

`transaction()` 方法接受两个参数（一个是可选的）并返回一个事务对象。第一个参数是事务希望跨越的对象存储空间的列表。如果你希望事务能够跨越所有的对象存储空间，可以传入一个空数组，但请不要这样做，因为规范规定传入一个空数组会导致 InvalidAccessError。如果没有为第二个参数指定任何内容，得到的是只读事务。如果你想写入数据，你需要传入 `"readwrite"` 标志。

现在我们已经有了一个事务，我们需要理解它的生命周期。事务和事件循环的联系非常密切。如果你创建了一个事务，但是并未使用它而返回给事件循环，那么事务将变为非活跃状态。保持事务活跃的唯一方法就是在其上发出请求。当请求完成时你将会得到一个 DOM 事件，并且，假设请求成功了，你将会有另外一个在回调中延长这个事务的机会。如果你没有延长事务就返回到了事件循环，那么事务将会变得不活跃，依此类推。只要还有待处理的请求事务就会保持活跃。事务生命周期真的很简单，但是可能需要一点时间你才能习惯它。再举几个示例也会有所帮助。如果你开始看到 `TRANSACTION_INACTIVE_ERR` 错误代码，那么你已经把某些事情搞乱了。

事务接收三种不同类型的 DOM 事件：`error`、`abort` 和 `complete`。我们已经提及 `error` 事件是冒泡机制，所以事务会接收由它产生的所有请求所产生的错误。更微妙的一点，错误会中断它所处的事务。除非你在错误发生的第一时间就调用了 `stopPropagation()` 并执行了其他操作来处理错误，不然整个事务将会回滚。这种机制迫使你考虑和处理错误，如果觉得细致的错误处理太繁琐，你可以在数据库上添加一个捕获所有错误的错误处理器。如果你在事务中没有处理一个已发生的错误事件或者调用了 `abort()` 方法，那么该事务会被回滚，并触发 `abort` 事件。另外，在所有请求完成后，事务的 `complete` 事件会被触发。如果你进行大量数据库操作，跟踪事务而不是具体的请求会使逻辑更加清晰。

现在你拥有了一个事务，你需要从中获取一个对象存储。你只能在创建事务时指定的对象存储中获取。然后你可以添加任何你需要的数据。

```js
// 在所有数据添加完毕后的处理
transaction.oncomplete = (event) => {
  console.log("全部完成了！");
};

transaction.onerror = (event) => {
  // 不要忘记错误处理！
};

const objectStore = transaction.objectStore("customers");
customerData.forEach((customer) => {
  const request = objectStore.add(customer);
  request.onsuccess = (event) => {
    // event.target.result === customer.ssn;
  };
});
```

调用 `add()` 方法产生的请求的 `result` 是被添加的数据的键。所以在该例中，它应该与被添加对象的 `ssn` 属性相等，因为对象存储使用 `ssn` 属性作为键路径。注意，`add()` 函数要求数据库中不能存在相同键的对象。如果你想修改一个已存在的记录，或者你不关心该数据是否已存在，你可以使用 `put()` 方法，就像下面更新数据库中的记录部分所展示的。

从数据库中删除数据
---------

删除数据是非常类似的：

```js
const request = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")
  .delete("444-44-4444");
request.onsuccess = (event) => {
  // 删除成功！
};
```

从数据库中获取数据
---------

现在数据库里已经有了一些信息，你可以通过几种方法对它进行查询。首先是简单的 `get()`。你需要提供键来查询值，像这样：

```js
const transaction = db.transaction(["customers"]);
const objectStore = transaction.objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // 错误处理！
};
request.onsuccess = (event) => {
  // 对 request.result 做些操作！
  console.log(`SSN 444-44-4444 对应的名字是 ${request.result.name}`);
};
```

对于一个“简单”的查询，这里的代码有点多了。下面看我们怎么把它再缩短一点，假设你在数据库的级别上进行错误处理：

```js
db
  .transaction("customers")
  .objectStore("customers")
  .get("444-44-4444").onsuccess = (event) => {
  console.log(`SSN 444-44-4444 对应的名字是 ${event.target.result.name}`);
};
```

看看这是怎么做到的。因为这里只用到一个对象存储，你可以只传该对象存储的名字作为参数，而不必传一个列表。并且，你只需读取数据，所以不需要 `"readwrite"` 事务。不指定事务模式来调用 `transaction()` 你会得到一个 `"readonly"` 事务。另外一个微妙的地方在于你并没有保存请求对象到变量中。因为 DOM 事件把请求作为它的目标（target），你可以使用该事件来获取 `result` 属性。

### 更新数据库中的记录

现在我们已经查询了一些数据，修改一下并把它插回数据库的操作非常简单。让我们来稍微更新一下上例中的数据：

```js
const objectStore = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // 错误处理！
};
request.onsuccess = (event) => {
  // 获取我们想要更新的旧值
  const data = event.target.result;

  // 更新对象中你想修改的值
  data.age = 42;

  // 把更新过的对象放回数据库。
  const requestUpdate = objectStore.put(data);
  requestUpdate.onerror = (event) => {
    // 对错误进行处理
  };
  requestUpdate.onsuccess = (event) => {
    // 成功，数据已更新！
  };
};
```

所以这里我们创建了一个 `objectStore`，并通过指定 ssn 值（`444-44-4444`）从中请求了一条客户记录。然后我们把请求的结果保存在变量（`data`）中，并更新了该对象的 `age` 属性，之后创建了第二个请求（`requestUpdate`）将客户数据放回 `objectStore` 来覆盖之前的值。

**备注：** 在这个例子中我们必须指定 `readwrite` 事务，因为我们想要写入数据库，而不仅仅是从中读取。

### 使用游标

使用 `get()` 要求你知道你想要检索哪一个键。如果你想要遍历对象存储空间中的所有值，那么你可以使用游标。看起来会像下面这样：

```js
const objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    console.log(`SSN ${cursor.key} 对应的名字是 ${cursor.value.name}`);
    cursor.continue();
  } else {
    console.log("没有更多记录了！");
  }
};
```

`openCursor()` 函数接受几个参数。首先，你可以使用我们将在稍后介绍的 key range 对象来限制被检索的项目的范围。第二，你可以指定你希望进行迭代的方向。在上面的示例中，我们以升序迭代所有的对象。游标的成功回调有点特别。游标对象本身是请求的 `result`（上面我们使用的是简写形式，所以是 `event.target.result`）。然后，可以在游标对象的 `key` 和 `value` 属性中找到实际的键和值。如果你想要继续，那么你必须调用游标上的 `continue()`。当你已经到达数据的末尾时（或者没有匹配 `openCursor()` 请求的条目）你仍然会得到一个成功回调，但是 `result` 属性是 `undefined`。

使用游标的一种常见模式是查询一个对象存储中的所有对象，然后把它们添加到一个数组中，像这样：

```js
const customers = [];

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  } else {
    console.log(`已获取的所有客户：${customers}`);
  }
};
```

**备注：** 或者，你可以使用 `getAll()`（或 `getAllKeys()`）来处理这种情况。下面的代码的效果和上例相同：

```js
objectStore.getAll().onsuccess = (event) => {
  console.log(`已获取的所有客户：${event.target.result}`);
};
```

查询游标的 `value` 属性会带来性能消耗，因为对象是延迟创建的。如果你使用 `getAll()`，浏览器必须一次创建所有的对象。如果你只想检索键，那么使用游标将比使用 `getAll()` 高效得多。当然如果你想获取一个由对象存储中所有对象组成的数组，请使用 `getAll()`。

### 使用索引

使用 SSN 作为键来存储客户数据是合理的，因为 SSN 唯一地标识了一个个体（对隐私来说这是否是一个好的想法是另外一个话题，不在本文的讨论范围内）。如果你想要通过姓名来查找一个客户，那么，你将需要在数据库中迭代所有的 SSN 直到你找到正确的那个。以这种方式来查找将会非常的慢，相反你可以使用索引。

```js
// 首先，确定你已经在 request.onupgradeneeded 中创建了索引：
// objectStore.createIndex("name", "name");
// 否则你将得到 DOMException。

const index = objectStore.index("name");

index.get("Donna").onsuccess = (event) => {
  console.log(`Donna 的 SSN 是 ${event.target.result.ssn}`);
};
```

“name”属性不是唯一的，因此 `name` 被设成 `"Donna"` 的记录可能不止一条。在这种情况下，你总是得到键值最小的那个。

如果需要访问带有给定 `name` 的所有的记录，可以使用游标。你可以在索引上打开两种不同类型的游标：常规游标可以映射索引属性到对象存储空间中的对象，键游标可以映射索引属性到用来存储对象存储空间中的对象的键。差异如下所示：

```js
// 使用常规游标来获取所有客户记录的对象
index.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key 是名字，如“Bill”，而 cursor.value 是整个对象。
    console.log(
      `名字：${cursor.key}，SSN：${cursor.value.ssn}，电子邮件：${cursor.value.email}`,
    );
    cursor.continue();
  }
};

// 使用键游标来获取客户记录的对象的键
index.openKeyCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key 是名字，如“Bill”，而 cursor.value 是 SSN。
    // 无法直接获取存储对象的其余部分。
    console.log(`Name: ${cursor.key}, SSN: ${cursor.primaryKey}`);
    cursor.continue();
  }
};
```

### 指定游标的范围和方向

如果你想要限定你在游标中看到的值的范围，你可以使用 `IDBKeyRange` 对象，然后把它作为第一个参数传给 `openCursor()` 或 `openKeyCursor()`。你可以构造一个只允许单个键的键范围，或者一个具有下限或上限的，或者一个既有上限也有下限的。边界可以是“闭合的”（也就是说键范围包含给定的值）或者是“开放的”（也就是说键范围不包括给定的值）。它的工作原理如下：

```js
// 仅匹配“Donna”
const singleKeyRange = IDBKeyRange.only("Donna");

// 匹配所有大于“Bill”的，包括“Bill”
const lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// 匹配所有大于“Bill”的，但不包括“Bill”
const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// 匹配所有小于“Donna”的，不包括“Donna”
const upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// 匹配所有在“Bill”和“Donna”之间的，但不包括“Donna”
const boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// 使用其中的一个键范围，把它作为 openCursor()/openKeyCursor() 的第一个参数
index.openCursor(boundKeyRange).onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // 对匹配结果进行一些操作。
    cursor.continue();
  }
};
```

有时候你可能想要以降序而不是升序（所有游标的默认顺序）来迭代。切换方向是通过给 `openCursor()` 方法的第二个参数传递 `prev` 来实现的：

```js
objectStore.openCursor(boundKeyRange, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // 对记录进行一些操作。
    cursor.continue();
  }
};
```

如果你只是想改变迭代的方向，而不想对结果进行筛选，你只需要给第一个参数传入 null。

```js
objectStore.openCursor(null, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // 对记录进行一些操作。
    cursor.continue();
  }
};
```

因为“name”索引不是唯一的，那就有可能存在具有相同 `name` 的多条记录。要注意的是这种情况不可能发生在对象存储空间上，因为键必须永远是唯一的。如果你想要在游标在索引迭代过程中过滤出重复的，你可以传递 `nextunique`（或 `prevunique`，如果你正在反向查找）作为方向参数。当使用 `nextunique` 或是 `prevunique` 时，被返回的总是键最小的记录。

```js
index.openKeyCursor(null, "nextunique").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // 对记录进行一些操作。
    cursor.continue();
  }
};
```

请参见“IDBCursor 常量”，以获取有效的方向参数。

当一个 web 应用在另一个标签页中被打开时的版本变更
---------------------------

当你的 web 应用以数据库版本变更的方式发生改变时，你需要考虑，如果用户在一个标签页中打开的应用里使用了旧版本的数据库，在另一个标签页里加载新版本的数据库时会发生什么。当你使用更高的版本号调用 `open()` 方法时，其他所有打开的数据库必须显式地确认请求，你才能对数据库进行修改（`onblocked` 事件会被触发，直到它们被关闭或重新加载）。它的工作原理如下：

```js
const openReq = mozIndexedDB.open("MyTestDatabase", 2);

openReq.onblocked = (event) => {
  // 如果其他的一些页签加载了该数据库，在我们继续之前需要关闭它们。
  console.log("请关闭其他打开了该站点的标签页！");
};

openReq.onupgradeneeded = (event) => {
  // 其他的数据库已经被关闭，一切就绪。
  db.createObjectStore(/\* … \*/);
  useDatabase(db);
};

openReq.onsuccess = (event) => {
  const db = event.target.result;
  useDatabase(db);
  return;
};

function useDatabase(db) {
  // 请确保添加了在其他标签页请求了版本变更时会被通知的事件处理器。
  // 我们必须关闭数据库。这允许其他标签页更新数据库。
  // 如果不这样做，在用户关闭这些标签页之前，版本升级将不会发生。
  db.onversionchange = (event) => {
    db.close();
    console.log("此页面的新版本已准备就绪。请重新加载或关闭此标签页！");
  };

  // 处理数据库。
}
```

你同时也应监听 `VersionError` 错误来处理以下场景：已经打开的应用的初始化代码尝试打开过时版本的数据库。

安全
--

IndexedDB 使用同源原则，这意味着它把存储绑定到了创建它的站点的源（典型情况下，就是站点的域或是子域），所以它不能被任何其他来源访问。

第三方窗口内容（比如 `<iframe>` 内容）可以访问它所嵌入的源的 IndexedDB 仓库，除非浏览器被设置成从不接受第三方 cookie（参见 Firefox bug 1147821）。

浏览器关闭警告
-------

当浏览器关闭（由于用户选择关闭或退出选项），包含数据库的磁盘被意外移除，或者数据库存储的权限丢失，将发生以下问题：

1. 受影响的数据库（在浏览器关闭的场景下，所有打开的数据库）的所有事务会以 `AbortError` 错误中断。该影响和在每个事务中调用 `IDBTransaction.abort()` (en-US) 相同。
2. 所有的事务完成后，数据库连接就会关闭。
3. 最终，表示数据库连接的 `IDBDatabase` 对象收到一个 `close` (en-US) 事件。你可以使用 `IDBDatabase.onclose` (en-US) 事件处理器来监听这些事件，这样你就可以知道什么时候数据库被意外关闭了。

上述的行为只在 Firefox 50、Google Chrome 31（大约）发行版本中支持。

在这些版本之前的浏览器，事务会静默中断，并且不会触发 `close` (en-US) 事件，这样就无法察觉数据库的异常关闭。

由于用户可以在任何时候关闭浏览器，因此你不能依赖于任何特定事务的完成。并且在老版本的浏览器，你甚至都无法感知它们是否顺利完成。针对这种行为这里有一些启示：

首先，你应该始终使数据库在事务结束时处于一个稳定的状态。比如，假设你使用了一个数据库来保存一个允许用户编辑的项目列表，你通过清空对象存储然后写入新列表来在用户编辑后保存它，这存在一个风险，那就是浏览器可能在清空数据后还没有写入数据时就关闭了，使得对象存储变得空空如也。为了避免这种情况，你应该在同一个事务中执行清空数据和写入数据的操作。

其次，你不应该把数据库事务绑定到卸载事件上。如果卸载事件被浏览器关闭所触发，卸载事件处理器中的任何事务都不会完成。跨浏览器会话维护信息的直观的实现方法是在浏览器（或特定页）打开时从数据库读取它，在用户和浏览器交互时更新它，然后在浏览器（或页面）关闭时保存至数据库。然而，这并不会生效。这样一来，数据库事务会在卸载事件处理器中被创建，但由于它们是异步的，所以在执行之前就会被中断。

实际上，这里没有办法可以确保 IndexedDB 事务可以执行完毕，即使是浏览器正常关闭的情况。参见 Firefox bug 870645。作为一个正常关闭通知的变通方案，如果此时有事务在数据库卸载时还没有完成，你可以跟踪你的事务并添加一个 `beforeunload` 事件来提醒用户。

至少通过添加中断提醒和 `IDBDatabase.onclose` (en-US)，你可以得知它何时关闭了。

本地化的排序
------

Mozilla 已经在 Firefox 43+ 中实现了对 IndexedDB 数据进行本地化排序的能力。默认情况下，IndexedDB 根本不会处理国际化的字符串排序，所有的数据按照英文字母序排列。例如，b、á、z、a 会被排序为：

* a
* b
* z
* á

这显然不是用户想要的数据排序方式，例如 Aaron 和 Áaron 在通讯录中理应相邻地排列。如果要获取国际化的排序，需要将整个数据内容调入内存，然后由客户端 JavaScript 实现排序，显然这样做不是很高效。

这是一个新的功能，它允许开发者在使用 `IDBObjectStore.createIndex()` (en-US)（查看它的参数）创建索引时指定区域。在这种情况下，一个游标会被用于迭代数据集，如果你想指定本地化的排序，你可以使用专门的 `IDBLocaleAwareKeyRange` (en-US)。

`IDBIndex` 还添加了新的属性来指示它已经被指定了区域设置：`locale`（返回被指定的区域或 null）和 `isAutoLocale`（如果创建索引时使用了自动的区域，即使用了平台默认的区域，则返回 `true`；否则返回 `false`）。

**备注：** 现在该特性被标志所隐藏——请在 `about:config` 中开启 `dom.indexedDB.experimental` 来启用它并测试该特性。

完整的 IndexedDB 示例
----------------

我们有一个使用 IndexedDB API 的完整示例。该示例使用 IndexedDB 来存储和检索出版物。

* 尝试这个示例
* 查看源代码
参见
--

如有需要，请进一步阅读以获取更多信息。

### 参考

* IndexedDB API 参考
* Indexed Database API 规范
* Firefox 源码中的 IndexedDB 的接口文件
### 教程和指南

* 将 IndexedDB 中的数据绑定到 UI 元素（2012）
* IndexedDB——浏览器中的存储
### 函数库

* localForage：一个提供 name:value 的简单语法的客户端数据存储的 Polyfill，它基于 IndexedDB 实现，并在不支持 IndexedDB 的浏览器中自动回退到 Web SQL（已弃用）和 localStorage。
* Dexie.js：对 IndexedDB 的封装，通过提供更友好和简单语法以进行快速的编码开发。
* JsStore：一个具备类 SQL 语法的简单、高级的 IndexedDB 封装实现。
* MiniMongo：由 localStorage 支持的客户端内存 MongoDB 数据库，并通过 http 与服务器进行同步。MiniMongo 被 MeteorJS 所使用。
* PouchDB：一个在浏览器中使用 IndexedDB 实现的 CouchDB 客户端。
* IDB：一个小型函数库，主要镜像了 IndexedDB API，但在可用性方面有所改进。
* idb-keyval：使用 IndexedDB 实现的超简单（~600B）且基于 promise 的键值存储
* $mol\_db：微小的（~1.3kB）TypeScript 封装，具有基于 promise 的 API 以及自动迁移功能。
* RxDB：可以在 IndexedDB 之上使用的 NoSQL 客户端数据库。支持索引、压缩和复制。还向 IndexedDB 添加了交叉表和变化检测功能。


HashChangeEvent.oldURL - Web API 接口参考

HashChangeEvent.oldURL
======================

**`oldURL`** 为 `HashChangeEvent` 接口的只读属性，其值为窗口导航改变前的 URL。

语法
--
```

let oldEventUrl = event.oldURL;
```

### 返回值

`DOMString`.

示例
--

```js
window.addEventListener("hashchange", function (event) {
  console.log("Hash changed from " + event.oldURL);
});
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-hashchangeevent-oldurl-dev |


HashChangeEvent.newURL - Web API 接口参考

HashChangeEvent.newURL
======================

**`newURL`** 为 `HashChangeEvent` 接口的只读属性，其值为窗口导航改变后的 URL。

语法
--
```

let newEventUrl = event.newURL;
```

### 返回值

`DOMString`.

示例
--

```js
window.addEventListener("hashchange", function (event) {
  console.log("Hash changed to " + event.newURL);
});
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-hashchangeevent-newurl-dev |


NamedNodeMap.getNamedItem() - Web API 接口参考

NamedNodeMap.getNamedItem()
===========================

`NamedNodeMap` 接口的 **`NamedNodeMap.getNamedItem()`** 方法返回对应给定名称的`属性`，如果没有对应名称的属性则返回 `null`

语法
--
```

myAttr = attrs.getNamedItem(name)
```

### 参数

* `name` 是期望获取的属性名
浏览器兼容性
------

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


HTMLCanvasElement.transferControlToOffscreen() - Web API 接口参考

HTMLCanvasElement.transferControlToOffscreen()
==============================================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
方法 **`HTMLCanvasElement.transferControlToOffscreen()`** 将控制转移到一个在主线程或者 web worker 的 `OffscreenCanvas` 对象上。

语法
--

```js
transferControlToOffscreen()
```

### 返回值

一个 `OffscreenCanvas` 对象。

样例
--

```js
var htmlCanvas = document.createElement("canvas");
var offscreen = htmlCanvas.transferControlToOffscreen();
var gl = offscreen.getContext("webgl");

// ... some drawing using the gl context ...

// Push frames back to the original HTMLCanvasElement
gl.commit();
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-canvas-transfercontroltooffscreen-dev |

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

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

参见
--

* 定义此方法的接口 `HTMLCanvasElement`
* `OffscreenCanvas`


HTMLCanvasElement.toBlob() - Web API 接口参考

HTMLCanvasElement.toBlob()
==========================

**`HTMLCanvasElement.toBlob()`** 方法创造 `Blob` 对象，用以展示 canvas 上的图片；这个图片文件可以被缓存或保存到本地（由用户代理自行决定）。

可以在调用时指定所需的文件格式和图像质量，若未指定文件格式（或不支持指定的文件格式），则默认导出 `image/png` 类型。浏览器需要支持 `image/png`，大多数浏览器还支持额外的图片格式，包括 `image/jpeg` 和 `image/webp`。

对于支持以指定分辨率编码的图片格式，如不特别指明，图片的默认分辨率为 96dpi。

语法
--

```js
toBlob(callback)
toBlob(callback, type)
toBlob(callback, type, quality)
```

### 参数

`callback`

回调函数，可获得一个单独的 `Blob` 对象参数。如果图像未被成功创建，可能会获得 `null` 值。

`type` 可选

`DOMString` 类型，指定图片格式，默认格式（未指定或不支持）为 `image/png`。

`quality` 可选

`Number` 类型，值在 0 与 1 之间，当请求图片格式为 `image/jpeg` 或者 `image/webp` 时用来指定图片展示质量。如果这个参数的值不在指定类型与范围之内，则使用默认值，其余参数将被忽略。

### 返回值

无。

### 异常

`SecurityError`

canvas“被污染”的情况下不能使用此方法。

示例
--

### 将 canvas 图像转换为文件

当一个内容画到 canvas 上时，我们可以将它生成任何一个格式支持的图片文件。比如，下面的代码段获得了 id 为“canvas”的 `<canvas>` 元素中的图像，复制成一个 PNG 图，在文档中加入一个新的 `<img>` 元素，这个 `<img>` 元素的源图就是使用 canvas 创建的那个图像。

```js
const canvas = document.getElementById("canvas");

canvas.toBlob((blob) => {
  const newImg = document.createElement("img");
  const url = URL.createObjectURL(blob);

  newImg.onload = () => {
    // 不再需要读取该 blob，因此释放该对象
    URL.revokeObjectURL(url);
  };

  newImg.src = url;
  document.body.appendChild(newImg);
});
```

注意，我们这里创建的是 PNG 图片；如果在 `toBlob()` 传入第二个参数，就可以指定图片格式。例如，生成 JPEG 格式的图片：

```js
canvas.toBlob(
  (blob) => {
    /\* … \*/
  },
  "image/jpeg",
  0.95,
); // JPEG，95% 图像质量
```

### 将 canvas 转换为 ico（仅限 Mozilla）

使用 `-moz-parse` 来将 canvas 转换为 ico。Windows XP 不支持将 PNG 转为 ico，因此转为 bmp 作为代替。设置下载属性，生成下载链接。下载属性的值将被用来作为文件名。

```js
const canvas = document.getElementById("canvas");
const d = canvas.width;
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(d / 2, 0);
ctx.lineTo(d, d);
ctx.lineTo(0, d);
ctx.closePath();
ctx.fillStyle = "yellow";
ctx.fill();

function blobCallback(iconName) {
  return (b) => {
    const a = document.createElement("a");
    a.textContent = "下载";
    document.body.appendChild(a);
    a.style.display = "block";
    a.download = `${iconName}.ico`;
    a.href = window.URL.createObjectURL(b);
  };
}
canvas.toBlob(
  blobCallback("passThisString"),
  "image/vnd.microsoft.icon",
  "-moz-parse-options:format=bmp;bpp=32",
);
```

### 使用 OS.File 保存图像到本地（chrome/add-on context only）

**备注：** 此方法可将 toBlob 生成的图片保存到本地，但仅在 Firefox、Chrome 上下文或带有相关插件的情况下可用，因为 Web 并不存在 OS API。

```js
const canvas = document.getElementById("canvas");
const d = canvas.width;
ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(d / 2, 0);
ctx.lineTo(d, d);
ctx.lineTo(0, d);
ctx.closePath();
ctx.fillStyle = "yellow";
ctx.fill();

function blobCallback(iconName) {
  return (b) => {
    const r = new FileReader();
    r.onloadend = () => {
      // r.result 包含了该 ArrayBuffer。
      Cu.import("resource://gre/modules/osfile.jsm");
      const writePath = OS.Path.join(
        OS.Constants.Path.desktopDir,
        `${iconName}.ico`,
      );
      const promise = OS.File.writeAtomic(writePath, new Uint8Array(r.result), {
        tmpPath: `${writePath}.tmp`,
      });
      promise.then(
        () => {
          console.log("写入文件成功");
        },
        () => {
          console.log("写入文件失败");
        },
      );
    };
    r.readAsArrayBuffer(b);
  };
}

canvas.toBlob(
  blobCallback("passThisString"),
  "image/vnd.microsoft.icon",
  "-moz-parse-options:format=bmp;bpp=32",
);
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-canvas-toblob-dev |

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

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

参见
--

* `Blob`


HTMLCanvasElement.getContext() - Web API 接口参考

HTMLCanvasElement.getContext()
==============================

**`HTMLCanvasElement.getContext()`** 方法返回`canvas` 的上下文，如果上下文没有定义则返回 `null` .

在同一个 canvas 上以相同的 `contextType` 多次调用此方法只会返回同一个上下文。

语法
--
```

var ctx = canvas.getContext(contextType);
var ctx = canvas.getContext(contextType, contextAttributes);
```

### 参数

上下文类型（contextType）

是一个指示使用何种上下文的 `DOMString` 。可能的值是：

* `"2d`", 建立一个 `CanvasRenderingContext2D` 二维渲染上下文。
* `"webgl"` (或`"experimental-webgl"`) 这将创建一个 `WebGLRenderingContext` 三维渲染上下文对象。只在实现WebGL 版本 1(OpenGL ES 2.0) 的浏览器上可用。
* "`webgl2`" (或 "`experimental-webgl2`") 这将创建一个 `WebGL2RenderingContext` 三维渲染上下文对象。只在实现 WebGL 版本 2 (OpenGL ES 3.0) 的浏览器上可用。
实验性
* `"bitmaprenderer"` 这将创建一个只提供将 canvas 内容替换为指定`ImageBitmap`功能的`ImageBitmapRenderingContext` 。

**备注：** 标识符 "`experimental-webgl`" 或 "`experimental-webgl2`" 用于新 WebGL的实现。这些实现还没有达到测试套件一致性或图形驱动程序平台局势尚不稳定。Khronos Group 集团认证WebGL 实现在某些一致性规则。

`上下文属性 (contextAttributes)`

你可以在创建渲染上下文的时候设置多个属性，例如：

```js
canvas.getContext("webgl", { antialias: false, depth: false });
```

2d 上下文属性：

* **`alpha`**: `boolean`值表明`canvas`包含一个`alpha`通道。如果设置为`false`, 浏览器将认为`canvas`背景总是不透明的，这样可以加速绘制透明的内容和图片。
* 非标准
 (Gecko only) **`willReadFrequently`**: `boolean`值表明是否有重复读取计划。经常使用`getImageData()`，这将迫使软件使用 2D `canvas` 并 节省内存（而不是硬件加速）。这个方案适用于存在属性 `gfx.canvas.willReadFrequently`的环境。并设置为`true` (缺省情况下，只有 B2G / Firefox OS).
* 非标准
 (Blink only) **`storage`**: `string` 这样表示使用哪种方式存储 (默认为：持久（"persistent"）).

WebGL 上下文属性：

* **`alpha`**: `boolean`值表明`canvas`包含一个`alpha`缓冲区。
* **`antialias`**: `boolean`值表明是否开启抗锯齿。
* **`depth`**: `boolean`值表明绘制缓冲区包含一个深度至少为 16 位的缓冲区。
* **`failIfMajorPerformanceCaveat`**: 表明在一个系统性能低的环境是否创建该上下文的`boolean`值。
* **`powerPreference`**: 指示浏览器在运行 WebGL 上下文时使用相应的 GPU 电源配置。可能值如下：
    + `"default"`:自动选择，默认值。
    + `"high-performance"`: 高性能模式。
    + `"low-power"`: 节能模式。
* **`premultipliedAlpha`**: 表明排版引擎将假设绘制缓冲区包含预混合 alpha 通道的`boolean`值。
* **`preserveDrawingBuffer`**: 如果这个值为`true`缓冲区将不会被清除，会保存下来，直到被清除或被使用者覆盖。
* **`stencil`**: 表明绘制缓冲区包含一个深度至少为 8 位的模版缓冲区`boolean`值。

### 返回值

`RenderingContext` 返回值是下列之一：

* `CanvasRenderingContext2D` 为 `"2d"`,
* `WebGLRenderingContext` 为`"webgl"` 和`"experimental-webgl"`,
* `WebGL2RenderingContext` 为`"webgl2"` 和`"experimental-webgl2"`, 或者
* `ImageBitmapRenderingContext` 为`"bitmaprenderer"`.

如果 `contextType` 不是上述之一，返回`null`.

例子
--

定义 `<canvas>` 元素：

```html
<canvas id="canvas" width="300" height="300"></canvas>
```

通过如下代码可以获取 `canvas``2d` 上下文：

```js
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
console.log(ctx); // CanvasRenderingContext2D { ... }
```

现在你已经获取到了 2D 画布的渲染上下文 (`CanvasRenderingContext2D`)，可以使用它画你想画的了。

规范
--

| Specification |
| --- |
| HTML Standard # dom-canvas-getcontext-dev |

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

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

参见
--

* `HTMLCanvasElement`.
* `OffscreenCanvas.getContext()` (en-US)
* 可用的上下文：`CanvasRenderingContext2D`, `WebGLRenderingContext` 和 `WebGL2RenderingContext` and `ImageBitmapRenderingContext`.


HTMLCanvasElement.width - Web API 接口参考

HTMLCanvasElement.width
=======================

**`HTMLCanvasElement.width`** 属性是一个对应 `<canvas>` 元素 CSS 像素 `width` 的正整数。当这个属性没有指定时，或者被赋予一个不合法的值，比如一个负值，默认使用 300.

这是其中之一，另一个是 `HTMLCanvasElement.height`, 它们控制了 canvas 的大小尺寸。

语法
--
```

var pxl = canvas.width;
canvas.width = pxl;
```

示例
--

有这样一个 `<canvas>` 元素：

```html
<canvas id="canvas" width="300" height="300"></canvas>
```

你能够通过下面的代码得到 canvas 的宽度：

```js
var canvas = document.getElementById("canvas");
console.log(canvas.width); // 300
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-canvas-height |

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

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

查看更多
----

* The interface defining it, `HTMLCanvasElement`.
* 另一个控制 canvas 大小的属性，`HTMLCanvasElement.height`.


HTMLCanvasElement: webglcontextlost event - Web API 接口参考

HTMLCanvasElement: webglcontextlost event
=========================================

如果浏览器检测到与 `WebGLRenderingContext`对象关联的图形缓冲区已丢失，则会触发WebGL API 中的 **`webglcontextlost`** 事件。

|  |  |
| --- | --- |
| 冒泡 | Yes |
| 可取消 | Yes |
| 继承 | `WebGLContextEvent` |
| 事件处理程序属性 | none |

例子
--

在 `WEBGL_lose_context` 扩展的帮助下，你可以模拟 `webglcontextlost` 事件：

```js
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");

canvas.addEventListener("webglcontextlost", (event) => {
  console.log(event);
});

//WEBGL\_lose\_context 是 webgl 是属于 WebGLAPI 的一个扩展 API，它提供一组方法用来模拟一个 WebGLRenderingContext 上下文的丢失和恢复。
gl.getExtension("WEBGL\_lose\_context").loseContext();

// "webglcontextlost" event is logged.
```

规范
--

| Specification |
| --- |
| WebGL Specification # 5.15.2 |

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

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

请参阅
---

* `WebGLContextEvent`
* `WebGLRenderingContext.isContextLost()`
* `WEBGL_lose_context`, `WEBGL_lose_context.loseContext()`, `WEBGL_lose_context.restoreContext()`


HTMLCanvasElement.captureStream() - Web API 接口参考

HTMLCanvasElement.captureStream()
=================================

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

该 **`HTMLCanvasElement.captureStream()`** 方法返回的 `CanvasCaptureMediaStream` 是一个实时视频捕获的画布。

语法
--
```

MediaStream = canvas.captureStream(frameRate);
```

### 参数

`frameRate` 可选

设置双精准度浮点值为每个帧的捕获速率。如果未设置，则每次画布更改时都会捕获一个新帧。如果设置为`0`，则会捕获单个帧。

### 返回值

对一个 `MediaStream` 对象的引用。

例子
--

```js
//获取所需要截取媒体流的 canvas element
var canvasElt = document.querySelector("canvas");

//截取到媒体流
var stream = canvasElt.captureStream(25); // 25 FPS

//使用媒体流
// E.g.使用 RTCPeerConnection 来传输给其他的电脑
// 下面的 pc 是其他地方创建的一个 RTCPeerConnection
pc.addStream(stream);
```

规范
--

| Specification |
| --- |
| Media Capture from DOM Elements # dom-htmlcanvaselement-capturestream |

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

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

看看其他
----

* `CanvasCaptureMediaStream`,所属接口。
* `HTMLMediaElement.captureStream()` (en-US), 允许从一个媒体中获取流
* `MediaStream`
* `Media Capture and Streams API`


HTMLCanvasElement.mozOpaque - Web API 接口参考

HTMLCanvasElement.mozOpaque
===========================

**非标准:** 该特性是非标准的，请尽量不要在生产环境中使用它！

非标准的 **`HTMLCanvasElement.mozOpaque`** 是一种 `Boolean` 的数据反映了 `<canvas>` 元素的 `moz-opaque` HTML 属性。它能够让画布（canvas）将半透明作为一个参考因素。如果画布知道没有半透明因素，作画的性能可以优化。

当使用 `HTMLCanvasElement.getContext()` 创建绘图上下文时，该 api 将被标准化为将 alpha 选项设置为 false。应该避免使用 mozOpaque。Firefox 将在未来停止支持它。

语法
--
```

var opaque = canvas.mozOpaque;
canvas.mozOpaque = true;
```

示例
--

有如下 `<canvas>` 元素：

```html
<canvas id="canvas" width="300" height="300" moz-opaque></canvas>
```

你可以获取或设置 `mozOpaque` 属性。例如，当 mimeType == 'image/jpeg'或类似值时，可以将其属性值设置为 true，以在不需要半透明度的情况下提高应用程序的性能。

```js
var canvas = document.getElementById("canvas");
console.log(canvas.mozOpaque); // true
// 停用该方法
canvas.mozOpaque = false;
```

规范
--

不属于任何标准。

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

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

参考
--

* 定义它的接口，`HTMLCanvasElement`.
* `<canvas>` 元素的`moz-opaque`HTML 属性
* 为 Firefox OS 优化你的 JavaScript 游戏


HTMLCanvasElement.height - Web API 接口参考

HTMLCanvasElement.height
========================

**`HTMLCanvasElement.height`** 属性是一个正`整数` ，使用了`<canvas>` 元素的 HTML 属性`height`来反映该元素高度的 CSS 像素值。当该属性没有被定义，或被定义为一个无效值（如负值）时，将使用`150`作为它的默认值。

控制 canvas 元素大小的属性有两个，这是其中一个，还有一个是`HTMLCanvasElement.width`。

语法
--
```

var pxl = canvas.height;
canvas.height = pxl;
```

示例
--

给出这样一个`<canvas>` 元素：

```html
<canvas id="canvas" width="300" height="300"></canvas>
```

可以通过以下代码获得这个 canvas 元素的高度：

```js
var canvas = document.getElementById("canvas");
console.log(canvas.height); // 300
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-canvas-height |

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

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

其他
--

* 定义的接口，`HTMLCanvasElement`.
* 另一个控制 canvas 大小的属性，`HTMLCanvasElement.width`.


HTMLCanvasElement.toDataURL() - Web API 接口参考

HTMLCanvasElement.toDataURL()
=============================

**`HTMLCanvasElement.toDataURL()`** 方法返回一个包含图片展示的 data URI。可以使用 `type` 参数指定其类型，默认为 PNG 格式。图片的分辨率为 96dpi。

* 如果画布的高度或宽度是 0，那么会返回字符串“`data:,`”。
* 如果传入的类型非“`image/png`”，但是返回的值以“`data:image/png`”开头，那么该传入的类型是不支持的。
* Chrome 支持“`image/webp`”类型。
语法
--
```

canvas.toDataURL(type, encoderOptions);
```

### 参数

`type` 可选

图片格式，默认为 `image/png`

`encoderOptions` 可选

在指定图片格式为 `image/jpeg` 或 `image/webp` 的情况下，可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围，将会使用默认值 `0.92`。其他参数会被忽略。

### 返回值

包含 data URI 的字符串。

示例
--

有如下`<canvas>`元素

```html
<canvas id="canvas" width="5" height="5"></canvas>
```

可以用这样的方式获取一个 data-URL

```js
var canvas = document.getElementById("canvas");
var dataURL = canvas.toDataURL();
console.log(dataURL);
// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby
// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"
```

### 设置 jpegs 图片的质量

```js
var fullQuality = canvas.toDataURL("image/jpeg", 1.0);
// data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...9oADAMBAAIRAxEAPwD/AD/6AP/Z"
var mediumQuality = canvas.toDataURL("image/jpeg", 0.5);
var lowQuality = canvas.toDataURL("image/jpeg", 0.1);
```

### 示例：动态更改图片

可以使用鼠标事件来动态改变图片（这个例子中改变图片灰度）。

#### HTML

```html
<img class="grayscale" src="myPicture.png" alt="Description of my picture" />
```

#### JavaScript

```js
window.addEventListener("load", removeColors);

function showColorImg() {
  this.style.display = "none";
  this.nextSibling.style.display = "inline";
}

function showGrayImg() {
  this.previousSibling.style.display = "inline";
  this.style.display = "none";
}

function removeColors() {
  var aImages = document.getElementsByClassName("grayscale"),
    nImgsLen = aImages.length,
    oCanvas = document.createElement("canvas"),
    oCtx = oCanvas.getContext("2d");
  for (
    var nWidth, nHeight, oImgData, oGrayImg, nPixel, aPix, nPixLen, nImgId = 0;
    nImgId < nImgsLen;
    nImgId++
  ) {
    oColorImg = aImages[nImgId];
    nWidth = oColorImg.offsetWidth;
    nHeight = oColorImg.offsetHeight;
    oCanvas.width = nWidth;
    oCanvas.height = nHeight;
    oCtx.drawImage(oColorImg, 0, 0);
    oImgData = oCtx.getImageData(0, 0, nWidth, nHeight);
    aPix = oImgData.data;
    nPixLen = aPix.length;
    for (nPixel = 0; nPixel < nPixLen; nPixel += 4) {
      aPix[nPixel + 2] =
        aPix[nPixel + 1] =
        aPix[nPixel] =
          (aPix[nPixel] + aPix[nPixel + 1] + aPix[nPixel + 2]) / 3;
    }
    oCtx.putImageData(oImgData, 0, 0);
    oGrayImg = new Image();
    oGrayImg.src = oCanvas.toDataURL();
    oGrayImg.onmouseover = showColorImg;
    oColorImg.onmouseout = showGrayImg;
    oCtx.clearRect(0, 0, nWidth, nHeight);
    oColorImg.style.display = "none";
    oColorImg.parentNode.insertBefore(oGrayImg, oColorImg);
  }
}
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-canvas-todataurl-dev |

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

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

参考
--

* 定义接口，`HTMLCanvasElement`
* HTTP 参考中的 Data URI


TextDecoderStream.fatal - Web API 接口参考

TextDecoderStream.fatal
=======================

`TextDecoderStream` 接口的只读属性 **`fatal`** 是一个 `boolean`，表示 `TextDecoderStream` 对象的错误模式是否设置为 `fatal`。

两个可能的错误模式为 `fatal` 或者 `replacement`，默认是 `replacement`，它将一个替换字符 `U+FFFD`（�）推送到输出。

值
-

一个 `boolean`。如果错误模式设置为 `fatal`，返回 `true`。否则它返回 `false`。

示例
--

```js
stream = new TextDecoderStream();
console.log(stream.fatal); // returns false
```

规范
--

| Specification |
| --- |
| Encoding Standard # dom-textdecoder-fatal |

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

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


TextDecoderStream.writable - Web API 接口参考

TextDecoderStream.writable
==========================

`TextDecoderStream` 接口的只读属性 **`writable`** 返回一个 `WritableStream`。

值
-

一个 `WritableStream`。

示例
--

从一个 `TextDecoderStream` 返回一个 `WritableStream`。

```js
stream = new TextDecoderStream();
console.log(stream.writeable); //a WritableStream
```

规范
--

| Specification |
| --- |
| Streams Standard # dom-generictransformstream-writable |

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

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


TextDecoderStream.readable - Web API 接口参考

TextDecoderStream.readable
==========================

`TextDecoderStream` 接口的只读属性 **`readable`** 返回一个 `ReadableStream`。

值
-

一个 `ReadableStream`。

示例
--

这个示例展示了如何从一个 `TextDecoderStream` 返回一个 `ReadableStream`。

```js
stream = new TextDecoderStream();
console.log(stream.readable); //a ReadableStream
```

规范
--

| Specification |
| --- |
| Streams Standard # dom-generictransformstream-readable |

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

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


TextDecoderStream() - Web API 接口参考

TextDecoderStream()
===================

**`TextDecoderStream()`** 构造函数创建一个新的 `TextDecoderStream` 对象，该对象用于将二进制编码的文本流转换为字符串。

语法
--

```js
new TextDecoderStream(label)
new TextDecoderStream(label, options)
```

### 参数

`label`

默认为 `utf-8` 的字符串。可以是任意有效的编码。

`options` 可选

一个具有属性的 `TextDecoderOptions` 对象：

`fatal`

一个布尔值，表示错误的模式。如果是 true，则在 decoder 遇到错误时抛出一个 `DOMException`。默认值是 `false`。

示例
--

以下示例演示如何从一个 `fetch()` 中获取并解码二进制数据。如果没有传递任何 `label`，数据的解码类型为 UTF-8。

```js
const response = await fetch("https://example.com");
const stream = response.body.pipeThrough(new TextDecoderStream());
```

规范
--

| Specification |
| --- |
| Encoding Standard # ref-for-dom-textdecoderstream① |

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

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


TextDecoderStream.ignoreBOM - Web API 接口参考

TextDecoderStream.ignoreBOM
===========================

`TextDecoderStream` 接口的只读属性 **`ignoreBOM`** 返回一个 `boolean`，表示是否忽略字节顺序标记（BOM）。

值
-

一个 `boolean`，初始值是 `false`。

示例
--

```js
stream = new TextDecoderStream();
console.log(stream.ignoreBOM); // returns false
```

规范
--

| Specification |
| --- |
| Encoding Standard # textdecoder-ignore-bom-flag |

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

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


TextDecoderStream.encoding - Web API 接口参考

TextDecoderStream.encoding
==========================

`TextDecoderStream` 接口的只读属性 **`encoding`** 返回一个字符串，其中包含指定的 encoder 使用的编码算法的名称。

值
-

一个小写的 ASCII 字符串。

示例
--

从一个 `TextDecoderStream` 返回 `encoding` 的值。

```js
stream = new TextDecoderStream();
console.log(stream.encoding); // returns the default "utf-8"
```

规范
--

| Specification |
| --- |
| Encoding Standard # dom-textdecoder-encoding |

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

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


使用 getUserMedia() 拍摄静态照片 - Web API 接口参考

使用 getUserMedia() 拍摄静态照片
========================

本文介绍在支持 `getUserMedia()` 的计算机或手机上如何使用 `navigator.mediaDevices.getUserMedia()` 访问摄像机，并用其拍照。

![getUserMedia-based image capture app — on the left we have a video stream taken from a webcam and a take photo button, on the right we have the still image output from taking the photo](/zh-CN/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos/web-rtc-demo.png)

如果你喜欢，你也可以直接跳转到演示。

HTML 标记
-------

我们的 HTML 界面有两个主要的操作部分：流和捕获面板以及演示面板。它们俩都在它们自己的 `<div>` 中并排渲染，以便于添加样式和控制。

左边的面板包含两个组件：一个 `<video>` 元素，它将接收来自 `navigator.mediaDevices.getUserMedia()` 的流，以及用于用户点击以捕获视频帧的 `<button>`。

```html
<div class="camera">
  <video id="video">视频流目前不可用。</video>
  <button id="startbutton">拍摄照片</button>
</div>
```

这很简单，当我们进入 JavaScript 代码时，我们将看到它们是如何紧密联系在一起的。

接下来，我们有一个 `<canvas>` 元素，捕获的帧被存储到其中，可能以某种方式进行操作，然后转换为输出图像文件。通过使用样式 `display``:none` 将画布保持隐藏，以避免画面的混乱——用户不需要看到这个中间过程。

我们还有一个 `<img>` 元素，我们将在其中绘制图像——这是让用户看到的最终显示。

```html
<canvas id="canvas"> </canvas>
<div class="output">
  <img id="photo" alt="捕获的图像会显示在这里。" />
</div>
```

这是所有相关的 HTML。其余的只是一些页面布局和提供一个返回页面链接的些许文本。

JavaScript 代码
-------------

现在来看看 JavaScript 代码。我们将把它分解成几个小的部分，使其更容易解释。

### 初始化

我们首先将整个脚本包装在匿名函数中，以避免使用全局变量，然后设置我们将要使用的各种变量。

```js
(() => {
  const width = 320;    // 外面会对照片的宽度进行缩放
  const height = 0;     // 高度会基于输入的视频流进行计算

  const streaming = false;

  let video = null;
  let canvas = null;
  let photo = null;
  let startbutton = null;
```

这些变量分别是：

`width`

无论输入视频的尺寸如何，我们将把所得到的图像缩放到宽度为 320 像素。

`height`

给定流的 `width` 和宽高比，计算出图像的输出高度。

`streaming`

指示当前是否有活动的视频流正在运行。

`video`

这将是页面加载完成后对 `<video>` 元素的引用。

`canvas`

这将是页面加载完成后对 `<canvas>` 元素的引用。

`photo`

这将在页面加载完成后引用 `<img>` 元素。

`startbutton`

这将引用用于触发捕获的 `<button>` 元素。我们会在页面加载完成之后得到。

### startup() 函数

当页面加载完成时，提供给 `EventTarget.addEventListener` 的 `startup()` 函数将会运行。此函数的作用是请求访问用户的网络摄像头，将用于输出的 `<img>` 初始化为默认状态，并建立从相机接收每帧视频所需的事件监听器，并在点击按钮捕获图像时作出反应。

#### 获取元素引用

首先，我们参考我们需要访问的主要内容。

```js
  function startup() {
    video = document.getElementById('video');
    canvas = document.getElementById('canvas');
    photo = document.getElementById('photo');
    startbutton = document.getElementById('startbutton');
```

#### 获取流媒体

接下来的任务是获取媒体流：

```js
navigator.mediaDevices
  .getUserMedia({ video: true, audio: false })
  .then((stream) => {
    video.srcObject = stream;
    video.play();
  })
  .catch((err) => {
    console.error(`An error occurred: ${err}`);
  });
```

在这里，我们调用 `MediaDevices.getUserMedia()` 并请求视频流（无音频）。它返回一个 promise，我们给它附加成功和失败情况下的回调方法。

成功回调接收一个 stream 对象作为输入。它是新视频的 `<video>` 元素的源。

一旦流被链接到 `<video>` 元素，我们通过调用 `HTMLMediaElement.play()` 开始播放。

如果打开流失败，则调用失败回调函数。在没有连接兼容的相机，或者用户拒绝访问时，则会发生这种情况。

### 监听视频开始播放

在 `<video>` 上调用 `HTMLMediaElement.play()` 之后，在视频流开始流动之前，有一段（希望简短）的时间段过去了。为了避免在此之前一直阻塞，我们为 `video` 加上一个 `canplay` 事件的监听器，当视频播放实际开始时会触发该事件。那时，视频对象中的所有属性都已基于流的格式进行配置。

```js
video.addEventListener(
  "canplay",
  (ev) => {
    if (!streaming) {
      height = (video.videoHeight / video.videoWidth) \* width;

      video.setAttribute("width", width);
      video.setAttribute("height", height);
      canvas.setAttribute("width", width);
      canvas.setAttribute("height", height);
      streaming = true;
    }
  },
  false,
);
```

这个回调什么都不做，除非它是第一次被调用；这是通过查看我们的 `streaming` 变量的值进行测试，这是第一次运行此方法时为 `false`。

如果这是第一次运行，我们会根据视频的实际大小，`video.videoWidth` 和要渲染视频宽度的宽度（`witdh`）之间的大小差异来设置视频的高度。

最后，通过在视频和画布上调用 `Element.setAttribute()` 来设置视频和画布的宽度（`witdh`）和高度（`height`），以使得两者相互匹配。最后，我们将 `streaming` 变量设置为 `true`，以防止我们无意中再次运行此设置代码。

#### 处理按钮上的点击

为了在每次用户点击 `startbutton` 时捕获静态照片，我们需要向按钮添加一个事件监听器，以便在发出 `click` 事件时被调用：

```js
startbutton.addEventListener(
  "click",
  (ev) => {
    takepicture();
    ev.preventDefault();
  },
  false,
);
```

这个方法很简单：它只是调用我们的 `takepicture()` 函数，在从流中捕获帧的部分中定义，然后在接收的事件上调用 `Event.preventDefault()`，以防止点击被多次处理。

#### 包装 startup() 方法

`startup()` 方法中只有两行代码：

```js
    clearphoto();
  }
```

这就是我们调用 `clearphoto()` 方法的地方，我们将在下面的清理照片框部分进行描述。

### 清理照片框

清理照片框包括创建一个图像，然后将其转换为可以显示最近捕获的帧的 `<img>` 元素使用的格式。该代码如下所示：

```js
function clearphoto() {
  const context = canvas.getContext("2d");
  context.fillStyle = "#AAA";
  context.fillRect(0, 0, canvas.width, canvas.height);

  const data = canvas.toDataURL("image/png");
  photo.setAttribute("src", data);
}
```

我们首先得到对我们用于屏幕外渲染的隐藏的 `<canvas>` 元素的引用。接下来，我们将 `fillStyle` 设置为 `#AAA`（相当浅的灰色），并通过调用 `fillRect()` 来填充整个画布。

最后在此功能中，我们将画布转换为 PNG 图像，并调用 `photo.setAttribute()` 以使我们捕获的静止框显示图像。

### 从流中捕获帧

最后一个定义的功能是整个练习的重点：`takepicture()` 函数，其捕获当前显示的视频帧的作业将其转换为 PNG 文件，并将其显示在捕获的帧框中。代码如下所示：

```js
function takepicture() {
  const context = canvas.getContext("2d");
  if (width && height) {
    canvas.width = width;
    canvas.height = height;
    context.drawImage(video, 0, 0, width, height);

    const data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);
  } else {
    clearphoto();
  }
}
```

正如我们需要处理画布内容的情况一样，我们首先得到隐藏画布的 `2D 绘图上下文`。

然后，如果宽度和高度都是非零（意味着至少有潜在有效的图像数据），我们将画布的宽度和高度设置为与捕获帧的宽度和高度相匹配，然后调用 `drawImage()` 将视频的当前帧绘制到上下文中，用帧图像填充整个画布。

**备注：** 这可以利用 `HTMLVideoElement` 接口看起来像任何接受 `HTMLImageElement` 作为参数的 API 的 `HTMLImageElement`，将视频的当前帧渲染为图像的内容。

一旦画布包含捕获的图像，我们通过调用它的 `HTMLCanvasElement.toDataURL()` 将它转换为 PNG 格式; 最后，我们调用 `photo.setAttribute()` 来使我们捕获的静态框显示图像。

如果没有可用的有效图像（即宽度和高度均为 0），则通过调用 `clearphoto()` 清除捕获帧框的内容。

演示
--

### HTML

```html
<div class="contentarea">
  <h1>MDN——navigator.mediaDevices.getUserMedia(): 静态照片拍摄演示</h1>
  <p>
    此示例演示了如何使用内置的网络摄像头来获取媒体流，并从中获取图像，以使用该图像来创建一个
    PNG 图像。
  </p>
  <div class="camera">
    <video id="video">视频流目前不可用。</video>
    <button id="startbutton">拍摄照片</button>
  </div>
  <canvas id="canvas"> </canvas>
  <div class="output">
    <img id="photo" alt="捕获的图像会显示在这里。" />
  </div>
  <p>
    访问我们的文章：<a
 href="https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC\_API/Taking\_still\_photos">
      使用 getUserMedia() 拍摄静态照片</a
 >以详细了解此处使用的技术。
  </p>
</div>
```

### CSS

```css
#video {
  border: 1px solid black;
  box-shadow: 2px 2px 3px black;
  width: 320px;
  height: 240px;
}

#photo {
  border: 1px solid black;
  box-shadow: 2px 2px 3px black;
  width: 320px;
  height: 240px;
}

#canvas {
  display: none;
}

.camera {
  width: 340px;
  display: inline-block;
}

.output {
  width: 340px;
  display: inline-block;
  vertical-align: top;
}

#startbutton {
  display: block;
  position: relative;
  margin-left: auto;
  margin-right: auto;
  bottom: 32px;
  background-color: rgba(0, 150, 0, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.2);
  font-size: 14px;
  font-family: "Lucida Grande", "Arial", sans-serif;
  color: rgba(255, 255, 255, 1);
}

.contentarea {
  font-size: 16px;
  font-family: "Lucida Grande", "Arial", sans-serif;
  width: 760px;
}
```

### JavaScript

```js
(() => {
  // The width and height of the captured photo. We will set the
  // width to the value defined here, but the height will be
  // calculated based on the aspect ratio of the input stream.

  const width = 320; // We will scale the photo width to this
  let height = 0; // This will be computed based on the input stream

  // |streaming| indicates whether or not we're currently streaming
  // video from the camera. Obviously, we start at false.

  let streaming = false;

  // The various HTML elements we need to configure or control. These
  // will be set by the startup() function.

  let video = null;
  let canvas = null;
  let photo = null;
  let startbutton = null;

  function showViewLiveResultButton() {
    if (window.self !== window.top) {
      // Ensure that if our document is in a frame, we get the user
      // to first open it in its own tab or window. Otherwise, it
      // won't be able to request permission for camera access.
      document.querySelector(".contentarea").remove();
      const button = document.createElement("button");
      button.textContent = "查看以上示例代码的实时演示";
      document.body.append(button);
      button.addEventListener("click", () => window.open(location.href));
      return true;
    }
    return false;
  }

  function startup() {
    if (showViewLiveResultButton()) {
      return;
    }
    video = document.getElementById("video");
    canvas = document.getElementById("canvas");
    photo = document.getElementById("photo");
    startbutton = document.getElementById("startbutton");

    navigator.mediaDevices
      .getUserMedia({ video: true, audio: false })
      .then((stream) => {
        video.srcObject = stream;
        video.play();
      })
      .catch((err) => {
        console.error(`An error occurred: ${err}`);
      });

    video.addEventListener(
      "canplay",
      (ev) => {
        if (!streaming) {
          height = video.videoHeight / (video.videoWidth / width);

          // Firefox currently has a bug where the height can't be read from
          // the video, so we will make assumptions if this happens.

          if (isNaN(height)) {
            height = width / (4 / 3);
          }

          video.setAttribute("width", width);
          video.setAttribute("height", height);
          canvas.setAttribute("width", width);
          canvas.setAttribute("height", height);
          streaming = true;
        }
      },
      false,
    );

    startbutton.addEventListener(
      "click",
      (ev) => {
        takepicture();
        ev.preventDefault();
      },
      false,
    );

    clearphoto();
  }

  // Fill the photo with an indication that none has been
  // captured.

  function clearphoto() {
    const context = canvas.getContext("2d");
    context.fillStyle = "#AAA";
    context.fillRect(0, 0, canvas.width, canvas.height);

    const data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);
  }

  // Capture a photo by fetching the current contents of the video
  // and drawing it into a canvas, then converting that to a PNG
  // format data URL. By drawing it on an offscreen canvas and then
  // drawing that to the screen, we can change its size and/or apply
  // other changes before drawing it.

  function takepicture() {
    const context = canvas.getContext("2d");
    if (width && height) {
      canvas.width = width;
      canvas.height = height;
      context.drawImage(video, 0, 0, width, height);

      const data = canvas.toDataURL("image/png");
      photo.setAttribute("src", data);
    } else {
      clearphoto();
    }
  }

  // Set up our event listener to run the startup process
  // once loading is complete.
  window.addEventListener("load", startup, false);
})();
```

### 结果

过滤器的乐趣
------

由于我们通过从 `<video>` 元素中抓取帧来捕获用户网络摄像头的图像，因此我们可以非常轻松地将过滤器和有趣的效果应用于视频。事实证明，使用 `filter` 属性应用于元素的任何 CSS 过滤器都会影响捕获的照片。这些过滤器可以从简单（使图像黑白）到复杂（高斯模糊和色调旋转）。

你可以使用例如 Firefox 开发者工具的样式编辑器来播放此效果；有关如何执行此操作的详细信息，请参阅编辑 CSS 过滤器。

使用特定设备
------

如果需要，你可以将允许的视频源限定为特定的设备或特定的一组设备。要做到这一点，请调用 `MediaDevices.enumerateDevices`。若返回的 promise 兑现了一个 `MediaDeviceInfo` (en-US) 对象（描述了可用的设备）数组，可以从中选取一个你想要允许的设备，并将对应的 `deviceId` (en-US) 或 `MediaTrackConstraints` 对象的 `deviceId` 作为参数传入到 `getUserMedia()` 中。

参见
--

* GitHub 上的示例代码
* `MediaDevices.getUserMedia`
* Canvas 教程中的使用视频帧
* `CanvasRenderingContext2D.drawImage()`


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

MediaStreamAudioSourceNode.MediaStreamAudioSourceNode()
=======================================================

\*\*`MediaStreamAudioSourceNode()`\*\*构造器创建一个新的 `MediaStreamAudioSourceNode`对象实例。

语法
--

```js
var myAudioSource = new MediaStreamAudioSourceNode(context, options);
```

### 参数

*context*

一个用来使用 node 的音频环境`AudioContext`。

*options*

`MediaStreamAudioSourceOptions`，一个 map 对象，定义`MediaStreamAudioSourceNode`的属性：

* `mediaStream`: 需要使用的音频流

示例
--

```js
// define variables
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();

// getUserMedia block - grab stream 获取音频流
// put it into a 把音频流放入 MediaStreamAudioSourceNode
if (navigator.mediaDevices.getUserMedia) {
  console.log("new getUserMedia supported.");
  navigator.mediaDevices
    .getUserMedia(
      // constraints: audio and video for this app
      {
        audio: true,
        video: false,
      },
    )
    .then(function (stream) {
      // Create a MediaStreamAudioSourceNode
      var options = {
        mediaStream: stream,
      };

      var source = new MediaStreamAudioSourceNode(audioCtx, options);
      source.connect(audioCtx.destination);
    })
    .catch(function (err) {
      console.log("The following gUM error occured: " + err);
    });
} else {
  console.log("new getUserMedia not supported on your browser!");
}
```

规范
--

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

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

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


FileSystemSyncAccessHandle：close() 方法 - Web API 接口参考

FileSystemSyncAccessHandle：close() 方法
=====================================

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

`FileSystemSyncAccessHandle` 接口的 **`close()`** 方法用于关闭一个打开的同步文件句柄，禁止之后对其的任何操作并且释放之前加在与文件句柄相关联的文件上的独占锁。

**备注：** 在规范早期版本中，`close()`、`flush()`、`getSize()` 和 `truncate()` 被错误地规定为异步方法。某些浏览器的旧版本是依照异步方式来实现这些方法的，不过，现今所有支持这些方法的浏览器都将它们实现为同步方法。

语法
--

```js
close()
```

### 参数

无。

### 返回值

无（`undefined`）。

### 异常

无。

示例
--

下面的异步事件处理函数包含在一个 Web Worker 中。当收到来自主线程的消息时，它将执行：

* 创建一个同步文件访问句柄。
* 获取文件的大小然后创建一个 `ArrayBuffer` 来容纳其内容。
* 将文件内容读取到缓冲区。
* 将收到的消息编码然后写入到文件的末尾。
* 将更改保存到磁盘然后关闭访问句柄。

```js
onmessage = async (e) => {
  // 接收从主线程发来的消息
  const message = e.data;

  // 获取草稿文件的句柄
  const root = await navigator.storage.getDirectory();
  const draftHandle = await root.getFileHandle("draft.txt", { create: true });
  // 获取同步访问句柄
  const accessHandle = await draftHandle.createSyncAccessHandle();

  // 获取文件的大小
  const fileSize = accessHandle.getSize();
  // 读取文件内容到缓冲区
  const buffer = new DataView(new ArrayBuffer(fileSize));
  const readBuffer = accessHandle.read(buffer, { at: 0 });

  // 将消息写入文件末尾
  const encoder = new TextEncoder();
  const encodedMessage = encoder.encode(message);
  const writeBuffer = accessHandle.write(encodedMessage, { at: readBuffer });

  // 将更改持久化至磁盘
  accessHandle.flush();

  // 用完 FileSystemSyncAccessHandle 后记得把它关闭
  accessHandle.close();
};
```

规范
--

| Specification |
| --- |
| File System Standard # api-filesystemsyncaccesshandle-close |

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

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

参见
--

* 文件系统 API
* 文件系统访问 API：简化本地文件访问


FileSystemSyncAccessHandle：read() 方法 - Web API 接口参考

FileSystemSyncAccessHandle：read() 方法
====================================

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

`FileSystemSyncAccessHandle` 接口的 **`read()`** 方法用于将与句柄相关联文件的内容读取到指定的缓冲区中，可选择在给定的偏移处开始读取。

语法
--

```js
read(buffer, options)
```

### 参数

`buffer`

一个表示要将文件内容读取到的缓冲区的 `ArrayBuffer` 或者 `ArrayBufferView` 对象（例如一个 `DataView`）。注意：你无法直接操作 `ArrayBuffer` 中的内容，应该先创建一个像 `Int8Array` 或者 `DataView` 这样的类型化数组对象，用来把缓冲区表示成特定的格式，然后再用类型化数组对象来读写缓冲区的内容。

`options` 可选

一个包含以下属性的选项对象：

`at`

一个表示应当从文件何处开始读取的字节偏移量数字。

### 返回值

一个表示从文件读取的字节数的数字。

### 异常

`InvalidStateError` `DOMException`

如果相关的访问句柄已经被关闭，抛出此异常。

`TypeError`

如果底层文件系统不支持从指定的偏移处开始读取文件，抛出此错误。

示例
--

下面的异步事件处理函数包含在一个 Web Worker 中。当收到来自主线程的消息时，它将执行：

* 创建一个同步文件访问句柄。
* 获取文件的大小然后创建一个 `ArrayBuffer` 来容纳其内容。
* 将文件内容读取到缓冲区。
* 将收到的消息编码然后写入到文件的末尾。
* 将更改保存到磁盘然后关闭访问句柄。

```js
onmessage = async (e) => {
  // 接收从主线程发来的消息
  const message = e.data;

  // 获取草稿文件的句柄
  const root = await navigator.storage.getDirectory();
  const draftHandle = await root.getFileHandle("draft.txt", { create: true });
  // 获取同步访问句柄
  const accessHandle = await draftHandle.createSyncAccessHandle();

  // 获取文件的大小
  const fileSize = accessHandle.getSize();
  // 读取文件内容到缓冲区
  const buffer = new DataView(new ArrayBuffer(fileSize));
  const readBuffer = accessHandle.read(buffer, { at: 0 });

  // 将消息写入文件末尾
  const encoder = new TextEncoder();
  const encodedMessage = encoder.encode(message);
  const writeBuffer = accessHandle.write(encodedMessage, { at: readBuffer });

  // 将更改持久化至磁盘
  accessHandle.flush();

  // 用完 FileSystemSyncAccessHandle 后记得把它关闭
  accessHandle.close();
};
```

**备注：** 在规范早期版本中，`close()`、`flush()`、`getSize()` 和 `truncate()` 被错误地规定为异步方法。某些浏览器的旧版本是依照异步方式来实现这些方法的，不过，现今所有支持这些方法的浏览器都将它们实现为同步方法。

规范
--

| Specification |
| --- |
| File System Standard # api-filesystemsyncaccesshandle-read |

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

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

参见
--

* 文件系统 API
* 文件系统访问 API：简化本地文件访问


FileSystemSyncAccessHandle：truncate() 方法 - Web API 接口参考

FileSystemSyncAccessHandle：truncate() 方法
========================================

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

`FileSystemSyncAccessHandle` 接口的 **`truncate()`** 方法用于将与句柄相关联文件的大小调整为指定的字节数。

**备注：** 在规范早期版本中，`close()`、`flush()`、`getSize()` 和 `truncate()` 被错误地规定为异步方法。某些浏览器的旧版本是依照异步方式来实现这些方法的，不过，现今所有支持这些方法的浏览器都将它们实现为同步方法。

语法
--

```js
truncate(newSize)
```

### 参数

`newSize`

要将文件调整到的字节数。

### 返回值

无（`undefined`）。

### 异常

`InvalidStateError` `DOMException`

如果相关的访问句柄已经被关闭，或者由于其他原因导致修改文件的二进制数据失败，抛出此异常。

`QuotaExceededError` `DOMException`

如果 `newSize` 大于文件原来的大小并且超出了浏览器的存储配额，抛出此异常。

`TypeError`

如果底层文件系统不支持将文件大小设为新的大小，抛出此错误。

Examples
--------

```js
async function truncateFile() {
  // 获取草稿文件的句柄
  const root = await navigator.storage.getDirectory();
  const draftHandle = await root.getFileHandle("draft.txt", { create: true });
  // 获取同步访问句柄
  const accessHandle = await draftHandle.createSyncAccessHandle();

  // 将文件裁剪至 0 字节
  accessHandle.truncate(0);

  // 将更改持久化至磁盘
  accessHandle.flush();

  // 用完 FileSystemSyncAccessHandle 后记得把它关闭
  accessHandle.close();
}
```

规范
--

| Specification |
| --- |
| File System Standard # api-filesystemsyncaccesshandle-truncate |

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

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

参见
--

* 文件系统 API
* 文件系统访问 API：简化本地文件访问


FileSystemSyncAccessHandle：getSize() 方法 - Web API 接口参考

FileSystemSyncAccessHandle：getSize() 方法
=======================================

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

`FileSystemSyncAccessHandle` 接口的 **`getSize()`** 方法用于返回与句柄相关联文件的字节大小。

**备注：** 在规范早期版本中，`close()`、`flush()`、`getSize()` 和 `truncate()` 被错误地规定为异步方法。某些浏览器的旧版本是依照异步方式来实现这些方法的，不过，现今所有支持这些方法的浏览器都将它们实现为同步方法。

语法
--

```js
getSize()
```

### 参数

无。

### 返回值

一个表示文件字节大小的数字。

### 异常

`InvalidStateError` `DOMException`

如果相关的访问句柄已经被关闭，抛出此异常。

示例
--

下面的异步事件处理函数包含在一个 Web Worker 中。当收到来自主线程的消息时，它将执行：

* 创建一个同步文件访问句柄。
* 获取文件的大小然后创建一个 `ArrayBuffer` 来容纳其内容。
* 将文件内容读取到缓冲区。
* 将收到的消息编码然后写入到文件的末尾。
* 将更改保存到磁盘然后关闭访问句柄。

```js
onmessage = async (e) => {
  // 接收从主线程发来的消息
  const message = e.data;

  // 获取草稿文件的句柄
  const root = await navigator.storage.getDirectory();
  const draftHandle = await root.getFileHandle("draft.txt", { create: true });
  // 获取同步访问句柄
  const accessHandle = await draftHandle.createSyncAccessHandle();

  // 获取文件的大小
  const fileSize = accessHandle.getSize();
  // 读取文件内容到缓冲区
  const buffer = new DataView(new ArrayBuffer(fileSize));
  const readBuffer = accessHandle.read(buffer, { at: 0 });

  // 将消息写入文件末尾
  const encoder = new TextEncoder();
  const encodedMessage = encoder.encode(message);
  const writeBuffer = accessHandle.write(encodedMessage, { at: readBuffer });

  // 将更改持久化至磁盘
  accessHandle.flush();

  // 用完 FileSystemSyncAccessHandle 后记得把它关闭
  accessHandle.close();
};
```

规范
--

| Specification |
| --- |
| File System Standard # api-filesystemsyncaccesshandle-getsize |

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

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

参见
--

* 文件系统 API
* 文件系统访问 API：简化本地文件访问