首页 前端知识 HTML5 编程高级教程(四)

HTML5 编程高级教程(四)

2024-11-04 09:11:35 前端知识 前端哥 436 205 我要收藏

原文:Pro HTML5 Programming

协议:CC BY-NC-SA 4.0

十、使用 Web 工作器 API

JavaScript 是单线程的。因此,长时间的计算(不一定是因为糟糕的代码)将阻塞 UI 线程,使其无法向文本框添加文本、单击按钮、使用 CSS 效果,并且在大多数浏览器中,在控制返回之前无法打开新的选项卡。为了解决这个问题,HTML5 Web 工作器 为 Web 应用提供了后台处理能力,并且通常在单独的线程上运行,以便使用 Web 工作器 的 JavaScript 应用可以利用多核 CPU。将长时间运行的任务分离到 Web 工作器 中也避免了可怕的慢脚本警告,如图 10-1 所示,当 JavaScript 循环持续几秒钟时就会出现这种警告。

Image

***图 10-1。*火狐浏览器中的慢速脚本警告

尽管网络工作者很强大,但也有他们做不了的事情。例如,当一个脚本在 web Worker 内部执行时,它不能访问 Web 页面的window对象(window.document),这意味着 Web Worker 不能直接访问 Web 页面和 DOM API。尽管 Web 工作者不能阻止浏览器 UI,但他们仍然会消耗 CPU 周期,降低系统的响应速度。

假设您想要创建一个 web 应用,它必须执行一些后台数字处理,但是您不希望这些任务干扰 web 页面本身的交互性。使用 Web Worker,您可以生成一个 Web Worker 来执行任务,并添加一个事件侦听器来侦听来自 Web Worker 的消息。

web 工作者的另一个用例可能是一个应用,它侦听来自后端服务器的广播新闻消息,当从后端服务器接收到消息时,将消息发布到主 Web 页面。这个 Web Worker 可能使用 Web 套接字或服务器发送的事件与后端服务器通信。

在这一章中,我们将探索你能对网络工作者做些什么。首先,我们将讨论 Web 工作者是如何工作的,以及在撰写本文时可用的浏览器支持级别。然后,我们将讨论如何使用 API 来创建新的 worker,以及如何在 worker 和产生它的上下文之间进行通信。最后,我们将向您展示如何使用 Web 工作器 构建应用。

浏览器对网络工作者的支持

