HTML5-权威指南-十一-

70 阅读41分钟

HTML5 权威指南(十一)

原文:The Definitive Guide to HTML5

协议:CC BY-NC-SA 4.0

#三十六、使用画布元素——第二部分

在这一章中,我将继续描述canvas元素的特性,展示如何绘制更复杂的形状(包括弧线和曲线),如何使用裁剪区域限制操作,以及如何绘制文本。我还描述了我们可以应用到画布上的效果和变换,包括阴影、透明、旋转和平移。表 36-1 对本章进行了总结。

Image

Image

使用路径绘图

第三十五章中的例子都依赖于我们画矩形的能力。矩形是一种有用的形状,但并不总是必需的。幸运的是,canvas 元素及其上下文提供了一组方法,允许我们使用路径来绘制形状。路径本质上是一组单独的线(称为子路径),它们累积起来形成一个形状。我们绘制子路径就像用笔在纸上画画一样,不需要将笔尖从页面上抬起来——每个子路径都从画布上最后一个子路径结束的点开始。表 2 显示了可用于绘制基本路径的方法。

Image

绘制路径的基本顺序如下:

  • 调用beginPath方法
  • 使用moveTo方法移动到开始位置
  • arclineTo等方法画出子路径。
  • 可选地调用closePath方法
  • 调用fillstoke方法

在接下来的小节中,我将向您展示如何将这个序列用于不同的子路径方法。

用线条绘制路径

最简单的路径是由直线组成的。清单 36-1 提供了一个演示。

清单 36-1。从直线创建路径

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                               ctx.strokeStyle = "black";             ctx.lineWidth = 4;

**            ctx.beginPath();** **            ctx.moveTo(10, 10);** **            ctx.lineTo(110, 10);** **            ctx.lineTo(110, 120);** **            ctx.closePath();** **            ctx.fill();**

**            ctx.beginPath();** **            ctx.moveTo(150, 10);** **            ctx.lineTo(200, 10);** **            ctx.lineTo(200, 120);** **            ctx.lineTo(190, 120);**

            ctx.fill(); **            ctx.stroke();**

            ctx.beginPath();             ctx.moveTo(250, 10);             ctx.lineTo(250, 120);             ctx.stroke();              

`

在这个例子中,我创建了三条路径。你可以在图 36-1 中看到它们是如何出现在画布上的。

Image

图 36-1。使用 lineTo 方法创建简单路径

对于第一条路径,我明确地画了两条线,然后使用了closePath方法。画布将关闭路径。然后我调用fill方法用fillStyle属性指定的样式填充形状(我在这个例子中使用了纯色,但是我们可以使用第三十五章中描述的任何渐变和图案)。

对于第二个形状,我指定了三个子路径,但没有关闭形状。你可以看到我同时调用了fillstroke方法,用颜色填充形状并沿着路径画一条线。请注意,填充颜色的绘制就像形状是闭合的一样。canvas 元素假设从最后一个点到第一个点的子路径,并使用它来填充形状。相比之下,stroke方法只遵循已经定义好的子路径。

Image 提示对于第二个形状,我在stroke方法之前调用了fill方法,这使得画布用纯色填充形状,然后沿着路径画一条线。如果lineWidth属性大于1,我们会得到不同的视觉效果,我们首先调用stroke方法。较宽的线条绘制在路径的两侧,因此当 fill 方法被调用时,部分线条被该方法覆盖,有效地缩小了线条宽度。

对于第三个形状,我只是在两点之间画了一条线,因为路径不必有多个子路径。当我们画线或保持形状打开时,我们可以使用lineCap属性来设置线条终止的样式。该属性的三个允许值是:buttround,square(默认为 butt)。清单 36-2 展示了这个属性及其使用中的每个值。

清单 36-2。设置lineCap属性

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  

            ctx.strokeStyle = "black";             ctx.lineWidth = 40;

            var xpos = 50;             var styles = ["butt", "round", "square"];             for (var i = 0; i < styles.length; i++) {                 ctx.beginPath();                 ctx.lineCap = styles[i];                 ctx.moveTo(xpos, 50);                 ctx.lineTo(xpos, 150);                 ctx.stroke();                 xpos += 50;             }              

`

本例中的脚本为每种样式画了一条非常粗的线。我还添加了一条引导线来演示roundsquare样式被绘制在线尾之外,如图图 36-2 所示。

Image

图 36-2。三种线帽风格

绘制矩形

rect方法向当前路径添加一个矩形子路径。如果你需要一个独立的矩形,那么第三十五章中描述的fillRectstrokeRect方法更合适。当你需要添加一个矩形到一个更复杂的形状时,rect方法很有用,如清单 36-3 所示。

清单 36-3。用 rect 方法画矩形

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                               ctx.strokeStyle = "black"; **            ctx.lineWidth = 4;**

            ctx.beginPath();             ctx.moveTo(110, 10);

            ctx.lineTo(110, 100);             ctx.lineTo(10, 10);             ctx.closePath();

            ctx.rect(110, 10, 100, 90);             ctx.rect(110, 100, 130, 30);

            ctx.fill();             ctx.stroke();              

`

当使用rect方法时,我们不必使用moveTo方法,因为我们将矩形的坐标指定为前两个方法参数。在清单中,我画了一对名为closePath的线来创建一个三角形,然后画了两个相邻的矩形。你可以在图 36-3 中看到结果。

Image

图 36-3。用 rect 方法画矩形

子路径不一定要接触才能形成路径的一部分。我们可以有几个不相连的子路径,它们仍然被视为同一形状的一部分。清单 36-4 给出了一个演示。

清单 36-4。使用断开的子路径

`...

...`

在此示例中,子路径没有连接,但总体结果仍然是一条路径。当我调用strokefill方法时,效果被应用到我创建的所有子路径上,正如你在图 36-4 中看到的。

Image

图 36-4。使用断开的子路径

画圆弧

我们使用arcarcTo方法在画布上绘制弧线,尽管每种方法绘制弧线的方式不同。表 36-3 描述了画布中与圆弧相关的方法。

Image

使用 arcTo 方法

清单 36-5 演示了如何使用arcTo方法。

清单 36-5。使用 arcTo 方法

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  

            ctx.fillStyle = "yellow";             ctx.strokeStyle = "black";             ctx.lineWidth = 4;

            ctx.beginPath();             ctx.moveTo(point1[0], point1[1]);             ctx.arcTo(point2[0], point2[1], point3[0], point3[1], 100);             ctx.stroke();

            drawPoint(point1[0], point1[1]);             drawPoint(point2[0], point2[1]);             drawPoint(point3[0], point3[1]);

            ctx.beginPath();             ctx.moveTo(point1[0], point1[1]);             ctx.lineTo(point2[0], point2[1]);             ctx.lineTo(point3[0], point3[1]);             ctx.stroke();

            function drawPoint(x, y) {                 ctx.lineWidth = 1;                 ctx.strokeStyle = "red";                 ctx.strokeRect(x -2, y-2, 4, 4);             }              

`

arcTo方法画出的圆弧依赖于两条线。第一行是从最后一个子路径的末端到前两个方法参数描述的点。第二条线是从前两个参数描述的点到第三个和第四个参数描述的点绘制的。然后,圆弧被绘制为最后一个子路径的端点和第二个点之间的最短直线,该直线描绘了半径由最后一个参数指定的圆弧。为了更容易理解,我在画布上添加了一些额外的路径来提供一些上下文,如图 36-5 所示。

Image

图 36-5。使用arcTo方法

你可以看到用红色画的两条线。我已经指定了一个半径,两条线的长度都是一样的,这意味着我们最终得到了一个整洁的曲线,它刚好接触到前一个子路径的最后一个点,以及第三和第四个方法参数所描述的点。半径和线条长度的大小并不总是那么方便,所以画布会根据需要调整它绘制的弧线。作为示范,清单 36-6 使用第三十章中描述的事件来监控鼠标的移动,并在鼠标在屏幕上移动时为不同的点画出弧线。

清单 36-6。响应鼠标移动画弧线

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element         ` `        

            draw();

            canvasElem.onmousemove = function (e) { **                if (e.ctrlKey) {** **                    point1 = [e.clientX, e.clientY];** **                } else if(e.shiftKey) {** **                    point2 = [e.clientX, e.clientY];    ** **                } else {** **                    point3 = [e.clientX, e.clientY];** **                }** **                ctx.clearRect(0, 0, 540, 140);** **                draw();** **            }**

            function draw() {

                ctx.fillStyle = "yellow";                 ctx.strokeStyle = "black";                 ctx.lineWidth = 4;

                ctx.beginPath();                 ctx.moveTo(point1[0], point1[1]);                 ctx.arcTo(point2[0], point2[1], point3[0], point3[1], 50);                 ctx.stroke();

                drawPoint(point1[0], point1[1]);                 drawPoint(point2[0], point2[1]);                 drawPoint(point3[0], point3[1]);

                ctx.beginPath();                 ctx.moveTo(point1[0], point1[1]);                 ctx.lineTo(point2[0], point2[1]);                 ctx.lineTo(point3[0], point3[1]);                 ctx.stroke();             }

            function drawPoint(x, y) {                 ctx.lineWidth = 1;                 ctx.strokeStyle = "red";                 ctx.strokeRect(x -2, y-2, 4, 4);             }              

`

本例中的脚本根据鼠标移动时按下的键来移动不同的点。如果按下 control 键,第一个点将被移动(代表前一个子路径的终点)。如果按下 shift 键,则移动第二个点(由arcTo方法的前两个参数表示的点)。如果两个键都没有按下,则移动第三个点(由第三和第四个方法参数表示的点)。值得花一点时间来研究这个例子,以了解弧线与两条线的位置之间的关系。你可以在图 36-6 中看到这个快照。

Image

图 36-6。线条和圆弧的关系

使用电弧法

使用arc方法稍微简单一些。我们使用前两个方法参数在画布上指定一个点。我们用第三个参数指定圆弧的半径,然后指定圆弧的开始和结束角度。最后一个参数指定是顺时针还是逆时针绘制弧线。清单 36-7 给出了一些例子。

清单 36-7。使用电弧法

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                               ctx.arc(200, 70, 60, Math.PI/2, Math.PI, true);             ctx.fill();             ctx.stroke();

            ctx.beginPath();             var val = 0;             for (var i = 0; i < 4; i++) {                 ctx.arc(350, 70, 60, val, val + Math.PI/4, false);                 val+= Math.PI/2;             }             ctx.closePath();             ctx.fill();             ctx.stroke();              

`

你可以在图 36-7 中看到这些弧线所描述的形状。

Image

图 36-7。使用电弧法

正如第一个和第二个弧所示,我们可以使用arc方法来绘制完整的圆或规则的弧,正如您所期望的那样。然而,如第三个图形所示,我们可以使用arc方法来创建更复杂的路径。如果我们使用 arc 方法,并且已经绘制了一个子路径,那么将直接从前面的子路径的末端到 arc 方法的前两个参数描述的坐标绘制一条线。这条线是在我们描述的弧线之外画的。我用这个怪癖结合一个for环将围绕同一点画的四个小圆弧连接在一起,得到如图 36-7 所示的形状。

绘制贝塞尔曲线

画布支持绘制两种贝塞尔曲线:三次和二次。您可能在绘图包中使用过贝塞尔曲线。我们选择一个起点和终点,然后添加一个或多个控制点来塑造曲线。贝塞尔曲线在画布上的问题是,我们没有任何视觉反馈,这使得我们更难获得我们想要的曲线。在接下来的例子中,我将在脚本中添加一些代码来提供一些上下文,但是在一个真实的项目中,您必须进行实验来获得您需要的曲线。表 36-4 显示了我们可以用来绘制曲线的方法。

Image

绘制三次贝塞尔曲线

bezierCurveTo方法从前一个子路径的末端到第 5 个和第 6 个方法参数指定的点绘制一条曲线。有两个控制点——由前四个参数指定。清单 36-8 展示了这种方法的使用(以及一些额外的路径,以便更容易理解参数值和产生的曲线之间的关系)。

清单 36-8。绘制三次贝塞尔曲线

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                              var cp2 = [350, 50];

            canvasElem.onmousemove = function(e) {                 if (e.shiftKey) {                     cp1 = [e.clientX, e.clientY];                 } else if (e.ctrlKey) {                     cp2 = [e.clientX, e.clientY];                 }                 ctx.clearRect(0, 0, 500, 140);                 draw();             }

            draw();

            function draw() {                 ctx.lineWidth = 3;                 ctx.strokeStyle = "black";                                   ctx.beginPath();                 ctx.moveTo(startPoint[0], startPoint[1]);                 ctx.bezierCurveTo(cp1[0], cp1[1], cp2[0], cp2[1],                     endPoint[0], endPoint[1]);                 ctx.stroke();

                ctx.lineWidth = 1;                 ctx.strokeStyle = "red";                             var points = [startPoint, endPoint, cp1, cp2];                 for (var i = 0; i < points.length; i++) {                     drawPoint(points[i]);                     }                 drawLine(startPoint, cp1);                 drawLine(endPoint, cp2);             }

            function drawPoint(point) {                 ctx.beginPath();

                ctx.strokeRect(point[0] -2, point[1] -2, 4, 4);             }

            function drawLine(from, to) {                 ctx.beginPath();                 ctx.moveTo(from[0], from[1]);                 ctx.lineTo(to[0], to[1]);                 ctx.stroke();             }              