大多数现代 web 浏览器都支持 Web 工作器。查看网站[caniuse.com](http://caniuse.com)(搜索 Web 工作器)获取最新的支持列表。虽然大多数其他 API 都有 polyfill(仿真)库,例如,对于 HTML5 Canvas,有像excanvas.jsflashcanvas.js这样的库提供 Canvas APIs 的仿真(在幕后使用 Flash),但是对于 Web 工作者来说,仿真没有太大意义。您可以调用您的辅助代码作为辅助,或者在您的页面中内嵌运行相同的代码,阻塞 UI 线程。基于工作人员的页面响应能力的提高可能足以让人们升级到更现代的浏览器(至少我们希望如此)。

使用 Web 工作器 API

在这一节中,我们将更详细地探索 Web 工作器 API 的使用。为了便于说明,我们创建了一个简单的浏览器页面:echo.html。使用 Web Worker 相当简单——创建一个 Web Worker 对象,并传入一个要执行的 JavaScript 文件。在页面中,您设置了一个事件监听器来监听传入的消息和 Web Worker 发布的错误,如果您想从页面与 Web Worker 通信,您可以调用postMessage来传入所需的数据。Web Worker JavaScript 文件中的代码也是如此——必须设置事件处理程序来处理传入的消息和错误,并且通过调用postMessage来处理与页面的通信。

检查浏览器支持

在调用 Web 工作器 API 函数之前,您需要确保浏览器支持您将要做的事情。这样,您可以提供一些替代文本,提示应用的用户使用更新的浏览器。清单 10-1 显示了你可以用来测试浏览器支持的代码。

***清单 10-1。*检查浏览器支持

function loadDemo() {   if (typeof(Worker) !== "undefined") {     document.getElementById("support").innerHTML =             "Excellent! Your browser supports Web 工作器";   } }

在本例中,您在loadDemo函数中测试浏览器支持,该函数可能在页面加载时被调用。对typeof(Worker)的调用将返回窗口的全局Worker属性,如果浏览器不支持 Web 工作器 API,该属性将是未定义的。在本例中,通过用合适的消息更新页面上先前定义的支持元素,页面被更新以反映是否有浏览器支持,如图 10-2 顶部所示。

Image

***图 10-2。*显示是否支持 Web 工作器 的示例

创建网络工作者

Web 工作器 用 JavaScript 文件的 URL 初始化,该文件包含 worker 将要执行的代码。这段代码设置事件侦听器,并与产生它的脚本进行通信。JavaScript 文件的 URL 可以是与主页具有相同来源(相同的方案、主机和端口)的相对或绝对 URL:

worker = new Worker("echoWorker.js");

内联工人

要启动一个 worker,你需要指向一个文件。您可能已经见过一些类型为javascript/worker的脚本元素示例,如下例所示:

 <script id="myWorker" type="javascript/worker">

不要让这种想法欺骗您,以为您可以简单地设置脚本元素的类型,以作为 Web Worker 运行 JavaScript 代码。在这种情况下,类型信息用于通知浏览器及其 JavaScript 引擎而不是解析并运行脚本。事实上,类型也可以是除了text/javascript之外的任何东西。所示的脚本示例是内联 Web 工作器 的构建块——只有当您的浏览器也支持文件系统 API (Blob Builder 或文件编写器)时,才能使用该功能。在这种情况下,您可以通过编程找到脚本块(在前面的例子中,是带有myWorker id 的元素),并将 Web Worker JavaScript 文件写入磁盘。之后,您可以在代码中调用内联 Web Worker。

共享工作者

还有另一种类型的工作者,在撰写本文时还没有得到广泛的支持:共享 Web 工作者。共享 Web Worker 类似于普通的 Web Worker,但是它可以在同一来源的多个页面上共享。共享网络工作者引入了用于PostMessage通信的端口的概念。共享的 Web 工作器 对于相同来源的多个页面(或选项卡)之间的数据同步或者在几个选项卡之间共享长期资源(如 WebSocket)非常有用。

启动共享 Web Worker 的语法如下:

sharedWorker = new SharedWorker(sharedEchoWorker.js');

加载并执行附加 JavaScript

由几个 JavaScript 文件组成的应用可以包含在页面加载时同步加载 JavaScript 文件的<script>元素。但是,因为 Web 工作器 不能访问document对象,所以有一种替代机制可以从 Workers 中同步导入额外的 JavaScript 文件,即importScripts:

importScripts("helper.js");

导入 JavaScript 文件只是将 JavaScript 加载并执行到现有的 worker 中。对importScripts的同一个调用可以导入多个脚本。它们按指定的顺序执行:

importScripts("helper.js", "anotherHelper.js");

与网络工作者交流

一旦产生了 Web Worker,就可以使用postMessage API 向 Web Worker 发送数据和从 Web Worker 接收数据。这与用于跨框架和跨窗口通信的postMessage API 相同。postMessage可用于发送大多数 JavaScript 对象,但不能用于函数或具有循环引用的对象。

假设您想要构建一个简单的 Web worker 示例,该示例允许用户向 Worker 发送消息,Worker 反过来回显该消息。这个例子在现实生活中可能不太有用,但它足以解释构建更复杂的例子所需的概念。图 10-3 显示了这个示例网页和它的 Web Worker 的运行。这个简单页面的代码列在本节的末尾。

Image

***图 10-3。*一个使用网络工作者的简单网页

为了与 Web Worker 建立适当的通信,必须将代码添加到主页(调用 Web Worker 的页面)以及 worker JavaScript 文件中。

编码主页面

为了从页面与 Web Worker 通信,您调用postMessage来传递所需的数据。要侦听 Web Worker 发送到页面的传入消息和错误,需要设置一个事件侦听器。

要在主页和 Web Worker 之间建立通信,首先将对postMessage的调用添加到主页,如下所示:

document.getElementById("helloButton").onclick = function() {     worker.postMessage("Here's a message for you"); }

在前面的例子中,当用户点击 Post a Message 按钮时,一条消息被发送到 Web Worker。接下来,向页面添加一个事件侦听器,用于侦听来自 Web Worker 的消息:

worker.addEventListener("message", messageHandler, true);
function messageHandler(e) {     // process message from worker }

编写 Web Worker JavaScript 文件

您现在必须向 Web Worker JavaScript 文件添加类似的代码——必须设置事件处理程序来处理传入的消息和错误,并且通过调用postMessage来处理与页面的通信。

要完成页面和 Web Worker 之间的通信,首先,添加对postMessage的调用;例如,在一个messageHandler函数中:

function messageHandler(e) {     postMessage("worker says: " + e.data + " too"); }

接下来,向 Web Worker JavaScript 文件添加一个事件侦听器,该文件处理来自主页的消息:

addEventListener("message", messageHandler, true);

在本例中,当收到消息时,立即调用messageHandler函数,以便消息可以被回显。

注意,如果这是一个共享的 worker,您将使用稍微不同的语法(使用一个port):

sharedWorker.port.addEventListener("message", messageHandler, true); sharedWorker.port.postMessage("Hello HTML5");  

此外,工作人员可以监听一个connect事件来接收连接。您可以用它来计算活动连接的数量。

处理错误

Web Worker 脚本中未处理的错误会在 Web Worker 对象上引发错误事件。当您调试使用 Web 工作器 的脚本时,侦听这些错误事件尤其重要。下面显示了一个 Web Worker JavaScript 文件中的错误处理函数示例,该文件将错误记录到控制台:

function errorHandler(e) {     console.log(e.message, e); }

要处理这些错误,您必须向主页添加一个事件侦听器:

worker.addEventListener("error", errorHandler, true);

大多数浏览器还没有一个很好的方法来单步调试 Web Worker 代码,但谷歌 Chrome 在其 Chrome 开发工具中提供了 Web Worker 调试功能(在脚本选项卡中,查找 Worker inspectors),如图图 10-4 所示。

Image

***图 10-4。*Chrome 开发者工具中的 Web Worker 调试选项

停止网络工作者

网络工作者不会自己停下来;但是启动它们的页面可以阻止它们。如果页面关闭,Web 工作器 将被垃圾收集,所以请放心,不会有任何僵尸 Workers 在执行后台任务。但是,当不再需要 Web Worker 时,您可能希望回收资源——可能是当主页被通知 Web Worker 已完成其任务时。您可能还希望取消长时间运行的任务来响应用户操作。调用terminate停止 Web Worker。被终止的 Web Worker 将不再响应消息或执行任何额外的计算。您不能重新启动工作进程;相反,您可以使用相同的 URL 创建一个新的工作进程:

worker.terminate();

在 Web 工作器 中使用 Web 工作器

Worker API 可以在 Web Worker 脚本中用来创建子工作器:

var subWorker = new Worker("subWorker.js");

大量工人

Peter 说:“如果你生成一个工人,而递归地用相同的 JavaScript 源文件生成子工人,至少你会看到一些有趣的结果。”

Image

使用计时器

虽然 Web 工作者不能访问window对象,但是他们可以使用完整的 JavaScript 计时 API,通常可以在全局窗口中找到:

var t = setTimeout(postMessage, 2000, "delayed message");

示例代码

为了完整起见,清单 10-2 和清单 10-3 显示了简单页面和 Web Worker JavaScript 文件的代码。

***清单 10-2。*调用网络工作者的简单 HTML 页面

`

Simple Web 工作器 Example

Simple Web 工作器 Example

Your browser does not support Web 工作器.

Stop Task
Post a Message

`

***清单 10-3。*简单的 Web Worker JavaScript 文件

function messageHandler(e) {     postMessage("worker says: " + e.data + " too"); } addEventListener("message", messageHandler, true);

用 Web 工作器 构建应用

到目前为止,我们一直关注于使用不同的 Web Worker APIs。让我们通过构建一个应用来看看 Web 工作器 API 到底有多强大:一个带有图像模糊过滤器的网页,并行运行在多个 Web 工作器 上。图 10-5 显示了这个应用在你启动时的样子。

Image

***图 10-5。*基于网络工作者的网页,带有图像模糊过滤器

这个应用将图像数据从画布发送到几个 Web 工作器(您可以指定数量)。然后,网络工作人员用简单的模糊滤镜处理图像。这可能需要几秒钟的时间,取决于图像的大小和可用的计算资源(即使是具有快速 CPU 的机器也可能有来自其他进程的负载,导致 JavaScript 执行需要更多的挂钟时间来完成)。图 10-6 显示了运行模糊过滤程序一段时间后的同一页面。

Image

***图 10-6。*运行一段时间后图像模糊的网页

然而,因为繁重的工作发生在 Web 工作器 中,所以不存在缓慢脚本警告的危险,因此,不需要手动将任务划分为调度的片——如果您不能使用 Web 工作器,您将不得不考虑这一点。

编写 blur.js 辅助脚本

blur.js应用页面中,我们可以使用一个模糊过滤器的简单实现,它会一直循环直到完全处理完它的输入,如清单 10-4 所示。

***清单 10-4。*文件 blur.js 中的一个 JavaScript 框模糊实现

`function inRange(i, width, height) {
    return ((i>=0) && (i < widthheight4));
}

function averageNeighbors(imageData, width, height, i) {
    var v = imageData[i];

// cardinal directions
    var north = inRange(i-width4, width, height) ? imageData[i-width4] : v;
    var south = inRange(i+width4, width, height) ? imageData[i+width4] : v;
    var west = inRange(i-4, width, height) ? imageData[i-4] : v;
    var east = inRange(i+4, width, height) ? imageData[i+4] : v;

// diagonal neighbors
    var ne = inRange(i-width4+4, width, height) ? imageData[i-width4+4] : v;
    var nw = inRange(i-width4-4, width, height) ? imageData[i-width4-4] : v;
    var se = inRange(i+width4+4, width, height) ? imageData[i+width4+4] : v;
    var sw = inRange(i+width4-4, width, height) ? imageData[i+width4-4] : v;

// average
    var newVal = Math.floor((north + south + east + west + se + sw + ne + nw + v)/9);

if (isNaN(newVal)) {
        sendStatus("bad value " + i + " for height " + height);
        throw new Error(“NaN”);
    }
    return newVal;
}

function boxBlur(imageData, width, height) {
    var data = [];
    var val = 0;
    for (var i=0; i<widthheight4; i++) {
        val = averageNeighbors(imageData, width, height, i);
        data[i] = val;
    }

return data;
}`

简而言之,这种算法通过平均附近的像素值来模糊图像。对于具有数百万像素的大型图像,这需要大量的时间。在 UI 线程中运行这样的循环是非常不可取的。即使没有出现慢速脚本警告,页面 UI 也不会响应,直到循环终止。由于这个原因,它为 Web 工作器 中的后台计算提供了一个很好的例子。

编码 blur.html 申请页面

清单 10-5 显示了调用 Web Worker 的 HTML 页面的代码。为了清楚起见,这个例子的 HTML 保持简单。这里的目的不是构建一个漂亮的界面,而是提供一个简单的框架,可以控制 Web 工作人员并演示他们的行为。在这个应用中,显示输入图像的 canvas 元素被注入到页面中。我们有按钮来开始模糊图像,停止模糊,重置图像,并指定要繁殖的工人数量。

清单 10-5。【blur.html 页面代码

`

Web 工作器

Web 工作器

Your browser does not support Web 工作器.

Blur
Stop Workers
Reload



    1
    2
    4
    8
    16

`

接下来,让我们将创建 workers 的代码添加到文件blur.html.中,我们实例化了一个worker对象,传入一个 JavaScript 文件的 URL。每个实例化的工作者将运行相同的代码,但是负责处理输入图像的不同部分:

function initWorker(src) {     var worker = new Worker(src);     worker.addEventListener("message", messageHandler, true);     worker.addEventListener("error", errorHandler, true);     return worker; }

让我们将错误处理代码添加到文件blur.html,如下所示。如果 worker 出现错误,页面将能够显示一条错误消息,而不是继续不知道。我们的例子应该不会遇到任何问题,但是监听错误事件通常是一种很好的实践,对于调试非常有价值。

function errorHandler(e) {     log("error: " + e.message); }

编写 blurWorker.js Web Worker 脚本

接下来,我们将工人用来与页面通信的代码添加到文件blurWorker.js(参见清单 10-6 )。当 Web 工作者完成计算块时,他们可以使用postMessage通知页面他们已经取得了进展。我们将使用这些信息来更新主页上显示的图像。创建后,我们的网络工作人员等待包含图像数据和指令的消息开始模糊。这个消息是一个 JavaScript 对象,包含消息类型和用数字数组表示的图像数据。

***清单 10-6。*发送和处理 blurWorker.js 文件中的图像数据

`function sendStatus(statusText) {
    postMessage({“type” : “status”,
                 “statusText” : statusText}
                );
}

function messageHandler(e) {
    var messageType = e.data.type;
    switch (messageType) {
        case (“blur”):
            sendStatus("Worker started blur on data in range: " +
                            e.data.startX + “-” + (e.data.startX+e.data.width));
            var imageData = e.data.imageData;
            imageData = boxBlur(imageData, e.data.width, e.data.height, e.data.startX);

postMessage({“type” : “progress”,
                         “imageData” : imageData,
                         “width” : e.data.width,
                         “height” : e.data.height,
                         “startX” : e.data.startX
                        });
            sendStatus("Finished blur on data in range: " +
                            e.data.startX + “-” + (e.data.width+e.data.startX));
            break;
        default:
            sendStatus("Worker got message: " + e.data);
    }
}
addEventListener(“message”, messageHandler, true);`

与网络工作者交流

在文件blur.html中,我们可以通过向工人发送一些代表模糊任务的数据和参数来使用他们。这是通过使用postMessage发送一个 JavaScript 对象来完成的,该对象包含 RGBA 图像数据的数组、源图像的尺寸以及工人负责的像素范围。每个工作者基于其接收的消息处理图像的不同部分:

`function sendBlurTask(worker, i, chunkWidth) {
        var chunkHeight = image.height;
        var chunkStartX = i * chunkWidth;
        var chunkStartY = 0;
        var data = ctx.getImageData(chunkStartX, chunkStartY,
                                    chunkWidth, chunkHeight).data;

worker.postMessage({‘type’ : ‘blur’,
                            ‘imageData’ : data,
                            ‘width’ : chunkWidth,
                            ‘height’ : chunkHeight,
                            ‘startX’ : chunkStartX});
}`

画布图像数据

Frank 说::postMessage被指定为允许imageData对象的高效序列化,以便与 canvas API 一起使用。一些包含 Worker 和postMessageAPI 的浏览器可能还不支持postMessage的扩展序列化能力。

正因为如此,我们在本章中给出的图像处理例子发送imageData.data(它像 JavaScript 数组一样序列化)而不是发送imageData对象本身。当 Web 工作者计算他们的任务时,他们将状态和结果反馈给页面。清单 10-6 展示了模糊过滤器处理数据后,数据是如何从工作者发送到页面的。同样,该消息包含一个 JavaScript 对象,该对象具有图像数据和坐标字段,用于标记已处理部分的边界。"

在 HTML 页面端,消息处理程序使用这些数据,并用新的像素值更新画布。经过处理的图像数据输入后,结果立即可见。我们现在有了一个示例应用,它可以处理图像,同时有可能利用多个 CPU 内核。此外,我们没有锁定用户界面,使其在 Web 工作人员活动时没有响应。图 10-7 显示了实际应用。

Image

***图 10-7。*模糊应用在行动中

实际应用

要查看这个示例的运行情况,页面blur.html必须由 web 服务器提供(例如,Apache 或 Python 的 SimpleHTTPServer)。下面的步骤展示了如何使用 Python SimpleHTTPServer 运行应用:

  1. 安装 Python。
  2. 导航到包含示例文件(blur.html)的目录。
  3. 启动 Python 如下:python -m SimpleHTTPServer 9999
  4. 打开浏览器并导航至[localhost:9999/blur.html](http://localhost:9999/blur.html)。你现在应该看到如图 10-7 所示的页面。
  5. 如果你让它运行一段时间,你会看到图像的不同象限慢慢模糊。同时模糊的象限数量取决于您启动的工作线程数量。
示例代码

为了完整起见,清单 10-7 、 10-8 和 10-9 包含了示例应用的完整代码。

清单 10-7。【blur.html 文件内容

`

Web 工作器

Web 工作器

Your browser does not support Web 工作器.

Blur
Stop Workers
Reload



    1
    2
    4
    8
    16

var imageURL = “example2.png”;
var image;
var ctx;
var workers = [];

function log(s) {
    var logOutput = document.getElementById(“logOutput”); logOutput.innerHTML = s + “
” + logOutput.innerHTML;
}

function setRunningState§ {
    // while running, the stop button is enabled and the start button is not
    document.getElementById(“startBlurButton”).disabled = p;
    document.getElementById(“stopButton”).disabled = !p;
}

function initWorker(src) {
    var worker = new Worker(src);
    worker.addEventListener(“message”, messageHandler, true);
    worker.addEventListener(“error”, errorHandler, true);
    return worker;
}

function startBlur() {
    var workerCount = parseInt(document.getElementById(“workerCount”).value);
    var width = image.width/workerCount;

for (var i=0; i<workerCount; i++) {
        var worker = initWorker(“blurWorker.js”);
        worker.index = i;
        worker.width = width;
        workers[i] = worker;

sendBlurTask(worker, i, width);
    }
    setRunningState(true);
}

function sendBlurTask(worker, i, chunkWidth) {
        var chunkHeight = image.height;
        var chunkStartX = i * chunkWidth;
        var chunkStartY = 0;
        var data = ctx.getImageData(chunkStartX, chunkStartY,
                                    chunkWidth, chunkHeight).data;

worker.postMessage({‘type’ : ‘blur’,
                            ‘imageData’ : data,
                            ‘width’ : chunkWidth,
                            ‘height’ : chunkHeight,
                            ‘startX’ : chunkStartX});
}

function stopBlur() {
    for (var i=0; i<workers.length; i++) {
        workers[i].terminate();
    }
    setRunningState(false);
} function messageHandler(e) {
    var messageType = e.data.type;
    switch (messageType) {
        case (“status”):
            log(e.data.statusText);
            break;
        case (“progress”):
            var imageData = ctx.createImageData(e.data.width, e.data.height);

for (var i = 0; i<imageData.data.length; i++) {
                var val = e.data.imageData[i];
                if (val === null || val > 255 || val < 0) {
                    log("illegal value: " + val + " at " + i);
                    return;
                }

imageData.data[i] = val;
            }
            ctx.putImageData(imageData, e.data.startX, 0);

// blur the same tile again
            sendBlurTask(e.target, e.target.index, e.target.width);
            break;
        default:
            break;
    }
}

function errorHandler(e) {
    log("error: " + e.message);
}

function loadImageData(url) {

var canvas = document.createElement(‘canvas’);
    ctx = canvas.getContext(‘2d’);
    image = new Image();
    image.src = url;

document.getElementById(“imageContainer”).appendChild(canvas);

image.onload = function(){
        canvas.width = image.width;
        canvas.height = image.height;
        ctx.drawImage(image, 0, 0);
        window.imgdata = ctx.getImageData(0, 0, image.width, image.height);
        n = ctx.createImageData(image.width, image.height);
        setRunningState(false);
        log(“Image loaded: " + image.width + “x” + image.height + " pixels”);
    };
} function loadDemo() {
    log(“Loading image data”);

if (typeof(Worker) !== “undefined”) {
        document.getElementById(“status”).innerHTML = “Your browser supports Web 工作器”;

document.getElementById(“stopButton”).onclick = stopBlur;
        document.getElementById(“startBlurButton”).onclick = startBlur;

loadImageData(imageURL);

document.getElementById(“startBlurButton”).disabled = true;
        document.getElementById(“stopButton”).disabled = true;
    }

}

window.addEventListener(“load”, loadDemo, true);
`

***清单 10-8。*文件内容 blurWorker.js

`importScripts(“blur.js”);

function sendStatus(statusText) {
    postMessage({“type” : “status”,
                 “statusText” : statusText}
                );
}

function messageHandler(e) {
    var messageType = e.data.type;
    switch (messageType) {
        case (“blur”):
            sendStatus("Worker started blur on data in range: " +
                            e.data.startX + “-” + (e.data.startX+e.data.width));
            var imageData = e.data.imageData;
            imageData = boxBlur(imageData, e.data.width, e.data.height, e.data.startX);

postMessage({“type” : “progress”,
                         “imageData” : imageData,
                         “width” : e.data.width,
                         “height” : e.data.height,
                         “startX” : e.data.startX
                        });
            sendStatus("Finished blur on data in range: " +
                            e.data.startX + “-” + (e.data.width+e.data.startX));
            break;
        default:
            sendStatus("Worker got message: " + e.data);
    }
} addEventListener(“message”, messageHandler, true);`

***清单 10-9。*blur . js 文件内容

`function inRange(i, width, height) {
    return ((i>=0) && (i < widthheight4));
}

function averageNeighbors(imageData, width, height, i) {
    var v = imageData[i];

// cardinal directions
    var north = inRange(i-width4, width, height) ? imageData[i-width4] : v;
    var south = inRange(i+width4, width, height) ? imageData[i+width4] : v;
    var west = inRange(i-4, width, height) ? imageData[i-4] : v;
    var east = inRange(i+4, width, height) ? imageData[i+4] : v;

// diagonal neighbors
    var ne = inRange(i-width4+4, width, height) ? imageData[i-width4+4] : v;
    var nw = inRange(i-width4-4, width, height) ? imageData[i-width4-4] : v;
    var se = inRange(i+width4+4, width, height) ? imageData[i+width4+4] : v;
    var sw = inRange(i+width4-4, width, height) ? imageData[i+width4-4] : v;

// average
    var newVal = Math.floor((north + south + east + west + se + sw + ne + nw + v)/9);

if (isNaN(newVal)) {
        sendStatus("bad value " + i + " for height " + height);
        throw new Error(“NaN”);
    }
    return newVal;
}

function boxBlur(imageData, width, height) {
    var data = [];
    var val = 0;

for (var i=0; i<widthheight4; i++) {
        val = averageNeighbors(imageData, width, height, i);
        data[i] = val;
    }

return data;
}`

总结

在本章中,您已经看到了如何使用 Web 工作器 来创建具有后台处理的 Web 应用。本章向您展示了 Web 工作器(以及内联和共享 Web 工作器)是如何工作的。我们讨论了如何使用 API 创建新的 worker,以及如何在 worker 和产生它的上下文之间进行通信。最后,我们向您展示了如何使用 Web 工作器 构建应用。在下一章,我们将演示 HTML5 让你保存数据的本地副本和减少应用中网络开销的更多方法。

十一、使用 Web 存储 API

在这一章中,我们将探索你可以用 HTML5 Web 存储做什么——有时被称为 DOM Storage——这是一个 API,它使得跨 Web 请求保留数据变得容易。在 web 存储 API 出现之前,远程 Web 服务器需要通过在客户机和服务器之间来回发送来存储任何持久的数据。随着 Web 存储 API 的出现,开发人员现在可以将数据直接存储在浏览器的客户端,以便跨请求重复访问,或者在您完全关闭浏览器后很长时间内检索,从而减少网络流量。

我们将首先看看 Web 存储与 cookies 有何不同,然后探讨如何存储和检索数据。接下来,我们将看看localStoragesessionStorage之间的区别,存储接口提供的属性和功能,以及如何处理 Web 存储事件。我们最后看一下 Web SQL 数据库 API 和一些实用的附加内容。

网络存储概述

为了解释 Web 存储 API,最好回顾一下它的前身,名字有趣的 cookie。浏览器 cookie——以在程序之间传递小数据值的古老编程技术命名——是一种在服务器和浏览器之间来回发送文本值的内置方式。服务器可以使用它们放入这些 cookies 中的值来跨网页跟踪用户信息。每当用户访问一个域时,Cookie 值就会来回传输。例如,cookie 可以存储一个会话标识符,通过在浏览器 cookie 中存储一个与服务器自己的购物车数据库相匹配的唯一 ID,允许 web 服务器知道哪个购物车属于某个用户。然后,当用户从一个页面移动到另一个页面时,购物车可以一致地更新。cookies 的另一个用途是将本地值存储到应用中,以便这些值可以在后续的页面加载中使用。

Cookie 值还可以用于用户不太喜欢的操作,比如跟踪用户为了定向广告访问了哪些页面。因此,一些用户要求浏览器包含允许他们随时或针对特定网站阻止或删除 cookies 的功能。

不管你喜不喜欢,早在 20 世纪 90 年代中期网景浏览器的早期,浏览器就已经支持 cookies 了。Cookies 也是少数几个自网络早期以来就一直得到浏览器厂商支持的特性之一。Cookies 允许跨多个请求跟踪数据,只要数据在服务器和浏览器代码之间仔细协调。尽管 cookies 无处不在,但它也有一些众所周知的缺点:

  • Cookies 的大小极其有限。一般来说,在一个 cookie 中只能设置大约 4KB 的数据,这意味着对于像文档或邮件这样的大数据量来说,这是不可接受的。
  • 对于该 cookie 范围内的每个请求,cookie 在服务器和浏览器之间来回传输。这不仅意味着 cookie 数据在网络上是可见的,使它们在未加密时存在安全风险,而且每次加载 URL 时,作为 cookie 保存的任何数据都将消耗网络带宽。因此,相对较小的 cookies 更有意义。

在许多情况下,不需要网络或远程服务器也能达到同样的效果。这就是 HTML5 Web 存储 API 的用武之地。通过使用这个简单的 API,开发人员可以将值存储在容易检索的 JavaScript 对象中,这些对象可以跨页面加载保持不变。通过使用sessionStoragelocalStorage,开发人员可以选择让这些值分别在单个窗口或选项卡中的页面加载中或浏览器重启中存在。存储的数据不通过网络传输,并且在返回访问页面时很容易访问。此外,使用高达几兆字节的 Web 存储 API 值可以持久保存更大的值。这使得 Web 存储适用于文档和文件数据,这些数据会很快突破 cookie 的大小限制。

浏览器对网络存储的支持

Web 存储是 HTML5 最广泛采用的特性之一。事实上,自 2009 年 Internet Explorer 8 发布以来,所有当前发布的浏览器版本都在一定程度上支持网络存储。在本文发表时,不支持存储的浏览器的市场份额正在缩减至个位数百分比。

Web 存储是目前在 web 应用中使用的最安全的新 API 之一,因为它得到了广泛的支持。不过,和往常一样,在使用 Web 存储之前先测试它是否受支持是个好主意。下一节“检查浏览器支持”将向您展示如何以编程方式检查 Web 存储是否受支持。

使用网络存储 API

Web 存储 API 使用起来非常简单。我们将首先介绍值的基本存储和检索,然后继续讨论sessionStoragelocalStorage之间的差异。最后,我们将看看 API 更高级的方面,比如值改变时的事件通知。

检查浏览器支持

给定域的存储数据库直接从window对象访问。因此,确定用户的浏览器是否支持 Web 存储 API 就像检查是否存在window.sessionStoragewindow.localStorage一样简单。清单 11-1 显示了一个例程,它检查存储支持并显示一条关于浏览器对 Web 存储 API 支持的消息。除了使用这些代码,您还可以使用 JavaScript 实用程序库 Modernizr,它可以处理一些可能导致误报的情况。

***清单 11-1。*检查网络存储支持

`function checkStorageSupport() {

//sessionStorage
  if (window.sessionStorage) {
    alert(‘This browser supports sessionStorage’);
  } else {
    alert(‘This browser does NOT support sessionStorage’);
  }

//localStorage
  if (window.localStorage) {
    alert(‘This browser supports localStorage’);
  } else {
    alert(‘This browser does NOT support localStorage’);
  }
}`

图 11-1 显示了对存储支持的检查。

Image

***图 11-1。*检查 Opera 中的浏览器支持

有些浏览器不支持从文件系统直接访问文件的sessionStorage。当你运行本章中的例子时,确保你从一个 web 服务器提供页面!例如,您可以在code/storage目录中启动 Python 的简单 HTTP 服务器,如下所示:

python -m SimpleHTTPServer 9999

之后,您可以在[localhost:9999/](http://localhost:9999)访问文件。比如[localhost:9999/browser-test.html](http://localhost:9999/browser-test.html)

但是,您可以自由地使用任何服务器或 URL 位置来运行这些示例。

`images 注意如果用户在浏览器设置为“私人”模式的情况下浏览,那么一旦浏览器关闭,localStorage 值实际上就不会存在。这是故意的,因为这种模式的用户明确选择不留下任何痕迹。尽管如此,如果存储值在以后的浏览会话中不可用,您的应用应该能够正常响应。

设置和检索值

首先,在您学习设置和检索页面中的简单值时,我们将重点关注会话存储功能。设置一个值可以很容易地在一条语句中完成,我们最初将使用手写符号来编写这条语句:

sessionStorage.setItem(‘myFirstKey', ‘myFirstValue');

此存储访问语句中有几个要点需要注意:

  • 我们可以省略对window的引用,因为存储对象在默认页面上下文中是可用的。
  • 我们正在调用的函数是setItem,它带有一个键字符串和一个值字符串。尽管有些浏览器可能支持传入非字符串值,但规范只允许字符串作为值。
  • 这个特定的调用将把字符串myFirstValue设置到会话存储器中,稍后可以通过键myFirstKey检索该字符串。

为了检索该值,手写符号包括调用getItem函数。例如,如果我们用下面的语句来扩充前面的例子

alert(sessionStorage.getItem(‘myFirstKey'));

浏览器发出显示文本myFirstValue的 JavaScript 警告。如您所见,从 Web 存储 API 设置和检索值非常简单。

然而,有一种更简单的方法来访问代码中的存储对象。您还可以使用 expando-properties 来设置存储中的值。使用这种方法,只需直接在sessionStorage对象上设置和检索对应于键值对的值,就可以完全避免setItemgetItem调用。使用这种方法,我们的值集调用可以重写如下:

sessionStorage.myFirstKey = ‘myFirstValue';

以至

sessionStorage[‘myFirstKey'] = ‘myFirstValue';

类似地,值检索调用可以重写为:

alert(sessionStorage.myFirstKey);

为了可读性,我们将在本章中交替使用这些格式。

这是最基本的。现在,您已经掌握了在应用中使用会话存储所需的所有知识。然而,您可能想知道这个sessionStorage对象有什么特别之处。毕竟,JavaScript 允许您设置和获取几乎任何对象的属性。区别在于范围。您可能没有意识到的是,我们的示例 set 和 get 调用不需要出现在同一个 web 页面中。只要页面是从同一个来源提供的——方案、主机和端口的组合——那么就可以使用相同的键从其他页面检索设置在sessionStorage上的值。这也适用于同一页面的后续加载。作为一名开发人员,您可能已经习惯了这样的想法:每当页面被重新加载时,脚本中所做的更改就会消失。对于在 Web 存储 API 中设置的值来说,这不再适用;它们将跨页面加载继续存在。

堵塞数据漏洞

这些价值观会持续多久?对于设置在sessionStorage中的对象,只要浏览器窗口(或标签)没有关闭,它们就会持续存在。一旦用户关闭窗口——或者浏览器,就此而言——sessionStorage值就会被清除。将一个sessionStorage值看作是一个便签提醒是很有用的。放入sessionStorage的价值不会持续很久,你也不应该放任何真正有价值的东西进去,因为这些价值不能保证在你寻找的时候就在你身边。

那么,为什么您会选择在 web 应用中使用会话存储区呢?会话存储非常适合通常用向导或对话框表示的短期流程。如果您需要存储几个页面中的数据,并且您不希望在用户下次访问您的应用时重新出现这些数据,那么可以随意将它们存储在会话存储区中。在过去,这些类型的值可能通过表单和 cookies 提交,并在每次页面加载时来回传输。使用存储消除了这种开销。

API 还有另一个非常特殊的用途,它解决了困扰许多 web 应用的一个问题:取值范围。举个例子,一个让你购买机票的购物应用。在这样的应用中,可以使用 cookies 在浏览器和服务器之间来回发送理想出发日期和返回日期等偏好数据。这使得服务器能够在用户浏览应用、选择座位和用餐时记住之前的选择。

然而,用户在购买旅游优惠时打开多个窗口,比较不同供应商相同出发时间的航班是很常见的。这在 cookie 系统中引起问题,因为如果用户在比较价格和可用性的同时在浏览器窗口之间来回切换,他们很可能在一个窗口中设置 cookie 值,该值将在下一次操作中意外地应用于从同一 URL 提供的另一个窗口。这有时被称为泄漏数据,是由于 cookies 是基于其存储位置共享的。图 11-2 显示了这是如何发生的。

Image

***图 11-2。*使用旅游网站比价时数据泄露

另一方面,使用sessionStorage允许像出发日期这样的临时值跨访问应用的页面保存,但不会泄漏到用户也在浏览航班的其他窗口。因此,这些偏好将被隔离到预订相应航班的每个窗口。

本地存储与会话存储

有时,应用需要在单个选项卡或窗口的生命周期之外持续存在的值,或者需要在多个视图之间共享的值。在这些情况下,使用不同的 Web 存储实现更合适:localStorage。好消息是你已经知道如何使用localStoragesessionStoragelocalStorage之间唯一的编程区别是访问它们的名字——分别通过sessionStoragelocalStorage对象。主要的行为差异在于价值观持续的时间和分享的方式。表 11-1 显示了两种储存方式的区别。

Image

请记住,浏览器有时会重新定义标签页或窗口的生命周期。例如,当浏览器崩溃时,或者当用户关闭带有许多打开标签的显示时,一些浏览器将保存并恢复当前会话。在这些情况下,当浏览器重启或恢复时,浏览器可以选择保留sessionStorage。所以,实际上,sessionStorage可能比你想象的要长寿!

其他 Web 存储 API 属性和功能

Web 存储 API 是 HTML5 集合中最简单的 API 之一。我们已经研究了从会话和本地存储区域设置和检索数据的显式和隐式方法。让我们通过查看完整的可用属性和函数调用来完成对 API 的调查。

可以从使用它们的文档的window对象中检索出sessionStoragelocalStorage对象。除了它们的名称和值的持续时间,它们在功能上是相同的。两者都实现了Storage接口,如清单 11-2 所示。

***清单 11-2。*存储接口

interface Storage {   readonly attribute unsigned long length;   getter DOMString key(in unsigned long index);   getter any getItem(in DOMString key);   setter creator void setItem(in DOMString key, in any data);   deleter void removeItem(in DOMString key);   void clear(); };

让我们更详细地看看这里的属性和功能。

  • length属性指定存储对象中当前存储了多少个键值对。请记住,存储对象特定于它们的原点,因此这意味着存储对象的项目(和长度)只反映为当前原点存储的项目。

  • key(index)函数允许检索给定的密钥。通常,当您希望遍历特定存储对象中的所有键时,这是最有用的。键是从零开始的,这意味着第一个键位于索引(0)处,最后一个键位于索引(长度–1)处。一旦检索到一个键,就可以用它来获取相应的值。在给定存储对象的生命周期中,键将保留它们的索引,除非移除一个键或它的前一个键。

  • 正如您已经看到的,getItem(key)函数是一种基于给定键检索值的方法。另一种是将键作为数组索引引用到存储对象。在这两种情况下,如果存储中不存在该键,将返回值null

  • Similarly, setItem(key, value) function will put a value into storage under the specified key name, or replace an existing value if one already exists under that key name. Note that it is possible to receive an error when setting an item value; if the user has storage turned off for that site, or if the storage is already filled to its maximum amount, a QUOTA_EXCEEDED_ERR error will be thrown during the attempt. Make sure to handle such an error should your application depend on proper storage behavior. Image

    ***图 11-3。*Chrome 出现配额超标错误

  • The removeItem(key) function does exactly as you might expect. If a value is currently in storage under the specified key, this call will remove it. If no item was stored under that key, no action is taken.

    `images 注意与一些集合和数据框架不同,删除一个项目不会因为调用删除它而返回旧值。确保您已经存储了与删除无关的任何所需副本。

  • 最后,clear()函数从存储列表中删除所有值。在空存储对象上调用此方法是安全的;因此,调用将什么也不做。

磁盘空间配额

Peter 说:“规范建议浏览器允许每个源有 5 兆字节。为了获得更多的空间,浏览器应该在达到配额时提示用户,并且可以为用户提供查看每个源使用了多少空间的方法。

现实中,行为还是有点不一致。一些浏览器默默地允许更大的配额或提示增加空间,而另一些则简单地抛出如图 11-3 中所示的QUOTA_EXCEEDED_ERR错误,而另一些,如图 11-4 中所示的 Opera,实现了一种动态分配更多配额的好方法。本例中使用的测试文件testQuota.html位于code/storage目录中。"

Image

***图 11-4。*歌剧增加临时配额

交流网络存储更新

有时,事情变得有点复杂,存储需要被多个页面、浏览器选项卡或工作人员访问。每当存储值改变时,您的应用可能需要连续触发许多操作。对于这些情况,Web 存储 API 包括一个事件机制,允许将数据更新通知传递给感兴趣的侦听器。对于与存储操作起源相同的每个窗口,Web 存储事件都在 window 对象上触发,而不管侦听窗口本身是否正在执行任何存储操作。

`images 注意 Web 存储事件可以用来在同源的窗口之间进行通信。这将在“实用附加功能”一节中进行更深入的探讨。

要注册接收窗口源的存储事件,只需注册一个事件侦听器,例如:

window.addEventListener("storage", displayStorageEvent, true);

如您所见,名称storage用于表示对存储事件的兴趣。每当针对该来源的Storage事件——无论是sessionStorage还是localStorage——被引发时,任何注册的事件监听器都将接收存储事件作为指定的事件处理程序。存储事件本身的形式如清单 11-3 所示。

***清单 11-3。*存储事件接口

interface StorageEvent : Event {   readonly attribute DOMString key;   readonly attribute any oldValue;   readonly attribute any newValue;   readonly attribute DOMString url;   readonly attribute Storage storageArea; };

StorageEvent对象将是传递给事件处理程序的第一个对象,它包含了理解存储变化本质所需的所有信息。

  • key属性包含存储中更新或删除的键值。
  • oldValue包含与更新前的密钥相对应的先前值,而newValue包含更改后的值。如果该值是新添加的,oldValue将为空,如果该值已被删除,newValue将为空。
  • url将指向storage事件发生的原点。
  • 最后,storageArea提供了对值被改变的sessionStoragelocalStorage的方便引用。这为处理程序提供了一种简单的方法来查询当前值的存储或根据其他存储更改进行更改。

清单 11-4 显示了一个简单的事件处理程序,它会弹出一个警告对话框,显示在页面原点触发的任何存储事件的内容。

***清单 11-4。*显示存储事件内容的事件处理程序

`// display the contents of a storage event
function displayStorageEvent(e) {
  var logged = “key:” + e.key + “, newValue:” + e.newValue + “, oldValue:” +
               e.oldValue +“, url:” + e.url + “, storageArea:” + e.storageArea;

alert(logged);
}

// add a storage event listener for this origin
window.addEventListener(“storage”, displayStorageEvent, true);`

探索网络存储

由于 Web 存储在功能上与 cookies 非常相似,所以最先进的浏览器以非常相似的方式对待它们也就不足为奇了。存储在localStoragesessionStorage中的值可以在最新的浏览器中像浏览 cookies 一样浏览,如图图 11-5 所示。

Image

***图 11-5。*谷歌浏览器资源面板中的存储值

该接口还允许用户根据需要删除存储值,并在访问页面时轻松查看给定网站记录的值。毫不奇怪,Safari 浏览器对 cookies 和存储有类似的统一显示,因为它与 Chrome 基于相同的底层 WebKit 渲染引擎。图 11-6 显示了 Safari 资源面板。

Image

***图 11-6。*Safari 的资源面板中的存储值

像其他浏览器一样,Opera 蜻蜓存储显示器不仅允许用户浏览和删除存储值,还允许用户创建存储值,如图图 11-7 所示。

Image

***图 11-7。*Opera 存储面板中的存储值

随着各种浏览器厂商越来越广泛地实现 Web 存储,用户和开发人员可用的容量和工具都将迅速增加。

使用网络存储构建应用

现在,让我们将您在将存储集成到 web 应用中所学到的东西放在一起。随着应用变得越来越复杂,在没有服务器交互的情况下管理尽可能多的数据变得越来越重要。将数据保存在客户端本地,通过从本地机器而不是远程位置获取数据,减少了网络流量并提高了响应速度。

开发人员面临的一个常见问题是,当用户在应用中从一个页面移动到另一个页面时,如何管理数据。传统上,web 应用通过在服务器上存储数据并在用户导航页面时来回移动数据来实现这一点。或者,应用可能试图将用户保持在单个页面中,并动态更新所有内容。然而,用户容易走神,当用户返回到应用页面时,将数据快速返回到显示中是增强用户体验的一个好方法。

在我们的示例应用中,我们将展示当用户在网站上从一个页面移动到另一个页面时,如何在本地存储临时应用数据,并在每个页面上从存储中快速加载它。为了做到这一点,我们将建立在前几章的例子上。在第五章中,我们展示了收集用户当前位置是多么容易。然后,在第七章中,我们演示了如何获取位置数据并将其发送到远程服务器,以便任何数量的感兴趣的用户都可以查看。在这里,我们将更进一步:我们将侦听通过 WebSocket 传递的广播位置数据,并将其存储在本地存储中,以便当用户从一个页面移动到另一个页面时,可以立即获得这些数据。

想象一下,我们的跑步俱乐部拥有来自其比赛参与者的实时位置信息,这些信息通过他们的移动设备广播并通过 WebSocket 服务器共享。当参赛者在比赛中上传新的位置信息时,web 应用可以很容易地实时显示每个参赛者的当前位置。智能网站会缓存这些比赛位置,以便在用户浏览网站页面时快速显示。这正是我们要建造的。

为了实现这一点,我们需要引入一个演示网站,可以保存和恢复我们的赛车数据。我们已经创建了一个三页的跑步网站示例,并将其放在我们的在线资源文件夹code/storage中,但是您可以使用您选择的任何网站进行演示。这里的关键仅仅是你有多个用户可以轻松访问的网页。我们将在这些页面中插入一些动态内容来表示一个实时的排行榜,或者一个比赛参与者的列表以及他们离终点的当前距离。图 11-8 显示了组成比赛网站的三个页面。

Image

***图 11-8。*榜样竞赛网站

我们的每个网页都将包含一个公共部分来显示排行榜数据。排行榜中的每个条目将显示我们的一名选手的姓名以及他或她目前离终点线的距离。当我们的任何页面被加载时,它将与一个比赛广播服务器建立 WebSocket 连接,并监听指示一个参赛者位置的消息。反过来,参赛者会将他们的当前位置发送到同一个广播服务器,从而使位置数据实时传输到页面。

所有这些都已经在前面与地理定位和 WebSocket 相关的章节中介绍过了。事实上,这里的许多演示代码都与本书前面的例子共享。然而,在这个例子中有一个关键的区别:当数据到达页面时,我们将把它存储在会话存储区中,以便以后检索。然后,每当用户导航到新页面时,在建立新的 WebSocket 连接之前,将检索并显示存储的数据。这样,临时数据就可以在页面之间传输,而无需使用任何 cookies 或 web 服务器通信。

为了保持我们的数据量较小,我们将通过网络以简单的格式发送我们的赛车位置信息,以便于阅读和解析。这种格式是一个String,它使用分号(;)作为分隔符来分隔数据块:名称、纬度和经度。例如,在纬度 37.20 和经度–121.53 的名为 Racer X 的赛车手将使用以下字符串进行标识:

;Racer X;37.20;-121.53

`images 注意一种常见的技术是使用 JSON 格式在客户机和服务器之间发送对象表示。我们将在本章后面的“实用附加功能”一节中告诉你如何去做。

现在,让我们深入研究代码本身。我们的每个页面都将包含相同的 JavaScript 代码,以连接到 WebSocket 服务器,处理和显示排行榜消息,并使用sessionStorage保存和恢复排行榜。因此,这段代码是包含在真实应用的 JavaScript 库中的主要候选代码。

首先,我们将建立一些您以前见过的实用方法。为了计算任何一个特定的参赛者离终点线的距离,我们需要例程来计算两个地理位置之间的距离,如清单 11-5 中的所示。

***清单 11-5。*距离计算例程

`// functions for determining the distance between two
    // latitude and longitude positions
    function toRadians(num) {
      return num * Math.PI / 180;
    }

function distance(latitude1, longitude1, latitude2, longitude2) {
      // R is the radius of the earth in kilometers
      var R = 6371;

var deltaLatitude = toRadians((latitude2-latitude1));
      var deltaLongitude = toRadians((longitude2-longitude1));
      latitude1 = toRadians(latitude1), latitude2 = toRadians(latitude2);

var a = Math.sin(deltaLatitude/2) *
              Math.sin(deltaLatitude/2) +
              Math.cos(latitude1) *               Math.cos(latitude2) *
              Math.sin(deltaLongitude/2) *
              Math.sin(deltaLongitude/2);

var c = 2 * Math.atan2(Math.sqrt(a),
                             Math.sqrt(1-a));
      var d = R * c;
      return d;
    }

// latitude and longitude for the finish line in the Lake Tahoe race
    var finishLat = 39.17222;
    var finishLong = -120.13778;`

在这组熟悉的函数中——之前在第五章中使用过——我们用一个distance函数计算两点之间的距离。这些细节并不特别重要,也不是赛道上距离的最准确表示,但它们对我们的例子来说已经足够了。

在终点线,我们为比赛的终点线位置确定了纬度和经度。正如你将看到的,我们将这些坐标与即将到来的参赛者位置进行比较,以确定参赛者离终点线的距离,从而确定他们在比赛中的排名。

现在,让我们来看一小段用于显示页面的 HTML 标记。

        <h2>Live T216 Leaderboard</h2>         <p id="leaderboardStatus">Leaderboard: Connecting...</p>         <div id="leaderboard"></div>

尽管大部分页面 HTML 与我们的演示无关,但在这几行代码中,我们用 idleaderboardStatusleaderboard声明了一些命名元素。leaderboardStatus是我们显示 WebSocket 连接信息的地方。我们将在排行榜上插入div元素,以指示我们从 WebSocket 消息中接收的位置信息,使用清单 11-6 中所示的实用函数。

***清单 11-6。*位置信息效用函数

`    // display the name and distance in the page
    function displayRacerLocation(name, distance) {
        // locate the HTML element for this ID
        // if one doesn’t exist, create it
        var incomingRow = document.getElementById(name);
        if (!incomingRow) {
            incomingRow = document.createElement(‘div’);
            incomingRow.setAttribute(‘id’, name);
            incomingRow.userText = name;

document.getElementById(“leaderboard”).appendChild(incomingRow);
        }

incomingRow.innerHTML = incomingRow.userText + " is " +
                              Math.round(distance*10000)/10000 + " km from the finish line";
    }`

这个实用程序是一个简单的显示例程,它获取参赛者的名字和离终点线的距离。图 11-9 显示了index.html页面上的引导板部分。

Image

***图 11-9。*竞赛领导委员会

该名称有两个用途:它不仅被放入该赛车手的状态消息中,还被用来引用存储该赛车手状态的唯一的div元素。如果我们的参赛者已经有了一个div,当我们使用标准的document.getElementById()程序查找时,我们会找到它。如果该参赛者的页面中不存在div,我们将创建一个并将其插入到leaderboard区域。无论哪种方式,我们都用离终点线的最新距离更新对应于该赛车手的div元素,这将立即在页面的显示中更新它。如果你已经阅读了第七章,你会从我们在那里创建的示例应用中熟悉这一点。

我们的下一个函数是消息处理器,每当数据从 broadcasting race WebSocket 服务器返回时都会调用它,如清单 11-7 所示。

清单 11-7。 WebSocket 消息处理功能

`    // callback when new position data is retrieved from the websocket
    function dataReturned(locationData) {
        // break the data into ID, latitude, and longitude
        var allData = locationData.split(“;”);
        var incomingId   = allData[1];
        var incomingLat  = allData[2];
        var incomingLong = allData[3];

// update the row text with the new values
        var currentDistance = distance(incomingLat, incomingLong, finishLat, finishLong);

// store the incoming user name and distance in storage
        window.sessionStorage[incomingId] = currentDistance;

// display the new user data in the page
        displayRacerLocation(incomingId, currentDistance);
    }`

这个函数接受前面描述的格式的字符串,一个分号分隔的消息,包含一个参赛者的姓名、纬度和经度。我们的第一步是使用 JavaScript split()例程将它分割成组件,分别产生incomingIdincomingLatincomingLong

接下来,它将赛车的纬度和经度,以及终点线的纬度和经度传递给我们之前定义的distance实用程序方法,将结果距离存储在currentDistance变量中。

现在我们实际上已经有了一些值得存储的数据,我们可以看一下使用 Web 存储的调用。

        // store the incoming user name and distance in storage         window.sessionStorage[incomingId] = currentDistance;

在这一行中,我们使用窗口上的sessionStorage对象将比赛者离终点线的当前距离存储为比赛者姓名和 ID 下的值。换句话说,我们将在会话存储中设置一个值,键是赛车手的名字,值是赛车手离终点的距离。您马上会看到,当用户在 web 站点上从一个页面导航到另一个页面时,将从存储器中检索这些数据。在函数的最后,我们调用我们先前定义的displayLocation()例程,以确保最近的位置更新在当前页面中可视地显示。

现在,让我们看看存储示例中的最后一个函数——清单 11-8 中的加载例程,每当访问者访问网页时就会触发。

***清单 11-8。*初始页面加载例程

`    // when the page loads, make a socket connection to the race broadcast server
    function loadDemo() {
        // make sure the browser supports sessionStorage
        if (typeof(window.sessionStorage) === “undefined”) {
            document.getElementById(“leaderboardStatus”).innerHTML = “Your browser does
                     not support HTML5 Web Storage”;
            return;
        }
        var storage = window.sessionStorage;
        // for each key in the storage database, display a new racer
        // location in the page
        for (var i=0; i < storage.length; i++) {
            var currRacer = storage.key(i);
            displayRacerLocation(currRacer, storage[currRacer]);
        }

// test to make sure that Web Sockets are supported
        if (window.WebSocket) {

// the location where our broadcast WebSocket server is located
            url = “ws://websockets.org:7999/broadcast”;
            socket = new WebSocket(url);
            socket.onopen = function() {
                document.getElementById(“leaderboardStatus”).innerHTML = "Leaderboard:

Connected!";
            }
            socket.onmessage = function(e) {
                dataReturned(e.data);
            }
        }
    }`

这是一个比其他函数更长的函数,并且有很多正在进行的函数。让我们一步一步来。首先,如清单 11-9 所示,我们做了一个基本的错误检查,通过检查查看页面的浏览器在窗口对象上是否支持sessionStorage。如果sessionStorage不可访问,我们简单地更新leaderboardStatus区域来指示,然后退出加载程序。在这个例子中,我们不会试图解决浏览器存储不足的问题。

***清单 11-9。*检查浏览器支持

        // make sure the browser supports sessionStorage         if (typeof(window.sessionStorage) === "undefined") {             document.getElementById("leaderboardStatus").innerHTML = "Your browser does                      not support HTML5 Web Storage";             return;         }

`images 但是,我们的目标是展示存储如何优化用户和网络的体验。

我们在页面加载上做的下一件事是使用存储来检索已经提供给我们网站的这个或其他页面的任何参赛者距离结果。回想一下,我们在每个站点页面上运行相同的脚本代码块,这样,当用户浏览不同的位置时,leader board 会跟随他们。因此,leader board 可能已经将其他页面的值存储到存储器中,这些值将在加载时直接在这里检索和显示,如清单 11-10 所示。只要用户不关闭窗口、选项卡或浏览器,先前保存的值将在导航期间跟随用户,从而清除会话存储。

***清单 11-10。*显示存储的参赛者数据

`        var storage = window.sessionStorage;

// for each key in the storage database, display a new racer
        // location in the page
        for (var i=0; i < storage.length; i++) {
            var currRacer = storage.key(i);
            displayRacerLocation(currRacer, storage[currRacer]);
        }`

这是代码的一个重要部分。这里,我们查询会话的长度——换句话说,存储包含的键的数量。然后,我们使用storage.key()获取每个键,并将其存储到currRacer变量中,稍后使用该变量引用带有storage[currRacer]的键的相应值。键和它的值一起表示一个参赛者和该参赛者的距离,它们存储在对上一页的访问中。

一旦我们有了先前存储的参赛者姓名和距离,我们就使用displayRacerLocation()函数显示它们。这一切在页面加载时发生得非常快,导致页面立即用先前传输的值填充其引导板。

`images 注意我们的示例应用依赖于作为唯一一个将值存储到会话存储区的应用。如果您的应用需要与其他数据共享存储对象,那么您将需要使用一种更细致的键策略,而不是简单地在根级别存储键。我们将在“实用附加功能”一节中探讨另一种储物策略。

我们的最后一个加载行为是使用一个简单的 WebSocket 将页面连接到 racer 广播服务器,如清单 11-11 所示。

***清单 11-11。*连接 WebSocket 广播服务

`        // test to make sure that WebSocket is supported
        if (window.WebSocket) {

// the location where our broadcast WebSocket server is located
            // for the sake of example, we’ll just show websockets.org
            url = “ws://websockets.org:7999/broadcast”;
            socket = new WebSocket(url);
            socket.onopen = function() {
                document.getElementById(“leaderboardStatus”).innerHTML = “Leaderboard:
                         Connected!”;
            }
            socket.onmessage = function(e) {
                dataReturned(e.data);
            }
        }`

正如我们之前在 WebSocket 章节中所做的,我们首先通过检查window.WebSocket对象的存在来确保浏览器支持 WebSocket。一旦我们确认它存在,我们就连接到运行 WebSocket 服务器的 URL。该服务器广播前面列出的分号分隔格式的赛车位置消息,每当我们通过socket.onmessage回调接收到这些消息之一时,我们调用前面讨论过的dataReturned()函数来处理和显示它。我们还使用socket.onopen回调用一条简单的诊断消息来更新我们的leaderboardStatus区域,以表明套接字成功打开。

我们的load套路到此结束。我们在脚本块中声明的最后一个代码块是注册函数,它请求在页面加载完成时调用loadDemo()函数:

    // add listeners on page load and unload     window.addEventListener("load", loadDemo, true);

正如您以前多次看到的,这个事件监听器请求在窗口完成加载时调用我们的loadDemo()函数。

但是,我们如何将赛车数据从赛道传输到广播 WebSocket 服务器并进入我们的页面呢?实际上,我们可以使用之前在 WebSocket 章节中声明的 tracker 示例,只需将其连接 URL 指向之前列出的广播服务器。然而,我们也创建了一个非常简单的 racer 广播源页面,如清单 11-12 所示,它有类似的用途。理论上,这个页面可以在参赛者的移动设备上运行。尽管它本身并不包含任何 Web 存储代码,但当在支持 WebSocket 和地理定位的浏览器中运行时,这是一种传输正确格式化的数据的便捷方式。文件racerBroadcast.html可以从本书提供的网站示例区域获得。

清单 11-12。【racerBroadcast.html 文件内容

`

Racer Broadcast

Racer Broadcast

Racer name:
Start

Geolocation:

HTML5 Geolocation not![Image](https://img-blog.csdnimg.cn/img_convert/2d1278b8ca9e8402e6d6f43f898ace7e.jpeg)  started.

WebSocket:

HTML5 Web Sockets are![Image](https://img-blog.csdnimg.cn/img_convert/75774bd83cf321c3578b1e6b5ddc8f33.jpeg) not supported in your browser.

`

我们不会花太多的篇幅详细讨论这个文件,因为它与第七章中的跟踪器示例几乎相同。主要区别在于该文件包含一个用于输入参赛者姓名的文本字段:

Racer name: <input type="text" id="racerName" value="Racer X"/>

现在,参赛者的姓名作为数据字符串的一部分发送到广播服务器:

var toSend =    ";" + document.getElementById("racerName").value                     + ";" + latitude + ";" + longitude;

要自己尝试一下,在支持 Web 存储、地理定位和 WebSocket 的浏览器中打开两个窗口,比如 Google Chrome。首先,加载跑步俱乐部的index.html页面。您将看到它使用 WebSocket 连接到比赛广播站点,然后等待任何参赛者数据通知。在第二个窗口中,打开racerBroadcast.html文件。在这个页面连接到 WebSocket 广播站点后,输入您的参赛者的名字,然后单击 Start 按钮。你会看到赛车手广播已经传送了你最喜欢的赛车手的位置,它应该会出现在你的另一个浏览器窗口的排行榜上。图 11-10 显示了这个样子。

Image

***图 11-10。*比试佩奇和 racerBroadcast.html 并排

现在,使用页面左侧的注册和关于比赛链接导航到其他赛车俱乐部页面。因为所有这些页面都已被配置为加载我们的脚本,所以它们将立即加载并使用以前的参赛者数据填充排行榜,这些数据是在浏览其他页面时提交的。发送更多的参赛者状态通知(从广播页面),当你导航时,你也会看到它们通过俱乐部网站页面传播。

现在我们已经完成了代码,让我们回顾一下我们构建了什么。我们已经创建了一个简单的功能块,适合包含在一个共享的 JavaScript 库中,它连接到一个 WebSocket 广播服务器并监听 racer 更新。当收到一个更新时,脚本显示页面中的位置并且使用sessionStorage存储它。当加载页面时,它检查任何先前存储的 racer 位置值,从而在用户导航站点时保持状态。我们从这种方法中获得了哪些好处?

  • *减少网络流量:*比赛信息存储在浏览器本地。一旦它到达,它就停留在每次页面加载,而不是使用 cookies 或服务器请求再次获取它。
  • *立即显示值:*浏览器页面本身可以缓存,而不是从网络加载,因为页面的动态部分——当前排行榜状态——是本地数据。这些数据可以快速显示,无需任何网络加载时间。
  • 临时存储:比赛结束后,比赛数据就没什么用了。因此,我们将它存储在会话存储区,这意味着当窗口或选项卡关闭时,它将被丢弃,不再占用任何空间。

关于防弹的一句话

Brian 说:“在这个例子中,我们只用了几行脚本代码就完成了很多工作。但是不要以为在一个真实的、可公开访问的网站上,一切都这么简单。我们采用了一些生产应用无法接受的捷径。

例如,我们的消息格式不支持名称相似的参赛者,最好用代表每个参赛者的唯一标识符来代替。我们的距离计算是“直线的”,并不能真正反映越野比赛的进展。标准免责声明适用-更多的本地化,更多的错误检查,更多的关注细节将使您的网站为所有参与者服务。"

我们在本例中演示的相同技术可以应用于任何数量的数据类型:聊天、电子邮件和体育比分是可以使用本地或会话存储进行缓存和逐页显示的其他示例,正如我们在这里展示的那样。如果您的应用定期在浏览器和服务器之间来回发送特定于用户的数据,请考虑使用 Web 存储来简化您的流程。

浏览器数据库存储的未来

键值存储 API 非常适合持久化数据,但是可以查询的索引存储呢?HTML5 应用最终也会访问索引数据库。数据库 API 的具体细节仍在酝酿中,有两个主要的提议。

Web SQL 数据库

其中一个提议,Web SQL 数据库,已经在 Safari、Chrome 和 Opera 中实现。表 11-2 显示了浏览器对 Web SQL 数据库的支持。

Image

Web SQL 数据库允许应用通过异步 JavaScript 接口访问 SQLite。虽然它不是通用 Web 平台的一部分,也不是 HTML5 应用最终推荐的数据库 API,但 SQL API 在针对特定平台(如 mobile Safari)时会很有用。无论如何,这个 API 在浏览器中展示了数据库的威力。就像其他存储 API 一样,浏览器可以限制每个源的可用存储量,并在用户数据被清除时清除数据。

Web SQL 数据库的命运

Frank 说:“尽管 Web SQL DB 已经存在于 Safari、Chrome 和 Opera 中,但它不会在 Firefox 中实现,并且在 WHATWG wiki 上被列为‘已停止’。该规范定义了一个 API 来执行以字符串形式给出的 SQL 语句,并遵从 SQL 方言的 SQLite。由于不希望标准要求特定的 SQL 实现,所以 Web SQL 数据库已经被更新的规范所超越,即索引数据库(以前称为 WebSimpleDB ),它更简单并且不依赖于特定的 SQL 数据库版本。索引数据库的浏览器实现目前正在进行中,我们将在下一节讨论它们。”

因为 Web SQL 数据库已经在野外实现了,所以我们包括了一个基本的例子,但是省略了 API 的完整细节。这个例子演示了 Web SQL 数据库 API 的基本用法。它打开一个名为mydb的数据库,创建一个racers表(如果这个名称的表还不存在),并用一个预定义名称的列表填充这个表。图 11-11 显示了 Safari 的 Web Inspector 中带有 racers 表的数据库。

Image

***图 11-11。*Safari 浏览器中带有参赛者表格的数据库

首先,我们按名称打开一个数据库。window.openDatabase()函数返回一个Database对象,通过它进行数据库交互。openDatabase()函数接受一个名称以及可选的版本和描述。对于开放的数据库,应用代码现在可以启动事务。使用transaction.executeSql()函数在事务上下文中执行 SQL 语句。这个简单的例子使用executeSql()创建一个表,将个赛车手的名字插入表中,然后查询数据库创建一个 HTML 表。图 11-12 显示了从表格中检索到的姓名列表的输出 HTML 文件。

Image

图 11-12。【sql.html 展示参赛者评选结果

数据库操作可能需要一些时间才能完成。查询在后台运行,而不是在结果集可用之前阻止脚本执行。当结果可用时,作为第三个参数给定给executeSQL()的函数被回调,事务和结果集作为参数。

清单 11-13 显示了文件sql.html的完整代码;所示的示例代码也位于code/storage文件夹中。

***清单 11-13。*使用 Web SQL 数据库 API

`

Web SQL Database

// open a database by name
    var db = openDatabase('db', '1.0', 'my first database', 2 * 1024 * 1024);
    function log(id, name) {
        var row = document.createElement(“tr”);
        var idCell = document.createElement(“td”);
        var nameCell = document.createElement(“td”);
        idCell.textContent = id;
        nameCell.textContent = name;
        row.appendChild(idCell);
        row.appendChild(nameCell);

document.getElementById(“racers”).appendChild(row);     }

function doQuery() {
        db.transaction(function (tx) {
                tx.executeSql(‘SELECT * from racers’, [], function(tx, result) {
                    // log SQL result set
                    for (var i=0; i<result.rows.length; i++) {
                        var item = result.rows.item(i);
                        log(item.id, item.name);
                    }
                });
            });
    }

function initDatabase() {
        var names = [“Peter Lubbers”, “Brian Albers”, “Frank Salim”];

db.transaction(function (tx) {
                tx.executeSql(‘CREATE TABLE IF NOT EXISTS racers (id integer primary keyImage
autoincrement, name)’);

for (var i=0; i<names.length; i++) {
                    tx.executeSql(‘INSERT INTO racers (name) VALUES (?)’, [names[i]]);
                }

doQuery();
            });
    }

initDatabase();

Web SQL Database

         
IdName
`
索引数据库 API

第二个关于浏览器数据库存储的提议在 2010 年获得了关注。索引数据库 API 受到微软和 Mozilla 的支持,被视为 Web SQL 数据库的一个计数器。Web SQL 数据库希望将已建立的 SQL 语言引入浏览器,而索引数据库旨在引入低级索引存储功能,希望在索引核心之上构建更多开发人员友好的库。

Web SQL API 支持使用查询语言对数据表发出 SQL 语句,而索引 DB API 直接对树状对象存储引擎发出同步或异步函数调用。与 Web SQL 不同,索引数据库不处理表和列。

对索引数据库 API 的支持正在增加(见表 11-3 )。

微软和 Mozilla 已经宣布他们将不支持 Web SQL 数据库,而是支持索引数据库。谷歌的 Chrome 也加入了支持,因此,索引数据库很可能是浏览器中标准化结构化存储的未来。他们的理由包括 SQL 不是真正的标准,以及 Web SQL 的唯一实现是 SQLite 项目。只有一个实现和一个松散的标准,他们不能在 HTML5 规范中支持 WebSQL。

索引数据库 API 避开了查询字符串,支持将值直接存储在 JavaScript 对象中的低级 API。存储在数据库中的值可以通过键或使用索引来检索,并且可以以同步或异步方式访问 API。与 WebSQL 提案一样,索引数据库的范围是由源确定的,因此您只能访问在您自己的 web 页面中创建的存储。

索引数据库存储的创建或修改是在事务的上下文中完成的,事务可以分为只读、读写或版本更改。虽然前两个可能是不言自明的,但是只要操作将修改数据库的结构,就会使用 VERSION_CHANGE 事务类型。

从索引数据库中检索记录是通过游标对象完成的。游标对象以递增或递减的顺序遍历一系列记录。在任何时候,游标要么有值,要么没有值,因为它要么正在加载,要么已经到达迭代的末尾。

索引数据库 API 的详细描述超出了本书的范围。如果您打算在内置 API 之上实现一个查询引擎,您应该参考官方规范,否则,您应该等待一个基于标准之上的建议引擎,以使用一个对开发人员更友好的数据库 API。在这一点上,没有第三方库获得突出地位或重要支持。

为什么要用锤子…

Brian 说:“…当你可以使用这些金属锭、熔炉和你选择的模具时?在 Mozilla 博客上,阿伦·阮冈纳赞认为他会欢迎像 Web SQL API 这样建立在索引数据库标准之上的 API。这种态度困扰了许多开发人员,因为人们普遍认为,为了使索引数据库可用,需要在标准之上构建第三方 JavaScript 库。对于大多数 web 开发人员来说,索引数据库本身太复杂了,无法以当前的形式使用它。

这就引出了一个问题:如果开发人员最终需要第三方库来利用内置的存储 API,那么简单地用本机代码构建存储,而不是作为必须在运行时下载和解释的 JavaScript 库,难道不是明智的吗?时间会证明索引数据库是否适合大多数人的需求。"

实用的临时演员

有时,有些技术并不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。

JSON 对象存储

尽管 Web 存储规范允许将任何类型的对象存储为键值对,但在当前的实现中,一些浏览器将值限制为文本字符串数据类型。但是,有一个实用的解决方法,因为现代版本的浏览器包含对 JavaScript 对象符号(JSON)的内置支持。

JSON 是数据交换的标准,可以将对象表示为字符串,反之亦然。十多年来,JSON 一直被用来通过 HTTP 将对象从浏览器客户端传输到服务器。现在,我们可以用它来序列化 Web 存储中的复杂对象,以便持久化复杂的数据类型。考虑清单 11-14 中的脚本块。

清单 11-14。 JSON 对象存储

`

var data;

function loadData() {
    data = JSON.parse(sessionStorage[“myStorageKey”])
  }

function saveData() {
    sessionStorage[“myStorageKey”] = JSON.stringify(data);
  }

window.addEventListener(“load”, loadData, true);
  window.addEventListener(“unload”, saveData, true);

`

如您所见,该脚本包含事件侦听器,用于在浏览器窗口中注册加载和卸载事件的处理程序。在这种情况下,处理程序分别调用loadData()saveData()函数。

loadData()函数中,向会话存储区查询存储键的值,并将该键传递给JSON.parse()函数。JSON.parse()例程将获取一个先前保存的对象的字符串表示,并将其重组为原始对象的副本。每次页面加载时都会调用这个例程。

类似地,saveData()函数接受一个数据值,并对其调用JSON.stringify(),将其转换为对象的字符串表示。该字符串又被存储回存储器中。通过在unload浏览器事件上注册 saveData()函数,我们确保它在用户每次导航离开或关闭浏览器或窗口时被调用。

这两个函数的实际结果是,我们希望在存储中跟踪的任何对象,无论它是否是复杂的对象类型,都可以在用户进出应用时存储和重新加载。这允许开发人员将我们已经展示的技术扩展到非文本数据。

分享的窗口

正如前面提到的,Web 存储事件能够在浏览相同来源的任何窗口中触发,这具有一些强大的含义。这意味着存储可以用来在窗口之间发送消息,即使它们并不都使用存储对象本身。这反过来意味着我们现在可以跨具有相同来源的窗口共享数据。

让我们使用一些代码示例来看看这是如何工作的。为了监听跨窗口消息,一个简单的脚本只需要注册一个存储事件的处理程序。让我们假设在[www.example.com/storageLog.html](http://www.example.com/storageLog.html)运行的页面包含清单 11-15 中的所示的代码(本例中的示例文件storageLog.html也位于code/storage文件夹中)。

***清单 11-15。*使用存储的跨窗口通信

`// display records of new storage events
function displayStorageEvent(e) {
  var incomingRow = document.createElement(‘div’);
  document.getElementById(“container”).appendChild(incomingRow);

var logged = “key:” + e.key + “, newValue:” + e.newValue + “, oldValue:” +
                e.oldValue + “, url:” + e.url + “, storageArea:” + e.storageArea;
                incomingRow.innerHTML = logged;
}

// add listeners on storage events
window.addEventListener(“storage”, displayStorageEvent, true);`

在为storage事件类型注册一个事件监听器之后,该窗口将接收任何页面中存储变化的通知。例如,如果正在浏览同一原点的浏览器窗口设置或更改了新的存储值,storageLog.html页面将收到通知。因此,要向接收窗口发送消息,发送窗口只需修改一个存储对象,其新旧值将作为通知的一部分发送。例如,如果使用localStorage.setItem()更新一个存储值,那么位于同一原点的storageLog.html页面中的displayStorageEvent()处理程序将接收一个事件。通过仔细协调事件名称和值,这两个页面现在可以进行通信,这在以前是很难实现的。图 11-13 显示了运行中的storageLog.html页面,简单地记录它接收到的存储事件。

Image

***图 11-13。*storageLog.html 页面日志存储事件概述

总结

在本章中,我们展示了如何使用 Web 存储作为浏览器 cookies 的替代方案,在窗口、标签和(用localStorage)甚至浏览器重启之间保存数据的本地副本。您已经看到,可以通过使用sessionStorage在窗口之间适当地隔离数据,并通过使用存储事件共享数据——甚至跨窗口共享。在我们的完整示例中,我们展示了一种实用的方法,可以在用户浏览网站时使用存储来逐页跟踪数据,这也可以很容易地应用于其他数据类型。我们甚至演示了在页面加载或卸载时如何存储非文本数据类型,以便在不同的访问中保存和恢复页面的状态。

在下一章,我们将向您展示 HTML5 如何让您创建离线应用。

十二、创建 HTML5 脱机 Web 应用

在这一章中,我们将探索你可以用离线 HTML5 应用做什么。HTML5 应用不一定需要持续访问网络,加载缓存资源现在可以由开发人员更灵活地控制。

html 5 离线 Web 应用概述

使用应用缓存的第一个也是最明显的原因是离线支持。在普遍连接的时代,离线应用仍然是可取的。没有网络连接时,您会做什么?在您说间歇性连接的时代已经结束之前,请考虑以下几点:

  • 你乘坐的所有航班都有机上无线网络吗?
  • 你的移动互联网设备有完美的信号覆盖吗(你最后一次看到零信号是什么时候)?
  • 你做报告时能指望有互联网连接吗?

随着越来越多的应用转移到 Web 上,假设所有用户 24/7 不间断连接是很诱人的,但互联网的现实是中断时有发生,而且在像航空旅行这样的情况下,可以预见一次会发生几个小时。

间歇性连接一直是网络计算系统的致命弱点。如果您的应用依赖于与远程主机的通信,而这些主机是不可达的,那么您就不走运了。但是,当您连接到互联网时,web 应用总是最新的,因为每次使用时代码都是从远程位置加载的。

如果您的应用只需要偶尔的通信,只要应用资源存储在本地,它们仍然是有用的。随着纯浏览器设备的出现,在没有持续连接的情况下继续运行的 web 应用只会变得更加重要。历史上,不需要持续连接的桌面应用比 web 应用更有优势。

HTML5 公开了对应用缓存的控制,以便两全其美:用 web 技术构建的应用可以在浏览器中运行,在线时可以更新,但也可以离线使用。但是,必须显式地使用这个新的脱机应用特性,因为当前的 web 服务器没有为脱机应用提供任何默认的缓存行为。

HTML5 离线应用缓存使得在没有网络连接的情况下运行应用成为可能。你不需要连接到互联网只是为了起草一封电子邮件。HTML5 引入了离线应用缓存,允许 Web 应用在没有网络连接的情况下运行。

应用开发人员可以指定包含 HTML5 应用的特定附加资源(HTML、CSS、JavaScript 和图像),以使应用可供离线使用。这有许多使用案例,例如:

  • 阅读和撰写电子邮件
  • 编辑文档
  • 编辑和显示演示文稿
  • 创建待办事项列表

使用离线存储可以避免加载应用所需的正常网络请求。如果缓存清单是最新的,浏览器知道它不需要检查其他资源是否也是最新的,并且大部分应用可以从本地应用缓存中快速加载。此外,从缓存中加载资源(而不是发出多个 HTTP 请求来查看资源是否已经更新)可以节省带宽,这对移动 web 应用尤其重要。目前,与桌面应用相比,web 应用的加载速度较慢。缓存可以弥补这一点。

应用缓存为开发人员提供了对缓存的明确控制。缓存清单文件允许您将相关资源分组到一个逻辑应用中。这是一个强大的概念,可以赋予 web 应用一些桌面应用的特征。你可以用新的、创造性的方式使用这种额外的力量。

缓存清单文件中标识的资源创建了所谓的应用缓存,这是浏览器持久存储资源的地方,通常在磁盘上。一些浏览器为用户提供了查看应用缓存中数据的方法。例如,Firefox 内部about:cache页面中的离线缓存设备部分向您展示了关于应用缓存的细节,以及查看缓存中单个文件的方法,如图图 12-1 所示。

Image

***图 12-1。*在 Firefox 中查看应用缓存条目

类似地,内部页面chrome://appcache-internals/提供了关于存储在系统上的不同应用缓存内容的详细信息。它还提供了查看内容和完全移除这些缓存的方法,如图 12-2 所示。

Image

***图 12-2。*在 Chrome 中查看应用缓存条目

浏览器支持 HTML5 离线网络应用

有关当前浏览器支持(包括移动支持)的完整概述,请参考[caniuse.com](http://caniuse.com)并搜索离线 Web 应用或应用缓存。如果您必须支持旧的浏览器,那么在使用 API 之前,最好先看看是否支持应用缓存。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。

使用 HTML5 应用缓存 API

在这一节中,我们将探索如何使用离线 Web 应用 API 的细节。

检查浏览器支持

在尝试使用脱机 Web 应用 API 之前,最好检查一下浏览器支持。清单 12-1 展示了如何做到这一点。

***清单 12-1。*检查浏览器对离线 Web 应用 API 的支持

if(window.applicationCache) {   // this browser supports offline applications }

创建简单的离线应用

假设您想要创建一个包含 HTML 文档、样式表和 JavaScript 文件的单页应用。为了给你的 HTML5 应用添加离线支持,你需要在html元素中包含一个manifest属性,如清单 12-2 所示。

***清单 12-2。*HTML 元素上的清单属性

`

  .   .   . `

在 HTML 文档旁边,提供一个带有扩展名*.appcache的清单文件,指定要缓存哪些资源。清单 12-3 显示了一个示例缓存清单文件的内容。

***清单 12-3。*示例缓存清单文件的内容

CACHE MANIFEST example.html example.js example.css example.gif

离线

为了让应用知道间歇性连接,HTML5 浏览器还公开了其他事件。您的应用可能有不同的在线和离线行为模式。对window.navigator对象的一些添加使得这变得更容易。首先,navigator.onLine是一个布尔属性,表示浏览器是否认为自己在线。当然,onLinetrue值并不能确定 web 应用必须与之通信的服务器可以从用户的机器上到达。另一方面,false值意味着浏览器甚至不会尝试通过网络连接。清单 12-4 显示了如何检查你的页面是在线还是离线。

***清单 12-4。*检查在线状态

`// When the page loads, set the status to online or offline
function loadDemo() {
  if (navigator.onLine) {
    log(“Online”);
  } else {
    log(“Offline”);
  }
}

// Now add event listeners to notify a change in online status
window.addEventListener(“online”, function(e) {
  log(“Online”); }, true);

window.addEventListener(“offline”, function(e) {
  log(“Offline”);
}, true);`

清单文件

脱机应用由一个清单组成,该清单列出了浏览器将缓存以供脱机使用的一个或多个资源。清单文件具有 MIME 类型text/cache-manifest。Python 标准库中的SimpleHTTPServer模块将提供带有.manifest扩展名和头文件Content-type: text/cache-manifest的文件。要配置设置,打开文件PYTHON_HOME/Lib/mimetypes.py,并添加以下行:

'.appcache'    : 'text/cache-manifest manifest',

其他 web 服务器可能需要额外的配置。例如,对于 Apache HTTP Server,您可以通过添加以下行来更新 conf 文件夹中的mime.types文件:

text/cache-manifest appcache

如果您使用的是 Microsoft IIS,在您网站的主页中,双击 MIME 类型图标,然后在添加 MIME 类型对话框中添加 MIME 类型为text/cache-manifest.appcache扩展名。

清单语法是以CACHE MANIFEST(作为第一行)开始的简单的行分隔文本。行可以以CRLFCRLF结尾——格式很灵活——但是文本必须是 UTF-8 编码的,这是大多数文本编辑器的典型输出。注释以哈希符号开始,并且必须在自己的行上;您不能将注释附加到文件中的其他非注释行。

***清单 12-5。*包含所有可能部分的示例清单文件

`CACHE MANIFEST

files to cache

about.html
html5.css
index.html
happy-trails-rc.gif
lake-tahoe.JPG

#do not cache signup page
NETWORK
signup.html

FALLBACK
signup.html     offline.html
/app/ajax/      default.html`

让我们看看不同的部分。

如果没有指定CACHE:标题,列出的文件将被视为要缓存的文件(缓存是默认行为)。下面的简单清单指定必须缓存三个文件(index.htmlapplication.jsstyle.css):

CACHE MANIFEST index.html application.js style.css

类似地,下面的部分将做同样的事情(如果您愿意,可以在一个清单文件中多次使用相同的CACHENETWORKFALLBACK头):

`CACHE MANIFEST

Cache section

CACHE:
Index.html
application.js
style.css`

通过在CACHE部分列出一个文件,您指示浏览器从应用缓存中提供文件,即使应用在线。没有必要指定应用的主 HTML 资源。最初指向清单文件的 HTML 文档被隐式包含在内(这称为主条目)。但是,如果您希望缓存多个 HTML 文档,或者希望多个 HTML 文档作为可缓存应用的可能入口点,那么它们都应该在缓存清单文件中明确列出。

FALLBACK条目允许您给出替代路径来替换无法获取的资源。清单 12-5 中的清单会导致对/app/ajax/或以/app/ajax/开头的子路径的请求在/app/ajax/*不可达时退回到default.html

NETWORK指定总是使用网络获取的资源。简单地从清单中省略这些文件的区别在于,主条目被缓存,而没有在清单文件中显式列出。为了确保应用从服务器请求文件,即使缓存的资源缓存在应用缓存中,您可以将该文件放在NETWORK:部分。

应用缓存 API

ApplicationCache API 是使用应用缓存的接口。一个新的window.applicationCache对象触发了几个与缓存状态相关的事件。该对象有一个数字属性window.applicationCache.status,表示缓存的状态。缓存可以有六种状态,如表 12-1 所示。

Image

今天,网络上的大多数页面都没有指定缓存清单,并且是未缓存的。Idle 是具有缓存清单的应用的典型状态。处于空闲状态的应用的所有资源都由浏览器存储,没有更新正在进行。如果曾经有一个有效的缓存,但是清单现在丢失了,则缓存进入过时状态。API 中有对应于这些状态的事件(和回调属性)。例如,当缓存在更新后进入空闲状态时,就会触发缓存事件。此时,应用可能会通知用户,他们可以断开网络连接,但仍然希望应用在脱机模式下可用。表 12-2 显示了一些常见事件及其相关的缓存状态。

Image

此外,当没有可用的更新或发生错误时,还有指示更新进度的事件:

  • onerror
  • onnoupdate
  • onprogress

window.applicationCache有一个update()方法。调用update()请求浏览器更新缓存。这包括检查清单文件的新版本,并在必要时下载新资源。如果没有缓存或者缓存过时,将会引发错误。

应用缓存正在运行

尽管创建清单文件并在应用中使用它相对简单,但是在服务器上更新页面时发生的事情并不像您想象的那样直观。要记住的主要事情是,一旦浏览器成功地将应用的资源缓存在应用缓存中,它将总是首先从缓存中提供这些页面。之后,浏览器将只做一件事:检查服务器上的清单文件是否已被更改。

为了更好地理解这个过程是如何工作的,让我们使用清单 12-5 中显示的清单文件来一步步完成一个示例场景。

  1. 当您第一次访问index.html页面时(在线时),比如说在[www.example.com](http://www.example.com),浏览器会加载页面及其子资源(CSS、JavaScript 和图像文件)。
  2. 在解析页面时,浏览器遇到 html 元素中的 manifest 属性,并继续加载在example.com站点的应用缓存的缓存(默认)和回退部分中列出的所有文件(浏览器允许大约 5 MB 的存储空间)。
  3. 从现在开始,当你导航到www.example.com时,浏览器将总是从应用缓存中加载站点,然后它将尝试检查清单文件是否已经更新(它只能在你在线时进行后者)。这意味着,如果你现在离线(自愿或不自愿),并将浏览器指向 http://www.example.com 的,浏览器将从应用缓存中加载该网站——是的,你仍然可以在离线模式下完整地使用该网站。
  4. 如果您尝试在脱机时访问缓存的资源,它将从应用缓存中加载。当您尝试访问网络资源(signup.html)时,将提供回退内容(offline.html)。只有当您重新联机时,网络文件才再次可用。
  5. 目前为止一切顺利。一切按预期运行。我们现在将试着带你穿过当你改变服务器上的内容时必须跨越的数字雷区。例如,当您更改服务器上的 about.html 页面,并通过在浏览器中重新加载该页面以在线模式访问该页面时,有理由期待更新后的页面出现。毕竟,你是在线的,可以直接访问服务器。然而,你只会看到和以前一样的旧页面,脸上可能带着困惑的表情。这是因为浏览器总是从应用缓存中加载页面,之后它只检查一件事:清单文件是否已经更新。因此,如果您希望下载更新的资源,您还必须对清单文件进行更改(不要只是“触摸”该文件,因为这不会被视为更改—它必须是逐字节的更改)。进行这种更改的一种常见方式是在文件顶部添加版本注释,如清单 12.5 所示。浏览器实际上并不理解版本注释,但这是一个很好的最佳实践。由于这个原因,也由于很容易忽略新的或删除的文件,建议您使用某种构建脚本来维护清单文件。html 5 Boilerplate 2.0(html5boilerplate.com)附带了一个构建文件,可以用来自动构建和版本化 appcache 文件,这是对已经很棒的资源的一个很好的补充。
  6. 当您对 about.html 页面和清单文件都进行了更改,并随后在线刷新浏览器中的页面时,您将再次失望地看到相同的旧页面。发生了什么事?尽管浏览器现在已经发现清单已经更新,并且将所有文件再次下载到新版本的缓存中,但是在执行服务器检查之前,页面已经从应用缓存中加载,并且浏览器不会自动在浏览器中为您重新加载页面。您可以将此过程与如何在后台下载新版本的软件程序(例如 Firefox 浏览器)进行比较,但需要重启程序才能生效。如果不能等待下一次页面刷新,可以通过编程方式为 onupdateready 事件添加一个事件侦听器,并提示用户刷新页面。一开始有点困惑,但仔细想想就明白了。

使用应用缓存提升性能

Peter 说:“应用缓存机制的一个很好的副作用是你可以用它来预取资源。常规浏览器缓存存储您访问过的页面,但存储的内容取决于客户端和服务器配置(浏览器设置和过期标题)。因此,至少可以说,依靠常规浏览器缓存返回特定页面是不稳定的——任何曾经试图依靠常规浏览器缓存在飞机上浏览网站页面的人可能都会同意这一点。

然而,使用应用缓存,你不仅可以在访问页面时缓存它们,还可以缓存你还没有访问过的页面*;它可以作为一种有效的预取机制。当需要使用这些预取的资源时,它将从本地磁盘上的应用缓存中加载,而不是从服务器上加载,从而大大加快加载速度。明智地使用(不要预取维基百科),您可以使用应用缓存来显著提高性能。需要记住的一件重要事情是,常规的浏览器缓存仍然有效,所以要注意误报,尤其是当您试图调试应用缓存行为时。"*

使用 HTML5 离线 Web 应用构建应用

在这个示例应用中,我们将在跑步时跟踪跑步者的位置(断断续续或没有连接)。例如,Peter 去跑步,他将带着新的支持地理定位的手机和 HTML5 网络浏览器,但在他家周围的树林中并不总是有很好的信号。他想使用这个应用来跟踪和记录他的位置,即使他不能使用互联网。

离线时,地理定位 API 应该可以在有硬件地理定位的设备上继续工作(比如 GPS ),但显然不能在使用 IP 地理定位的设备上工作。IP 地理定位需要网络连接来将客户端的 IP 地址映射到坐标。此外,脱机应用始终可以通过 API(如本地存储或索引数据库)访问本地机器上的持久存储。

该应用的示例文件位于图书页面上的[www.apress.com](http://www.apress.com)和图书网站上的offline代码文件夹中,您可以通过导航到 code/offline 文件夹并发出以下命令来开始演示:

Python –m SimpleHTTPServer 9999.

在启动 web 服务器之前,确保您已经将 Python 配置为提供清单文件(带有*的文件)。appcache 扩展)与前面描述的正确 mime 类型。这是脱机 web 应用失败的最常见原因。如果它不像预期的那样工作,请在 Chrome 开发者工具中检查控制台,查看可能的描述性错误信息。

这将在端口 9999 上启动 Python 的 HTTP 服务器模块(您可以在任何端口上启动它,但是您可能需要管理员权限来绑定到低于 1024 的端口。启动 HTTP 服务器后,您可以导航到[localhost:9999/tracker.html](http://localhost:9999/tracker.html)来查看运行中的应用。

图 12-3 显示了当你第一次访问火狐网站时会发生什么:你被提示选择在你的电脑上存储数据(然而,注意,不是所有的浏览器都会在存储数据前提示你)。

Image

图 12-3。 Firefox 提示为网络应用存储数据

在允许应用存储数据之后,应用缓存进程启动,浏览器开始下载应用缓存清单文件中引用的文件(这发生在页面加载之后,因此对页面的响应性影响最小。图 12-4 显示了 Chrome 开发者工具如何在资源窗格中提供关于localhost原点缓存内容的详细概述。它还在控制台中提供有关在处理页面和清单时触发的应用缓存事件的信息。

Image

***图 12-4。*Chrome 中的离线页面,详细介绍了 Chrome 开发者工具中的应用缓存

要运行这个应用,您需要一个 web 服务器来服务这些静态资源。请记住,清单文件必须提供内容类型text/cache-manifest。如果您的浏览器支持应用缓存,但该文件提供了不正确的内容类型,您将收到缓存错误。一个简单的测试方法是查看 Chrome 开发者工具控制台中触发的事件,如图 12-4 所示;它会告诉您 appcache 文件是否使用了错误的 mime 类型。

要使用完整的功能运行此应用,您需要一个可以接收地理位置数据的服务器。这个例子的服务器端补充可能会存储、分析和提供这些数据。它可能来自静态应用,也可能不来自静态应用。图 12-5 显示了在 Firefox 中以离线模式运行的示例应用。你可以在 Firefox 和 Opera 中使用文件Image离线工作来打开这个模式。其他浏览器没有这个便利功能,但是你可以断网。但是,请注意,断开网络连接不会中断与运行在 localhost 上的 Python 服务器的连接。

Image

***图 12-5。*离线模式下的应用

为应用资源创建清单文件

首先,在文本编辑器中,创建如下的tracker.appcache文件。该清单文件将列出属于该应用的文件:

`CACHE MANIFEST

JavaScript

./offline.js
#./tracker.js
./log.js

stylesheets

./html5.css

images`

为用户界面创建 HTML 结构和 CSS

这是该示例的基本 UI 结构。tracker.htmlhtml5.css都将被缓存,因此应用将由应用缓存提供服务。

`

     HTML5 Offline Application               
      

Offline Example

    


      
        Check for Updates
        

Log


        
        
      
    

`

关于这个应用的离线能力,在这个 HTML 中有一些事情需要注意。第一个是 HTML 元素上的manifest属性。本书中的大多数 HTML 例子都省略了<html>元素,因为它在 HTML5 中是可选的。但是,脱机缓存的能力取决于在那里指定清单文件。

第二个要注意的是按钮。这将使用户能够控制该应用的离线配置。

创建离线 JavaScript

对于这个例子,JavaScript 包含在多个带有<script>标签的.js文件中。这些脚本与 HTML 和 CSS 一起被缓存。

<offline.js> /*  * log each of the events fired by window.applicationCache  */ window.applicationCache.onchecking = function(e) { `log(“Checking for application update”);
}

window.applicationCache.onnoupdate = function(e) {
    log(“No application update found”);
}

window.applicationCache.onupdateready = function(e) {
    log(“Application update ready”);
}

window.applicationCache.onobsolete = function(e) {
    log(“Application obsolete”);
}

window.applicationCache.ondownloading = function(e) {
    log(“Downloading application update”);
}

window.applicationCache.oncached = function(e) {
    log(“Application cached”);
}

window.applicationCache.onerror = function(e) {
    log(“Application cache error”);
}

window.addEventListener(“online”, function(e) {
    log(“Online”);
}, true);

window.addEventListener(“offline”, function(e) {
    log(“Offline”);
}, true);

/*
 * Convert applicationCache status codes into messages
 */
showCacheStatus = function(n) {
    statusMessages = [“Uncached”,“Idle”,“Checking”,“Downloading”,“Update Ready”,“Obsolete”];
    return statusMessages[n];
}

install = function() {
    log(“Checking for updates”);
    try {
        window.applicationCache.update();
    } catch (e) {
        applicationCache.onerror();
    }
}

onload = function(e) {
    // Check for required browser features
    if (!window.applicationCache) {
        log(“HTML5 Offline Applications are not supported in your browser.”);
        return;
    }

if (!navigator.geolocation) {
        log(“HTML5 Geolocation is not supported in your browser.”);
        return;
    }     if (!window.localStorage) {
        log(“HTML5 Local Storage not supported in your browser.”);
        return;
    }

log("Initial cache status: " + showCacheStatus(window.applicationCache.status));
    document.getElementById(“installButton”).onclick = checkFor;
}

<log.js>
log = function() {
    var p = document.createElement(“p”);
    var message = Array.prototype.join.call(arguments, " ");
    p.innerHTML = message;
    document.getElementById(“info”).appendChild§;
}`

检查应用缓存支持

除了离线应用缓存之外,这个示例还使用了地理位置和本地存储。我们确保浏览器在页面加载时支持所有这些功能。

`onload = function(e) {
    // Check for required browser features
    if (!window.applicationCache) {
        log(“HTML5 Offline Applications are not supported in your browser.”);
        return;
    }

if (!navigator.geolocation) {
        log(“HTML5 Geolocation is not supported in your browser.”);
        return;
    }

if (!window.localStorage) {
        log(“HTML5 Local Storage is not supported in your browser.”);
        return;
    }

if (!window.WebSocket) {
        log(“HTML5 WebSocket is not supported in your browser.”);
        return;
    }
    log("Initial cache status: " + showCacheStatus(window.applicationCache.status));
    document.getElementById(“installButton”).onclick = install;
}`

添加更新按钮处理程序

接下来,添加更新应用缓存的更新处理程序,如下所示:

install = function() {     log("Checking for updates");     try {         window.applicationCache.update();     } catch (e) {         applicationCache.onerror();     } }

单击此按钮将明确启动缓存检查,这将导致在必要时下载所有缓存资源。当可用更新完全下载后,会在 UI 中记录一条消息。此时,用户知道应用已经成功安装,可以在脱机模式下运行。

添加地理位置跟踪代码

该代码基于第四章中的地理定位代码。它包含在tracker.js JavaScript 文件中。

`/*
 * Track and report the current location
 */
var handlePositionUpdate = function(e) {
    var latitude = e.coords.latitude;
    var longitude = e.coords.longitude;
    log(“Position update:”, latitude, longitude);
    if(navigator.onLine) {
        uploadLocations(latitude, longitude);
    }
    storeLocation(latitude, longitude);
}

var handlePositionError = function(e) {
    log(“Position error”);
}

var uploadLocations = function(latitude, longitude) {
    var request = new XMLHttpRequest();
    request.open(“POST”, “http://geodata.example.net:8000/geoupload”, true);
    request.send(localStorage.locations);
}

var geolocationConfig = {“maximumAge”:20000};

navigator.geolocation.watchPosition(handlePositionUpdate,
                                    handlePositionError,
                                    geolocationConfig);`

添加存储代码

接下来,添加当应用处于离线模式时向localStorage写入更新的代码。

var storeLocation = function(latitude, longitude) {     // load stored location list     var locations = JSON.parse(localStorage.locations || "[]");     // add location     locations.push({"latitude" : latitude, "longitude" : longitude});     // save new location list     localStorage.locations = JSON.stringify(locations); }

该应用使用 HTML5 本地存储器存储坐标,如第九章所述。本地存储非常适合支持脱机的应用,因为它提供了一种在浏览器中本地保存数据的方法。这些数据将在今后的会议中提供。当网络连接恢复时,应用可以与远程服务器同步。

在这里使用存储还有一个好处,就是允许从失败的上传请求中恢复。如果应用由于任何原因遇到网络错误,或者如果应用被关闭(由于用户操作、浏览器或操作系统崩溃或页面导航),数据将被存储以备将来传输。

添加离线事件处理

每次位置更新处理程序运行时,它都会检查连接状态。如果应用在线,它将存储并上传坐标。如果应用离线,它将只存储坐标。当应用重新联机时,它可以更新 UI 以显示联机状态,并上传联机时存储的任何数据。

`window.addEventListener(“online”, function(e) {
    log(“Online”);
}, true);

window.addEventListener(“offline”, function(e) {
    log(“Offline”);
}, true);`

当应用不在运行时,连接状态可能会改变。例如,用户可能已经关闭了浏览器、刷新或导航到不同的站点。为了处理这些情况,我们的离线应用会在每次页面加载时检查它是否已经恢复在线。如果有,它将尝试与远程服务器同步。

// Synchronize with the server if the browser is now online if(navigator.onLine) {     uploadLocations(); }

总结

在本章中,您已经看到了如何使用 HTML5 离线 Web 应用来创建引人注目的应用,这些应用甚至可以在没有互联网连接的情况下使用。通过在缓存清单文件中指定属于 web 应用的文件,然后从应用的主 HTML 页面引用这些文件,可以确保所有文件都被缓存。然后,通过为联机和脱机状态更改添加事件侦听器,您可以使您的站点根据 Internet 连接是否可用而有不同的行为。

在最后一章,我们将讨论 HTML5 编程的未来。

十三、HTML5 的未来

正如您在本书中已经看到的,HTML5 提供了强大的编程特性。我们还讨论了 HTML5 开发背后的历史和 HTML5 新的无插件范例。在这一章中,我们将看看事情的发展方向。我们将讨论一些还没有完全成熟,但有着巨大潜力的特性。

浏览器对 HTML5 的支持

随着每个新的浏览器更新,HTML5 功能的采用正在加速。在我们写这本书的时候,我们提到的几个特性已经在浏览器中发布了。不可否认,浏览器中的 HTML5 开发正在获得巨大的发展势头。

今天,许多开发人员仍然在努力开发与旧浏览器兼容的一致的 web 应用。Internet Explorer 6 代表了当今互联网上普遍使用的最苛刻的传统浏览器,但即使是 IE6 的寿命也是有限的,因为越来越难获得任何支持它的操作系统。假以时日,将会有接近零的用户使用 IE6 浏览网页。越来越多的 Internet Explorer 用户正在升级到最新版本。总会有一个最老的浏览器与之抗衡,但那就是久而久之;在撰写本文时,Internet Explorer 6 的市场份额不到 10%,并且还在下降。大多数升级的用户会直接选择现代的替代品。随着时间的推移,最小公分母将包括 HTML5 视频、画布、WebSocket 和任何其他您今天可能必须模仿的功能,以达到更广泛的受众。

在本书中,我们介绍了在多种浏览器中基本稳定的特性。对 HTML 和 API 的其他扩展目前正处于开发的早期阶段。在这一章中,我们将看看一些即将推出的功能。一些还处于早期试验阶段,而另一些可能会看到最终的标准化和广泛的可用性,只需对其当前状态进行微小的改变。

HTML 在发展

在这一节中,我们将探索几个可能在不久的将来出现在浏览器中的令人兴奋的特性。你可能也不需要等到 2022 年才能看到这些。可能不会有一个形式化的 HTML6WHATWG 暗示未来的开发将简称为“HTML”开发将是渐进的,特定的特性和它们的规范将单独发展,而不是作为一个整合的努力。随着人们对浏览器的共识越来越多,浏览器将会采用新的特性,而即将到来的新特性甚至可能在 HTML5 定型之前就已经在浏览器中广泛使用了。负责推动 Web 向前发展的社区致力于发展平台,以满足用户和开发人员的需求。

WebGL

WebGL 是一个用于网络 3D 图形的 API。历史上,包括 Mozilla、Opera 和 Google 在内的几个浏览器供应商已经为 JavaScript 开发了独立的实验性 3D APIs。今天,WebGL 正沿着标准化和跨 HTML5 浏览器广泛可用的道路前进。浏览器供应商和 Khronos 集团正在进行标准化过程,Khronos 集团是 OpenGL 的负责机构,OpenGL 是 1992 年创建的跨平台 3D 绘图标准。OpenGL 目前处于规范版本 4.0,作为微软 Direct3D 的对手和竞争者,广泛用于游戏和计算机辅助设计应用。

正如你在第二章中看到的,你通过调用元素上的getContext("2d")从一个canvas元素中得到一个 2D 绘图上下文。不出所料,这为其他类型的绘图环境打开了大门。WebGL 也使用了canvas元素,但是通过 3D 上下文。当前的实现使用实验性的厂商前缀(moz-webglwebkit-3d等)。)作为getContext()调用的参数。例如,在支持 WebGL 的 Firefox 版本中,您可以通过调用canvas元素上的getContext("moz-webgl")来获得 3D 上下文。对getContext()的这种调用所返回的对象的 API 不同于 2D 的 canvas 等价类,因为它提供的是 OpenGL 绑定,而不是绘图操作。WebGL 版本的 canvas 上下文管理纹理和顶点缓冲区,而不是调用画线和填充形状。

三维 HTML

像 HTML5 的其他部分一样,WebGL 将成为 web 平台不可或缺的一部分。因为 WebGL 呈现给一个canvas元素,所以它是文档的一部分。您可以定位和变换 3D canvas元素,就像您可以在页面上放置图像或 2D 画布一样。事实上,你可以用任何其他的canvas元素做任何你能做的事情,包括叠加文本和视频以及表演动画。与纯 3D 显示技术相比,结合其他文档元素和 3D 画布将使平视显示器(hud)以及混合 2D 和 3D 界面的开发更加简单。想象一下,拍摄一个 3D 场景,并使用 HTML 标记在其上覆盖一个简单的 web 用户界面。与许多 OpenGL 应用中的非本地菜单和控件非常不同,WebGL 软件将轻松地结合漂亮的 HTML5 表单元素。

Web 的现有网络架构也将补充 WebGL。WebGL 应用将能够从 URL 获取纹理和模型等资源。多人游戏可以用 WebSocket 通信。例如,图 13-1 显示了一个这样的例子。谷歌最近使用 HTML5 WebSocket、Audio 和 WebGL 将经典的 3D 游戏 Quake II 移植到了网络上,并完成了多人游戏。游戏逻辑和图形用 JavaScript 实现,调用 WebGL 画布进行渲染。使用持久的 WebSocket 连接来实现与服务器的通信,以协调玩家的移动。

Image

***图 13-1。*雷神之锤 II

3D 着色器

WebGL 是 OpenGL ES 2 在 JavaScript 中的绑定,所以它使用了 OpenGL 中标准化的可编程图形管道,包括着色器。着色器允许将高度灵活的渲染效果应用于 3D 场景,从而增加显示的真实感。WebGL 着色器是用 GL 着色语言(GLSL)编写的。这又给 web 堆栈增加了一种单一用途的语言。带有 WebGL 的 HTML5 应用由用于结构的 HTML、用于样式的 CSS、用于逻辑的 JavaScript 和用于着色器的 GLSL 组成。开发人员可以将他们的 OpenGL 着色器知识转移到 web 环境中的类似 API。

WebGL 很可能成为网络上 3D 图形的基础层。正如 JavaScript 库抽象了 DOM 并提供了强大的高级结构一样,在 WebGL 之上也有提供额外功能的库。目前正在为场景图、COLLADA 等 3D 文件格式以及游戏开发的完整引擎开发库。图 13-2 显示了 Shader Toy——一个由 Inigo Quilez 构建的 WebGL 着色器工作台,附带了其他九个 demoscene 艺术家的着色器。这张截图展示了 Rgba 的 Leizex。我们可以预计,在不久的将来,高级渲染库将会大规模涌现,为网络编程新手带来 3D 场景创作能力。

Image

***图 13-2。*着色器玩具是一个 WebGL 着色器工作台

设备

Web 应用需要访问多媒体硬件,如网络摄像头、麦克风或附加的存储设备。为此,已经有一个提议的device元素,让 web 应用能够从连接的硬件访问数据流。当然,这涉及到严重的隐私问题,所以不是每个脚本都能随意使用你的摄像头。当应用请求提升权限时,我们可能会看到一个提示用户权限的 UI 模式,就像在地理位置和存储 API 中看到的那样。网络摄像头的明显应用是视频会议,但计算机视觉在网络应用中还有许多其他惊人的可能性,包括增强现实和头部跟踪。

音频数据 API

可编程音频 API 将为<audio><canvas><img>做的事情。在canvas标签出现之前,网页上的图像对于脚本来说是不透明的。图像创建和操作必须在场外进行,即在服务器上进行。现在,有了基于canvas元素的来创建和操作视觉媒体的工具。类似地,音频数据 API 将支持 HTML5 应用中的音乐创作。这将有助于完善 web 应用可用的内容创建功能,并使我们更接近一个在 Web 上为 Web 创建媒体的自托管工具世界。想象一下,不用离开浏览器就可以在网上编辑音频。

简单的声音回放可以用<audio>元素来完成。然而,任何即时操纵、分析或生成声音的应用都需要一个较低级别的 API。不访问音频数据,文本到语音、语音到语音的翻译、合成器和音乐可视化都是不可能的。

我们可以期待标准音频 API 能够很好地处理来自数据元素的麦克风输入以及音频标签中包含的文件。有了<device>和一个音频数据 API,你也许可以开发一个 HTML5 应用,允许用户在一个页面中记录和编辑声音。音频剪辑将能够存储在本地浏览器存储器中,并与基于canvas的编辑工具结合使用。

目前,Mozilla 在夜间版本中有一个实验性的实现。Mozilla 音频数据 API 可以作为标准跨浏览器音频编程能力的起点。

触摸屏设备事件

随着网络访问越来越多地从台式机和笔记本电脑转移到手机和平板电脑,HTML5 也在继续适应交互处理的变化。当苹果推出 iPhone 时,它也在浏览器中引入了一组特殊事件,可用于处理多点触摸输入和设备旋转。虽然这些事件还没有被标准化,但是它们正在被其他移动设备供应商所采用。今天学习它们将允许你为现在最流行的设备优化你的网络应用。

方向

在移动设备上处理的最简单的事件是方向事件。定向事件可以添加到文档正文中:

<body onorientationchange="rotateDisplay();">

在方向更改的事件处理程序中,您的代码可以引用window.orientation属性。该属性将给出表 13-1 中显示的旋转值之一,该值相对于页面初始加载时设备所处的方向。

Image

一旦知道了方向,您就可以选择相应地调整内容。

手势

移动设备支持的下一种事件是一种高级事件,称为手势。将手势事件视为代表大小或旋转的多点触摸变化。这通常在用户将两个或更多手指同时放在屏幕上并挤压或扭转时执行。扭曲表示旋转,而收缩或缩小分别表示缩小或放大。为了接收手势事件,您的代码需要注册表 13-2 中显示的一个处理程序。

Image

在手势期间,事件处理程序可以自由检查相应事件的旋转和缩放属性,并相应地更新显示。清单 13-1 展示了一个手势处理程序的使用示例。

***清单 13-1。*手势处理器示例

`function gestureChange(event) {
  // Retrieve the amount of change in scale caused by the user gesture
  // Consider a value of 1.0 to represent the original size, while smaller
  //  numbers represent a zoom in and larger numbers represent a zoom
  //  out, based on the ratio of the scale value
var scale = event.scale;

// Retrieve the amount of change in rotation caused by the user gesture
  // The rotation value is in degrees from 0 to 360, where positive values
  //   indicate a rotation clockwise and negative values indicate a counter-
  //   clockwise rotation
var rotation = event.rotation;

// Update the display based on the rotation.
}

// register our gesture change listener on a document node
node.addEventListener(“gesturechange”, gestureChange, false);`

手势事件特别适用于需要操作对象或显示的应用,如图表工具或导航工具。

触动

对于那些需要对设备事件进行低级控制的情况,触摸事件提供了您可能需要的尽可能多的信息。表 13-3 显示了不同的触摸事件。

Image

与其他移动设备事件不同,触摸事件需要表示同时存在多个数据点(许多潜在的手指)。因此,触摸处理的 API 稍微复杂一点,如清单 13-2 所示。

***清单 13-2。*触摸 API

`function touchMove(event) {
// the touches list contains an entry for every finger currently touching the screen
var touches = event.touches;

// the changedTouches list contains only those finger touches modified at this
  // moment in time, either by being added, removed, or repositioned
varchangedTouches = event.changedTouches;

// targetTouches contains only those touches which are placed in the node
  // where this listener is registered
vartargetTouches = event.targetTouches;

// once you have the touches you’d like to track, you can reference
  // most attributes you would normally get from other event objects
varfirstTouch = touches[0];
varfirstTouchX = firstTouch.pageX;
varfirstTouchY = firstTouch.pageY;
}

// register one of the touch listeners for our example
node.addEventListener(“touchmove”, touchMove, false);`

您可能会发现设备的本机事件处理会干扰您对触摸和手势事件的处理。在这些情况下,您应该进行以下呼叫:

event.preventDefault();

这将覆盖默认浏览器界面的行为,并自己处理事件。在移动事件标准化之前,建议您查阅应用所针对的设备的文档。

对等网络

我们也没有看到高级网络在 web 应用中的终结。对于 HTTP 和 WebSocket,都有一个客户端(浏览器或其他用户代理)和一个服务器(URL 的主机)。对等(P2P)网络允许客户端直接通信。这通常比通过服务器发送所有数据更有效。效率,当然,降低托管成本,提高应用性能。P2P 应该有助于更快的多人游戏和协作软件。

P2P 与device元素结合的另一个直接应用是 HTML5 中的高效视频聊天。在点对点视频聊天中,对话双方可以直接互相发送数据,而不需要通过中央服务器。在 HTML5 之外,P2P 视频聊天在 Skype 等应用中非常流行。由于流式视频需要高带宽,如果没有点对点通信,这两种应用都不可能实现。

浏览器供应商已经在试验 P2P 网络,例如 Opera 的 Unite 技术,它直接在浏览器中托管一个简化的 web 服务器。Opera Unite 允许用户创建并向他们的同伴公开服务,用于聊天、文件共享和文档协作。

当然,网络的 P2P 网络需要一个考虑到安全性和网络中介的协议,以及一个供开发者编程的 API。

终极方向

到目前为止,我们一直致力于让开发人员能够构建强大的 HTML5 应用。一个不同的角度是考虑 HTML5 如何增强 web 应用的用户。许多 HTML5 特性允许你删除或减少脚本的复杂性,并执行以前需要插件的专长。例如,HTML5 video 允许您指定控件、自动播放、缓冲行为和占位符图像,而无需任何 JavaScript。使用 CSS3,您可以将动画和效果从脚本移动到样式。这种声明性代码使应用更符合用户风格,并最终将权力还给每天使用您的作品的人。

您已经看到了所有现代浏览器中的开发工具如何公开有关 HTML5 特性的信息,如存储,以及至关重要的 JavaScript 调试、分析和命令行评估。HTML5 开发将趋向于简单性、声明性代码和浏览器或 web 应用本身的轻量级工具。

谷歌对 HTML 的持续发展充满信心,它已经发布了谷歌 Chrome 操作系统,这是一个围绕浏览器和媒体播放器构建的精简操作系统。谷歌的操作系统旨在使用 HTML APIs 包含足够的功能,以提供引人注目的用户体验,其中应用使用标准化的 web 基础设施交付。同样,微软已经宣布 Windows 8 将不支持新 Metro 模式中的任何插件,包括该公司自己的 Silverlight 插件。

总结

在这本书里,你已经学会了如何使用强大的 HTML5 APIs。明智地使用它们!

在这最后一章中,我们已经让你看到了一些即将到来的事情,如 3D 图形,新的设备元素,触摸事件和 P2P 网络。HTML5 的发展没有显示出放缓的迹象,将会非常令人兴奋。

回想一分钟。对于那些已经在网上冲浪,或者甚至已经为之开发了十年或更长时间的人来说,想想 HTML 技术在过去几年里已经走了多远。十年前,“专业 HTML 编程”意味着学习使用 HTML 4 的新功能。当时最前沿的开发人员刚刚发现动态页面更新和,“Ajax”这个术语距离引入还有数年时间,即使 Ajax 描述的技术已经开始获得关注。浏览器中的许多专业编程都是为了处理框架和操作图像地图而编写的。

今天,需要几页脚本的功能只需标记就能完成。现在,对于那些愿意下载众多免费 HTML5 浏览器之一、打开他们最喜欢的文本编辑器并尝试专业 HTML5 编程的人来说,多种新的通信和交互方法都是可用的。

我们希望你喜欢这个 web 开发的探索,并且我们希望它激发了你的创造力。我们期待着十年后写下你使用 HTML5 创造的创新。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/19868.html
标签
评论
发布的文章
大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!