`

为了让您了解曲线是如何绘制的,本例中的脚本会根据鼠标的移动来移动贝塞尔曲线上的控制点。如果按下 shift 键,则第一个控制点移动。如果按下控制键,则移动第二个控制点。你可以在图 36-8 中看到效果。

Image

图 36-8。绘制三次贝塞尔曲线

绘制二次贝塞尔曲线

二次贝塞尔曲线只有一个控制点,因此quadraticCurveTo方法比bezierCurveTo方法少两个参数。清单 36-9 显示了之前的例子,修改后显示一条二次曲线,用quadraticCurveTo方法绘制。

清单 36-9。绘制二次贝塞尔曲线

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  

            canvasElem.onmousemove = function(e) {                 if (e.shiftKey) {                     cp1 = [e.clientX, e.clientY];                 }                 ctx.clearRect(0, 0, 500, 140);                 draw();             }

            draw();

            function draw() {                 ctx.lineWidth = 3;                 ctx.strokeStyle = "black";                                   ctx.beginPath();                 ctx.moveTo(startPoint[0], startPoint[1]);                 ctx.quadraticCurveTo(cp1[0], cp1[1], endPoint[0], endPoint[1]);                 ctx.stroke();

                ctx.lineWidth = 1;                 ctx.strokeStyle = "red";                             var points = [startPoint, endPoint, cp1];                 for (var i = 0; i < points.length; i++) {                     drawPoint(points[i]);                     }                 drawLine(startPoint, cp1);                 drawLine(endPoint, cp1);             }

            function drawPoint(point) {                 ctx.beginPath();

                ctx.strokeRect(point[0] -2, point[1] -2, 4, 4);             }

            function drawLine(from, to) {                 ctx.beginPath();                 ctx.moveTo(from[0], from[1]);                 ctx.lineTo(to[0], to[1]);                 ctx.stroke();             }              

`

你可以在图 36-9 中看到一个示例曲线。

Image

图 36-9。一条二次贝塞尔曲线

创建裁剪区域

正如本章前面所演示的,我们可以使用strokefill方法来绘制或填充路径。还有一个替代方案,就是使用表 36-5 中描述的方法。

Image

一旦我们定义了一个剪辑区域,只有出现在该区域内的路径才会显示在屏幕上。清单 36-10 给出了一个演示。

清单 36-10。使用剪辑区域

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                               ctx.rect(100, 20, 300, 100);             ctx.clip();

            ctx.fillStyle = "red";             ctx.beginPath();             ctx.rect(0, 0, 500, 140);             ctx.fill();

             

`

本例中的脚本绘制了一个填充画布的矩形,创建了一个较小的剪辑区域,然后绘制了另一个填充画布的矩形。正如你在图 36-10 中看到的,只画出了第二个矩形中适合裁剪区域的部分。

Image

图 36-10。裁剪区域的效果

绘图文本

我们可以在画布上绘制文本,尽管对这样做的支持是非常基本的。表 36-6 显示了可用的方法。

Image

Image

我们可以使用三个绘制状态属性来控制文本的绘制方式,如表 36-7 所示。

Image

清单 36-11 展示了我们如何填充和描边文本。我们使用与 CSS 字体速记属性相同的格式字符串为font属性指定值,我在第二十二章的中描述过。

清单 36-11。在画布上绘制文本

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  

            ctx.font = "100px sans-serif";             ctx.fillText("Hello", 50, 100);             ctx.strokeText("Hello", 50, 100);              

`

文本是使用fillStylestrokeStyle属性绘制的,这意味着我们拥有与形状相同的颜色、渐变和图案。在这个例子中,我用两种纯色填充和描边文本。你可以在图 36-11 中看到效果。

Image

图 36-11。填充和描边文本

使用效果和变换

我们可以在画布上应用一些效果和变换,如下面几节所述。

使用阴影

有四个绘制状态属性,我们可以使用它们来为我们在画布上绘制的形状和文本添加阴影。这些属性在表 36-8 中描述。

Image

清单 36-12 显示了我们如何使用这些属性来应用阴影。

清单 36-12。对形状和文本应用阴影

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  

            ctx.shadowOffsetX = 5;             ctx.shadowOffsetY = 5;             ctx.shadowBlur = 5;             ctx.shadowColor = "grey";

            ctx.strokeRect(250, 20, 100, 100);

            ctx.beginPath();             ctx.arc(420, 70, 50, 0, Math.PI, true);             ctx.stroke();

            ctx.beginPath();             ctx.arc(420, 80, 40, 0, Math.PI, false);             ctx.fill();

            ctx.font = "100px sans-serif";             ctx.fillText("Hello", 10, 100);             ctx.strokeText("Hello", 10, 100);              

`

本示例将阴影应用于文本、矩形和一个完整的圆。和两条弧线。如图 36-12 所示,无论形状是开放的、闭合的、填充的还是描边的,阴影都被应用到形状上。

Image

图 36-12。对文本和形状应用阴影

使用透明度

我们可以用两种方法来设置我们绘制的文本和形状的透明度。第一种是使用rgba函数(而不是rgb)指定一个fillStylestrokeStyle值,如第四章所述。我们也可以使用通用的globalAlpha绘制状态属性。清单 36-13 显示了globalAlpha属性的使用。

清单 36-13。使用 globalAlpha 属性

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  

            ctx.font = "100px sans-serif";             ctx.fillText("Hello", 10, 100);             ctx.strokeText("Hello", 10, 100);       

            ctx.fillStyle = "red"; **            ctx.globalAlpha = 0.5;**             ctx.fillRect(100, 10, 150, 100);              

`

globalAlpha值的范围可以从 0(完全透明)到 1(完全不透明,这是默认值)。在这个例子中,我绘制了一些文本,将globalAlpha属性设置为 0.5,然后填充部分覆盖文本的矩形。你可以在图 36-13 中看到结果。

Image

图 36-13。通过 globalAlpha 属性使用透明度

使用构图

我们可以结合使用透明性和globalCompositeOperation属性来控制图形和文本在画布上的绘制方式。该属性的允许值在表 36-9 中描述。对于这个属性,由属性设置后执行的任何操作组成,目标图像是属性设置时画布的状态

Image

Image

globalCompositeOperation属性的值可以产生一些惊人的效果。清单 36-14 包含一个select元素,该元素包含所有合成值的选项。值得花一点时间来研究这个例子,看看每个合成模式是如何工作的。

清单 36-14。使用 globalCompositeOperation 属性

`

             Example                      canvas {border: thin solid black; margin: 4px;}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  Composition Value:             copy             destination-atopdestination-in             destination-overdistination-out             lightersource-atop             source-insource-out             source-overxor                  

            var compVal = "copy";

            document.getElementById("list").onchange = function(e) {                 compVal = e.target.value;                 draw();             }

            draw();

            function draw() {                 ctx.clearRect(0, 0, 300, 120);                 ctx.globalAlpha = 1.0;                 ctx.font = "100px sans-serif";                 ctx.fillText("Hello", 10, 100);                 ctx.strokeText("Hello", 10, 100);       

                ctx.globalCompositeOperation = compVal;

                ctx.fillStyle = "red";                 ctx.globalAlpha = 0.5;                 ctx.fillRect(100, 10, 150, 100);             }              

`

在图 36-14 中可以看到source-outdestination-over的值。有些浏览器对样式的解释略有不同,所以您可能看不到图中显示的确切内容。

Image

图 36-14。使用 globalCompositeOperation 属性

使用变换

我们可以对画布应用一个转换,然后将它应用于任何后续的绘制操作。表 36-10 描述了转换方法。

Image

这些方法创建的转换仅适用于后续的绘制操作——画布的现有内容保持不变。清单 36-15 展示了我们如何使用缩放、旋转和平移方法。

清单 36-15。使用转换

`

             Example                      canvas {border: thin solid black; margin: 4px;}             body > * {float:left;}                                         Your browser doesn't support the canvas element                  

            ctx.clearRect(0, 0, 300, 120);             ctx.globalAlpha = 1.0;             ctx.font = "100px sans-serif";             ctx.fillText("Hello", 10, 100);             ctx.strokeText("Hello", 10, 100);       

**            ctx.scale(1.3, 1.3);** **            ctx.translate(100, -50);** **            ctx.rotate(0.5);**

            ctx.fillStyle = "red";             ctx.globalAlpha = 0.5;             ctx.fillRect(100, 10, 150, 100);

            ctx.strokeRect(0, 0, 300, 200);              

`

在本例中,我填充并描边了一些文本,然后缩放、平移和旋转画布,这将影响我随后绘制的填充矩形和描边矩形。你可以在图 36-15 中看到效果。

Image

图 36-15。变换画布

总结

在这一章中,我已经展示了如何使用不同的路径在画布上绘画,包括直线、矩形、弧线和曲线。我还演示了 canvas 文本工具以及如何应用阴影和透明等效果。我通过演示画布支持的不同合成模式和转换完成了这一章。

三十七、使用拖放

HTML5 增加了对拖放的支持。这是我们以前不得不依赖 jQuery 等 JavaScript 库来处理的事情。将拖放功能内置到浏览器中的好处是,它可以正确地集成到操作系统中,并且正如您将看到的,可以在不同的浏览器之间工作。

这项功能仍处于早期阶段,在主流浏览器提供的规范和实现之间还有很大的差距。并非所有的浏览器都实现了规范的所有部分,并且一些特性以完全不同的方式实现。在这一章中,我已经向你展示了目前的工作方式。这并不是 HTML5 标准定义的完整功能集,但它足以启动并运行。表 37-1 对本章进行了总结。

Image

Image

创建源项目

我们告诉浏览器文档中的哪些元素可以通过draggable属性拖动。该属性有三个允许值,在表 37-2 中描述。

Image

默认值是auto值,由浏览器决定,这通常意味着默认情况下可以拖动所有元素,我们必须通过将draggable属性设置为false来显式禁用拖动。当使用拖放功能时,我倾向于将draggable属性显式设置为true,尽管主流浏览器默认认为所有元素都是draggable。清单 37-1 显示了一个简单的 HTML 文档,其中有一些可以拖动的元素。

清单 37-1。定义可拖动项目

`

             Example                      #src > * {float:left;}             #target, #src > img {border: thin solid black; padding: 2px; margin:4px;}             #target {height: 81px; width: 81px; text-align: center; display: table;}             #target > p {display: table-cell; vertical-align: middle;}             #target > img {margin: 1px;}                            
            <img draggable="true" id="banana" src="banana100.png" alt="banana"/>             <img draggable="true" id="apple" src="apple100.png" alt="apple"/>             <img draggable="true" id="cherries" src="cherries100.png" alt="cherry"/>             
                

Drop Here

            </div         
            `

`             

`

在这个例子中,有三个img元素,每个元素的draggable属性都被设置为true。我还创建了一个具有targetiddiv元素,我们将很快将其设置为被拖动的img元素的接收者。您可以在图 37-1 中看到该文档如何出现在浏览器中。

Image

图 37-1。三个可拖动图像和一个目标

我们可以拖动水果图片,而不需要做任何进一步的工作,但是浏览器会指示我们不能将它们放在任何地方。这通常是通过将禁止进入标志显示为光标来实现的,如图图 37-2 所示。

Image

图 37-2。浏览器显示不能放下被拖动的项目

处理拖动事件

我们通过一系列事件利用拖放功能。这些是针对被拖动元素的事件和针对潜在拖放区的事件。表 37-3 描述了那些被拖动元素的事件。

我们可以使用这些事件在视觉上强调拖动操作,如清单 37-2 所示。

清单 37-2。使用针对被拖动元素的事件

`

             Example                      #src > * {float:left;}             #target, #src > img {border: thin solid black; padding: 2px; margin:4px;}             #target {height: 81px; width: 81px; text-align: center; display: table;}             #target > p {display: table-cell; vertical-align: middle;}             #target > img {margin: 1px;}             **img.dragged {background-color: lightgrey;}**                            
            banana             apple             cherry             
                

Drop Here

            </div         

        

            src.ondragstart = function(e) {                 e.target.classList.add("dragged");             }

            src.ondragend = function(e) {                 e.target.classList.remove("dragged");                 msg.innerHTML = "Drop Here";             }

            src.ondrag = function(e) {                 msg.innerHTML = e.target.id;             }              

`

我定义了一个新的 CSS 样式,应用于dragged类中的元素。为了响应dragstart事件,我将元素添加到这个类中,并为了响应dragend事件,将它从这个类中移除。为了响应drag事件,我将拖放区中显示的文本设置为被拖动元素的id值。在拖动操作期间,每隔几毫秒就调用一次拖动事件,因此这不是最有效的方法,但它确实演示了该事件。您可以在图 3 中看到效果。请注意,我们仍然没有一个工作的空投区,但我们越来越接近。

Image

图 37-3。使用 dragstart、dragend 和 drag 事件

创建拖放区

要使一个元素成为拖放区,我们需要处理dragenterdragover事件。这是针对空投区的两个事件。全套在表 37-4 中描述。

Image

dragenterdragover事件的默认动作是拒绝接受任何拖动的项目,所以我们必须做的最重要的事情是防止默认动作被执行。清单 37-3 包含了一个例子。

Image 注意拖放的规范告诉我们,我们还必须将dropzone属性应用于我们想要放入拖放区的元素,并且属性值应该包含我们愿意接受的操作和数据类型的细节。这不是浏览器实际实现该功能的方式。对于这一章,我描述了事物真正工作的方式,而不是它们是如何被指定的。

清单 37-3。通过处理 dragenter 和 dragover 事件创建拖放区

`

             Example                      #src > * {float:left;}             #target, #src > img {border: thin solid black; padding: 2px; margin:4px;}             #target {height: 81px; width: 81px; text-align: center; display: table;}             #target > p {display: table-cell; vertical-align: middle;}             #target > img {margin: 1px;}             img.dragged {background-color: lightgrey;}                            
            banana             apple             cherry             
                

Drop Here

            
        

        

            target.ondragenter = handleDrag;             target.ondragover = handleDrag;

            function handleDrag(e) {                 e.preventDefault();             } src.ondragstart = function(e) {                 e.target.classList.add("dragged");             }

            src.ondragend = function(e) {                 e.target.classList.remove("dragged");                 msg.innerHTML = "Drop Here";             }

            src.ondrag = function(e) {                 msg.innerHTML = e.target.id;             }              

`

有了这些东西,我们就有了一个活跃的空投区。当我们拖动一个项目到拖放区元素上时,浏览器会显示如果我们拖放它,它将被接受,如图图 37-4 所示。

Image

图 37-4。浏览器指示可以删除某个项目

接收水滴

我们通过处理drop事件来接收被拖放的元素,当一个项目被放到拖放区元素上时,该事件被触发。清单 37-4 展示了我们如何使用一个全局变量作为被拖动元素和拖放区之间的通道来响应drop事件。

清单 37-4。处理掉落事件

`

             Example                      #src > * {float:left;}             #src > img {border: thin solid black; padding: 2px; margin:4px;}             #target {border: thin solid black; margin:4px;}             #target { height: 81px; width: 81px; text-align: center; display: table;}` `            #target > p {display: table-cell; vertical-align: middle;}             img.dragged {background-color: lightgrey;}                            
            banana             apple             cherry             
                

Drop Here

            
        

        

            var draggedID;

            target.ondragenter = handleDrag;             target.ondragover = handleDrag;

            function handleDrag(e) {                 e.preventDefault();             }

            target.ondrop = function(e) {                 var newElem = document.getElementById(draggedID).cloneNode(false);                 target.innerHTML = "";                 target.appendChild(newElem);                 e.preventDefault();             }

            src.ondragstart = function(e) {                 draggedID = e.target.id;                 e.target.classList.add("dragged");             }

            src.ondragend = function(e) {                 var elems = document.querySelectorAll(".dragged");                 for (var i = 0; i < elems.length; i++) {                     elems[i].classList.remove("dragged");                 }             }              

`

dragstart事件被触发时,我设置了draggedID变量的值。这允许我记下被拖动元素的id属性值。当drop事件被触发时,我使用这个值来克隆被拖动的img元素,并将其添加为拖放区元素的子元素。

Image 提示在这个例子中,我阻止了drop事件的默认动作。如果没有这一点,浏览器可以做一些意想不到的事情。例如,在这个场景中,Firefox 导航离开页面,并显示被拖动的img元素的src属性所引用的图像。

你可以在图 37-5 中看到效果。

Image

图 37-5。响应拖动事件

使用数据传输对象

与拖放触发的事件一起调度的对象是DragEvent,它是从MouseEvent派生出来的。DragEvent对象定义了EventMouseEvent对象的所有功能(在第三十章的中有描述),其附加属性如表 37-5 所示。

Image

我们使用DataTransfer对象将任意数据从被拖动的元素传输到拖放区元素。DataTransfer对象定义的属性和方法在表 37-6 中描述。

在前一个例子中,我克隆了元素本身;然而,DataTransfer对象允许我们使用更复杂的方法。我们能做的第一件事是使用DataTransfer对象将数据从被拖动的元素转移到拖放区,如清单 37-5 所示。

清单 37-5。使用 DataTransfer 对象传输数据

`

             Example                      #src > * {float:left;}             #src > img {border: thin solid black; padding: 2px; margin:4px;}             #target {border: thin solid black; margin:4px;}             #target { height: 81px; width: 81px; text-align: center; display: table;}             #target > p {display: table-cell; vertical-align: middle;}             img.dragged {background-color: lightgrey;}                            
            banana             apple             cherry             
                

Drop Here

            
        

        

            target.ondragenter = handleDrag;             target.ondragover = handleDrag;`

`            function handleDrag(e) {                 e.preventDefault();             }

            target.ondrop = function(e) {                 var droppedID = e.dataTransfer.getData("Text");                 var newElem = document.getElementById(droppedID).cloneNode(false);                 target.innerHTML = "";                 target.appendChild(newElem);                 e.preventDefault();             }

            src.ondragstart = function(e) {                 e.dataTransfer.setData("Text", e.target.id);                 e.target.classList.add("dragged");             }

            src.ondragend = function(e) {                 var elems = document.querySelectorAll(".dragged");                 for (var i = 0; i < elems.length; i++) {                     elems[i].classList.remove("dragged");                 }             }              

`

当响应dragstart事件时,我使用setData方法来设置我想要传输的数据。对于指定数据类型的第一个参数,只有两个受支持的值— TextUrl(浏览器只可靠地支持Text)。第二个参数是我们想要传输的数据:在本例中,是被拖动元素的id属性。为了检索值,我使用了getData方法,使用数据类型作为参数。

您可能想知道为什么这是比使用全局变量更好的方法。答案是它可以跨浏览器工作,我的意思不是跨相同浏览器中的窗口或标签,而是跨不同的类型的浏览器。这意味着我可以从 Chrome 文档中拖放元素到 Firefox 文档中,因为拖放支持与操作系统中的相同功能集成在一起。如果您打开一个文本编辑器,键入单词banana,选择它,然后将其拖动到浏览器中的拖放区,您将看到香蕉图像正在显示,就像我们在同一文档中拖动img元素时一样。

按数据过滤拖动的项目

我们可以使用存储在DataTransfer对象中的数据来选择我们愿意在拖放区接受的元素种类。清单 37-6 展示了如何操作。

清单 37-6。使用 DataTransfer 对象过滤拖动的元素

`…

…`

在这个例子中,我从DataTransfer对象获取数据值,并检查它是什么。我表示,只有当数据值为banana时,我才愿意接受被拖动的元素。这具有过滤掉苹果和樱桃图像的效果。当用户将它们拖放到拖放区时,浏览器会显示它们不能被拖放。

Image 提示这种过滤在 Chrome 中不起作用,因为getData方法在为dragenterdragover事件调用处理程序时不起作用。

拖放文件

隐藏在浏览器深处的是另一个新的 HTML5 功能,称为文件 API ,它允许我们在本地机器上处理文件,尽管是以严格控制的方式。控制的一部分是我们通常不直接与文件 API 交互。相反,它是通过其他功能公开的,包括拖动和放下。清单 37-7 展示了当用户从操作系统中拖放文件到拖放区时,我们如何使用文件 API 来响应。

清单 37-7。处理文件

`

             Example                      body > * {float: left;}             #target {border: medium double black; margin:4px; height: 75px;                 width: 200px; text-align: center; display: table;}             #target > p {display: table-cell; vertical-align: middle;}             table {margin: 4px; border-collapse: collapse;}             th, td {padding: 4px};                            
            

Drop Files Here

        
                 

        

            target.ondragenter = handleDrag;             target.ondragover = handleDrag;

            function handleDrag(e) {                 e.preventDefault();             }

            target.ondrop = function(e) {                 var files = e.dataTransfer.files;                 var tableElem = document.getElementById("data");                 tableElem.innerHTML = "NameTypeSize";                 for (var i = 0; i < files.length; i++) {                     var row = "" + files[i].name + "" +                         files[i].type+ "" +                         files[i].size + "";                     tableElem.innerHTML += row;                 }                 e.preventDefault();             }              

`

当用户将文件放到拖放区时,DataTransfer对象的 files 属性返回一个FileList对象。我们可以将此视为一个由File对象组成的数组,每个对象代表用户拖放的一个文件(用户可以选择多个文件并一次性拖放)。表 37-7 显示了File对象的属性。

Image

在这个例子中,script列举了放到拖放区的文件,并在一个表格中显示了File属性的值。你可以在图 37-6 中看到这个效果,我把一些示例文件放到了拖放区。

Image

图 37-6。显示关于文件的数据

在表单中上传拖放的文件

我们可以将拖放功能、文件 API 和使用 Ajax 请求上传数据结合起来,以允许用户从操作系统拖动想要包含在表单提交中的文件。清单 37-8 包含了一个演示。

清单 37-8。结合拖放,文件 API 和表单数据对象

`

             Example                      .table {display:table;}             .row {display:table-row;}             .cell {display: table-cell; padding: 5px;}             .label {text-align: right;}             #target {border: medium double black; margin:4px; height: 50px;                 width: 200px; text-align: center; display: table;}             #target > p {display: table-cell; vertical-align: middle;}                                         
                
                    
Bananas:
                    
                
                
                    
Apples:
                    
                
                
                    
Cherries:
                    
                
                
                    
File:
                    
                
                
                    
Total:
                    
0 items
                
            
            
                

Drop Files Here

            
            Submit Form                              target.ondragenter = handleDrag;             target.ondragover = handleDrag;

            function handleDrag(e) {                 e.preventDefault();             }

            target.ondrop = function(e) {                 fileList = e.dataTransfer.files;                 e.preventDefault();             }

            function handleButtonPress(e) {                 e.preventDefault();

                var form = document.getElementById("fruitform");                 var formData = new FormData(form);

                if (fileList || true) {                     for (var i = 0; i < fileList.length; i++) {                         formData.append("file" + i, fileList[i]);                     }                 }

                httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.open("POST", form.action);                 httpRequest.send(formData);             }

            function handleResponse() {                 if (httpRequest.readyState == 4 && httpRequest.status == 200) {                     var data = JSON.parse(httpRequest.responseText);                     document.getElementById("results").innerHTML = "You ordered "                         + data.total + " items";                 }             }               

`

在这个例子中,我在取自第三十三章的例子中添加了一个拖放区,在那里我演示了如何使用FormData对象将表单数据上传到服务器。我们可以通过使用FormData.append方法,将一个File对象作为该方法的第二个参数传入,将文件放入拖放区。提交表单时,文件内容将作为表单请求的一部分自动上传到服务器。

总结

在这一章中,我展示了对拖放元素的支持。这个特性的实现还有很多不尽如人意的地方,但是它很有前途,我希望主流浏览器很快会开始解决这种不一致的问题。如果你不能等到那时(或者你不在乎在其他浏览器和操作系统之间来回拖动),那么你应该考虑使用一个 JavaScript 库,比如 jQuery 和 jQuery UI。

三十八、使用地理定位

地理定位 API 允许我们获得关于用户当前地理位置的信息(或者至少是运行浏览器的系统的位置)。这不是 HTML5 规范的一部分,但它通常被归为与 HTML5 相关的新特性的一部分。表 38- 1 对本章进行了总结。

Image

使用地理定位

我们通过全局navigator.geolocation属性访问地理位置特性,该属性返回一个Geolocation对象——这个对象的方法在表 38- 2 中有描述。

Image

Image

获取当前位置

顾名思义,getCurrentPosition方法获取当前位置,尽管位置信息不是作为方法本身的结果返回的。相反,我们提供了一个成功回调函数,当位置信息可用时调用该函数——这考虑到了在请求位置和位置变为可用之间可能存在延迟的事实。清单 38 - 1 展示了我们如何使用这种方法获得位置信息。

清单 38- 1。获取当前位置

`

             Example                      table {border-collapse: collapse;}             th, td {padding: 4px;}             th {text-align: right;}                                                                                                                                                                                                                                                                                     
Longitude:-Latitude:-
Altitude:-Accuracy:-
Altitude Accuracy:-Heading:-
Speed:-Time Stamp:-
                         var properties = ["longitude", "latitude", "altitude", "accuracy",                                   "altitudeAccuracy", "heading", "speed"];

                for (var i = 0; i < properties.length; i++) {                     var value = pos.coords[properties[i]];                     document.getElementById(properties[i]).innerHTML = value;                 }                 document.getElementById("timestamp").innerHTML = pos.timestamp;             }              

`

本例中的脚本调用getCurrentPosition,将displayPosition函数作为方法参数传递。当位置信息可用时,指定的函数被调用,浏览器传入一个给出位置细节的Position对象——细节显示在一个table元素的单元格中。Position对象非常简单,正如你在表格 38- 3 中看到的。

Image

我们真正感兴趣的是由Position.coords属性返回的Coordinates对象。表格 38- 4 描述了Coordinates对象的属性。

Image

并不是Coordinates对象中的所有数据值都会一直可用。浏览器获取位置信息的机制是未指定的,并且使用了许多技术。移动设备越来越多地拥有 GPS、加速度计和指南针设施,这意味着最准确和完整的数据将在这些平台上可用。

我们仍然可以获得其他设备的位置信息——浏览器使用地理定位服务,试图根据网络信息确定位置。如果您的系统有 Wi-Fi 适配器,那么在范围内的网络会与作为街道级视图(如 Google Street View)调查的一部分的网络目录进行比较。如果 Wi-Fi 不可用,则可以使用您的 ISP 提供的 IP 地址来大致了解位置。

从网络信息推断出的位置的准确性各不相同,但它可能是惊人的准确。当我开始测试这个功能时,我惊讶地发现我的位置被报告得如此之窄。事实上,它是如此准确,以至于我在截图中替换了帝国大厦的位置——利用真实的位置信息(来自我和附近的 Wi-Fi 网络),你可以很容易地找到我的房子,并看到我的汽车停在车道上的照片。可怕的事情——以至于当一个文档使用地理定位功能时,所有浏览器做的第一件事就是要求用户授予许可——你可以在图 38-1 中看到 Chrome 是如何做到这一点的。

Image

38- 1。授予地理定位功能的权限

如果用户批准了请求,那么就获得位置信息,并且当位置信息可用时,就调用回调函数。你可以在图 38- 2 中看到我的台式电脑提供的那种数据。

Image

38- 2。显示地理定位服务提供的位置信息

我用来写书的电脑没有安装任何专门的定位硬件——没有 GPS、指南针、高度计或加速度计。因此,唯一可用的数据是纬度和经度以及这些值的准确性。对于我的位置,Chrome 估计我在已报告位置的 69 米(约 75 码)内(这在我的情况下是一个低估)。

Image 提示 Chrome、Firefox 和 Opera 都使用谷歌地理定位服务。Internet Explorer 和 Safari 使用自己的。我只能报告我的位置,但微软服务报告的精度约为 48,000 米(约 30 英里)。我发现数据精确到大约 3 英里。苹果服务报告的精度为 500 米,但提供了所有数据中最好的——它在几英尺内确定了我的位置。哇哦!

处理地理位置错误

我们可以为getCurrentPosition方法提供第二个参数,这允许我们提供一个函数,如果在获取位置时出现错误,这个函数将被调用。向该函数传递一个PositionError对象,该对象定义了表 38- 5 中描述的属性。

Image

code属性有三个可能的值。这些属性在表 38- 6 中描述。

Image

清单 38 - 2 展示了我们如何使用PositionError对象接收错误。

清单 38 - 2。用 PositionError 对象处理错误

<!DOCTYPE HTML> `              Example                      table {border-collapse: collapse;}             th, td {padding: 4px;}             th {text-align: right;}                            

                                                                                                                                                                                                                                                                                                                    
Longitude:-Latitude:-
Altitude:-Accuracy:-
Altitude Accuracy:-Heading:-
Speed:-Time Stamp:-
Error Code:-Error Message:-

        );

            function displayPosition(pos) {                 var properties = ["longitude", "latitude", "altitude", "accuracy",                                   "altitudeAccuracy", "heading", "speed"];

                for (var i = 0; i < properties.length; i++) {                     var value = pos.coords[properties[i]];                     document.getElementById(properties[i]).innerHTML = value;                 }                 document.getElementById("timestamp").innerHTML = pos.timestamp;             }

            function handleError(err) {                 document.getElementById("errcode").innerHTML = err.code;                 document.getElementById("errmessage").innerHTML = err.message;             }

             

`

创建错误的最简单方法是在浏览器提示时拒绝权限。本例中的脚本显示了table元素中的错误细节,您可以在图 38- 3 中看到效果。

Image

图 38 - 3。显示地理位置错误的详细信息

指定地理位置选项

我们可以提供给getCurrentPosition方法的第三个参数是一个PositionOptions对象。这个特性允许我们对获取位置的方式进行一些控制。表 38- 7 显示了该对象定义的属性。

Image

highAccuracy属性设置为true只是要求浏览器给出最好的可能结果——并不能保证它会带来更准确的位置。对于移动设备,如果禁用了节能模式,或者在某些情况下打开了 GPS 功能,可能会获得更准确的位置(低精度位置可能来自 Wi-Fi 或手机信号塔数据)。对于其他设备,可能没有更高精度的数据可用。清单 38-3 展示了当请求一个位置时我们如何使用PositionOptions对象。

清单 38-3。请求位置数据时指定选项

`

             Example                      table {border-collapse: collapse;}             th, td {padding: 4px;}             th {text-align: right;}                                                                                                                                                                                                                                                                                                                                                 
Longitude:-Latitude:-
Altitude:-Accuracy:-
Altitude Accuracy:-Heading:-
Speed:-Time Stamp:-
Error Code:-Error Message:-
                         enableHighAccuracy: false,                 timeout: 2000,                 maximumAge: 30000             };

            navigator.geolocation.getCurrentPosition(displayPosition,                                                      handleError, options);

            function displayPosition(pos) {                 var properties = ["longitude", "latitude", "altitude", "accuracy",                                   "altitudeAccuracy", "heading", "speed"];

                for (var i = 0; i < properties.length; i++) {                     var value = pos.coords[properties[i]];                     document.getElementById(properties[i]).innerHTML = value;                 }                 document.getElementById("timestamp").innerHTML = pos.timestamp;             }

            function handleError(err) {                 document.getElementById("errcode").innerHTML = err.code;                 document.getElementById("errmessage").innerHTML = err.message;             }

             

`

这里有一个奇怪的地方,我们没有创建一个新的PositionOptions对象。相反,我们创建一个普通的Object并定义与表中的属性相匹配的属性。在这个例子中,我已经表明我不需要最高级别的分辨率,我准备在请求超时前等待 2 秒钟,并且我愿意接受已经缓存了 30 秒钟的数据。

监控位置

我们可以通过使用watchPosition方法接收关于位置的持续更新。这个方法采用与getCurrentPosition方法相同的参数,以相同的方式工作——不同的是回调函数将随着位置的改变而被重复调用。清单 38 - 4 展示了我们如何使用watchPosition方法。

清单 38-4。使用观察位置方法

`

             Example                      table {border-collapse: collapse;}             th, td {padding: 4px;}             th {text-align: right;}                                                                                                                      ` `                                                                                                                                                                                                                          
Longitude:-Latitude:-
Altitude:-Accuracy:-
Altitude Accuracy:-Heading:-
Speed:-Time Stamp:-
Error Code:-Error Message:-
        **Cancel Watch**         

            var watchID = navigator.geolocation.watchPosition(displayPosition,                                                      handleError,                                                      options);

            document.getElementById("pressme").onclick = function(e) {                 navigator.geolocation.clearWatch(watchID);             };

            function displayPosition(pos) {                 var properties = ["longitude", "latitude", "altitude", "accuracy",                                   "altitudeAccuracy", "heading", "speed"];

                for (var i = 0; i < properties.length; i++) {                     var value = pos.coords[properties[i]];                     document.getElementById(properties[i]).innerHTML = value;                 }                 document.getElementById("timestamp").innerHTML = pos.timestamp;             }

            function handleError(err) {                 document.getElementById("errcode").innerHTML = err.code;                 document.getElementById("errmessage").innerHTML = err.message;             }

             

`

在这个例子中,脚本使用watchPosition方法来监控位置。这个方法返回一个 ID 值,当我们想要停止监控时,可以将这个 ID 值传递给clearWatch方法。当按下button元素时,我会这样做。

Image 警告主流浏览器的当前版本没有很好地实现watchPosition方法,更新的位置也不总是即将到来。使用定时器(我在第二十七章的中描述过)并定期调用getCurrentPosition方法可能会更好。

总结

在这一章中,我描述了地理定位 API,它提供了浏览器所在系统的当前位置信息。我解释说,浏览器获取位置数据的机制各不相同,位置数据不仅限于那些支持 GPS 的设备。

三十九、使用 Web 存储

Web 存储允许我们在浏览器中存储简单的键/值数据。Wen 存储类似于 cookies,但是实现得更好,我们可以存储更多的数据。有两种类型的 web 存储——本地存储和会话存储。这两种类型共享相同的机制,但是存储数据的可见性及其寿命不同。表 39-1 对本章进行了总结。

Image 提示还有另一种存储规范,索引数据库 API,它允许更丰富的数据存储和类似 SQL 的查询。在我写这篇文章的时候,这个规范仍然是易变的,浏览器实现也是实验性的和不稳定的。

Image

使用本地存储

我们通过localStorage全局属性访问本地存储特性——该属性返回一个Storage对象,如表 39-2 所述。Storage对象用于存储成对的字符串,以键/值的形式组织。

Image

对象允许我们存储键/值对,其中键和值都是字符串。键必须是惟一的,这意味着如果我们使用一个已经存在于Storage对象中的键来调用setItem方法,这个值就会被更新。清单 39-1 展示了我们如何添加、修改和清除本地存储中的数据。

清单 39-1。使用本地存储器

`

             Example                      body > * {float: left;}             table {border-collapse: collapse; margin-left: 50px}             th, td {padding: 4px;}             th {text-align: right;}             input {border: thin solid black; padding: 2px;}             label {min-width: 50px; display: inline-block; text-align: right;}             #countmsg, #buttons {margin-left: 50px; margin-top: 5px; margin-bottom: 5px;}                   ` `        
            
Key:
            
Value:
            
                Add                 Clear             
            

There are items

        

        

                     
Item Count:-

        

            var buttons = document.getElementsByTagName("button");             for (var i = 0; i < buttons.length; i++) {                 buttons[i].onclick = handleButtonPress;             }

            function handleButtonPress(e) {                 switch (e.target.id) {                     case 'add':                         var key = document.getElementById("key").value;                         var value = document.getElementById("value").value;                         localStorage.setItem(key, value);                         break;                     case 'clear': **                        localStorage.clear();**                         break;                 }                 displayData();             }

            function displayData() {                 var tableElem = document.getElementById("data");                 tableElem.innerHTML = "";                 var itemCount = localStorage.length;                 document.getElementById("count").innerHTML = itemCount;                 for (var i = 0; i < itemCount; i++) {                     var key = localStorage.key(i);                     var val = localStorage[key];                     tableElem.innerHTML += "" + key + ":"                         + val + "";                 }             }              

`

在本例中,我报告了本地存储中的项目数量,并枚举了存储的名称/值对集以填充一个表元素。我添加了两个input元素,当按下Add按钮时,我使用它们的内容来存储项目。为了响应Clear按钮,我清除了本地存储器的内容。你可以在图 39-1 中看到效果。

Image

图 39-1。使用本地存储器

浏览器不会删除我们使用localStorage对象添加的数据,除非用户清除浏览数据。(规范还允许出于安全原因删除数据,但没有明确说明需要删除本地数据的安全问题。)

监听存储事件

通过本地存储功能存储的数据可用于任何具有相同来源的文档。当一个文档对本地存储进行更改时,就会触发storage事件,我们可以在来自相同来源的其他文档中监听该事件,以确保我们能够及时了解更改。

用存储事件调度的对象是一个StorageEvent对象,其成员在表 39-3 中描述。

Image

清单 39-2 显示了一个我保存为storage.html的文档,它监听并分类本地存储对象发出的事件。

清单 39-2。编目本地存储事件

`

             Storage                      table {border-collapse: collapse;}             th, td {padding: 4px;}                                                                                                                                                    
keyoldValuenewValueurlstorageArea
        

**            function handleStorage(e) {** **                var row = "";** **                row += "" + e.key + "";** **                row += "" + e.oldValue + "";** **                row += "" + e.newValue + "";** **                row += "" + e.url + "";** **                row += "" + (e.storageArea == localStorage) + "";** **                tableElem.innerHTML += row;** **            };**              

`

通过共享已更改存储的任何文档的Window对象来触发storage事件。在这个例子中,每次收到一个事件,我都会在一个table元素中添加一个新行——你可以在图 39-2 中看到这个效果。

Image

图 39-2。显示存储事件的详细信息

图中的事件向我展示了如何向本地存储添加新项目。顺序是:

  • 添加新的一对:Banana / Yellow
  • 添加新的一对:Apple / Red
  • 将与Apple相关的值更新为Green
  • 添加新的一对:Cherry / Red
  • 按下Clear按钮(调用clear方法)

您可以看到,当事件中没有要报告的值时,使用了null。例如,当我向存储器中添加一个新项目时,oldValue属性返回null。表中最后一个事件的keyoldValue,newValue属性为null。这是为响应被调用的clear方法而触发的事件,该方法从存储中删除所有项目。

属性告诉我们哪个文档触发了变化,这很有帮助。storageArea属性返回已经改变的Storage对象,它可以是本地或会话存储对象(我稍后将解释会话存储)。对于这个例子,我们只接收来自本地存储对象的事件。

Image 注意事件不会在做出改变的文档中被调度。我猜我们已经知道发生了什么。这些事件只能在来自同一来源的其他文件中找到。

使用会话存储

会话存储就像本地存储一样工作,除了数据对于每个浏览上下文是私有的,并且在文档关闭时被删除。我们通过sessionStorage全局变量访问会话存储,该变量返回一个Storage对象(之前在表 39-2 中描述过)。您可以在清单 39-3 中看到正在使用的会话存储。

清单 39-3。使用会话存储

`

             Example                      body > * {float: left;}             table {border-collapse: collapse; margin-left: 50px}             th, td {padding: 4px;}             th {text-align: right;}             input {border: thin solid black; padding: 2px;}             label {min-width: 50px; display: inline-block; text-align: right;}             #countmsg, #buttons {margin-left: 50px; margin-top: 5px; margin-bottom: 5px;}                            
            
Key:
            
Value:
            
                Add                 Clear             
            

There are items

        

        

                     
Item Count:-

        

            var buttons = document.getElementsByTagName("button");             for (var i = 0; i < buttons.length; i++) {                 buttons[i].onclick = handleButtonPress;             }

            function handleButtonPress(e) {                 switch (e.target.id) {                     case 'add':                         var key = document.getElementById("key").value;                         var value = document.getElementById("value").value; **                        sessionStorage.setItem(key, value);**                         break;                     case 'clear': **                        sessionStorage.clear();**                         break;                 }                 displayData();             }

            function displayData() {                 var tableElem = document.getElementById("data");                 tableElem.innerHTML = ""; **                var itemCount = sessionStorage.length;**                 document.getElementById("count").innerHTML = itemCount;                 for (var i = 0; i < itemCount; i++) { **                    var key = sessionStorage.key(i);** **                    var val = sessionStorage[key];**                     tableElem.innerHTML += "" + key + ":"                         + val + "";                 }             }              

`

这个例子的工作方式与本地存储的例子相同,只是可见性和寿命受到限制。这些限制对如何处理storage事件有影响——请记住,存储事件仅针对共享存储的文档触发。在会话存储的情况下,这意味着事件将只为嵌入的文档触发,比如那些在iframe中的文档。清单 39-4 显示了一个iframe被添加到先前包含 storage.html 文档的例子中。

清单 39-4。通过会话存储使用存储事件

`

             Example                      body > * {float: left;}             table {border-collapse: collapse; margin-left: 50px}             th, td {padding: 4px;}             th {text-align: right;}             input {border: thin solid black; padding: 2px;}             label {min-width: 50px; display: inline-block; text-align: right;}             #countmsg, #buttons {margin-left: 50px; margin-top: 5px; margin-bottom: 5px;}             iframe {clear: left;}                            
            
Key:
            
Value:
            
                Add                 Clear             
` `            

There are items

        

        

                     
Item Count:-

**        **

        

            var buttons = document.getElementsByTagName("button");             for (var i = 0; i < buttons.length; i++) {                 buttons[i].onclick = handleButtonPress;             }

            function handleButtonPress(e) {                 switch (e.target.id) {                     case 'add':                         var key = document.getElementById("key").value;                         var value = document.getElementById("value").value;                         sessionStorage.setItem(key, value);                         break;                     case 'clear':                         sessionStorage.clear();                         break;                 }                 displayData();             }

            function displayData() {                 var tableElem = document.getElementById("data");                 tableElem.innerHTML = "";                 var itemCount = sessionStorage.length;                 document.getElementById("count").innerHTML = itemCount;                 for (var i = 0; i < itemCount; i++) {                     var key = sessionStorage.key(i);                     var val = sessionStorage[key];                     tableElem.innerHTML += "" + key + ":"                         + val + "";                 }             }              

`

您可以在图 39-3 中看到事件是如何报告的。

Image

图 39-3。会话存储中的存储事件

总结

在本章中,我描述了 web 存储特性,它允许我们在浏览器中存储键/值对。这是一个简单的特性,但是本地存储的寿命使它特别有用,特别是对于存储简单的用户偏好。