Dive Into HTML5:离线Web程序(续)

事件流

到目前为止,我们已经讨论了有关离线 web 程序、缓存清单和离线程序缓存(“appcache”)的相关问题。简单来说,我们只是把资源下载下来,浏览器去决定如何去做,这样就可以工作了。现在你已经知道这个过程了。这就是我们讨论的关于 web 应用程序开发的流程。事实上,并不是这么简单的。

首先,我们来讨论一下事件流,特别是 DOM 事件。当浏览器访问指定了缓存清单的页面之后,它就会在window.applicationCache对象发出一系列的事件。或许你觉得这听起来很复杂,但是这已经是我能做的最简单的解释了。

  1. 只要浏览器发现在<html>标签上有manifest属性,它就会发出一个checking事件。(这里我们列出的所有事件都是window.applicationCache对象上的。)checking事件一定会发出,不管你之前是否访问过这个页面,也不管这个页面指定的缓存清单文件是不是同一个。
  2. 如果浏览器之前没有见过这个缓存清单……
    • 发出downloading事件,然后开始下载缓存清单中列出的资源;
    • 下载过程中,浏览器会不停发出progress事件,该事件包含已经下载了多少文件,以及还有多少文件正在下载列表中等待下载。
    • 当所有缓存清单中列出的资源都被成功下载后,浏览器将发出最后一个事件:cached。这是离线 web 程序已经全部缓存的信号,也就是说,当这个事件发出之后就可以离线使用了。此时,你的离线工作已经全部完成。
  3. 另一方面,如果你之前访问过这个页面,或者是访问过其它页面,但是这些页面指向同一个缓存清单,此时,浏览器已经知道这个缓存清单文件了。有些清单中的资源可能已经在 appcache 中了,也可能整个应用都已经在 appcache 中。此时,问题就是,自上一次检查之后,缓存清单是不是有修改呢?
    • 如果答案是否,缓存清单没有改变,则浏览器立刻发出noupdate事件。这就是全部工作,完成!
    • 如果答案是是,缓存清单改变了,浏览器则发出downloading事件,然后开始下载缓存清单中每一个资源文件。
    • 下载过程中,浏览器一直发出progress事件,该事件包含已经下载了多少文件,以及还有多少文件正在下载列表中等待下载。
    • 当缓存清单中列出的所有文件已经重新下载完成之后,浏览器发出最后一个事件:updateready。这是你的离线程序已经重新缓存成功的信号,此时离线程序已经可以能够离线使用了。但是注意,此时新版本还没有使用。要让用户不刷新页面就可以使用新版本,我们可以调用window.applicationCache.swapCache()函数。

如果在这个过程中有什么致命错误发生,浏览器将发出error事件,然后停止缓存。下面是一些不可恢复错误列表:

  • 缓存清单返回 HTTP 404 错误(页面没有找到)或者 401(永久移除)。
  • 缓存清单找到,并且没有改变,但是指向这个缓存清单的 HTML 页面没有能够顺利下载。
  • 正在更新时缓存清单被修改了。
  • 缓存清单找到,并且有改变,但是浏览器不能成功下载缓存清单中列出的某一个资源。

调试的艺术

这里我们主要指出两个重点。第一个是你已经读到过的,但是我们没有指出来。这里就再重复一遍:即是只有一个资源无法下载,整个缓存过程都会失败!浏览器会发出 error 事件,但是不会指出究竟是哪里出错了。这使得调试离线 web 程序要比其它调试困难得多。

第二点重要之处在于,有些东西技术上说并不是错误,直到你真正搞懂到底怎么回事之后,它看起来很像一个浏览器 bug。这取决于浏览器是如何检查缓存清单文件是否发生改变。这很讨厌,但是很重要,要引起我们足够的注意。

  1. 通过普通 HTTP 语义,浏览器检查缓存清单是否超期。就像通过 HTTP 访问的其它文件一样,web 服务器将在 HTTP 响应头添加关于文件的元信息。有些 HTTP 头(Expires 和 Cache-Control)可以告诉浏览器如何去缓存文件,而不需要去问服务器是不是文件已经被修改。这种缓存并不是专用于离线 web 应用的。它可以影响到 HTML 页面、CSS、脚本、图片以及其他资源。
  2. 如果缓存清单超期(根据 HTTP 头),浏览器就会要求服务器查看是否有新的版本,如果有,则下载。为了达到这一目的,浏览器会发出一个包含缓存清单文件的最新修改日期的 HTTP 请求,这个时间实际是服务器在浏览器下载的这个清单文件头中包含了的。如果服务器发现清单文件自这个时间以来并没有被修改,则简单地返回一个 304 状态(没有修改)。还是这句话,这些并不是专用于离线 web 程序的,这是每一个 web 资源都可以这样。
  3. 如果服务器觉得清单文件在那个日期之后有修改,则会返回 HTTP 200 状态吗(OK),跟着新文件的内容,同时还有新的 Cache-Control 头和一个新的最近修改日期。这样的话,我们又可以重复前面两步了。(HTTP 很神奇。web 服务器通常会为将来可能发生的事情做准备。如果服务器决定要给你发送文件,那么它会尽可能做到以后不会再无缘无故地又发送一遍。)一旦新的缓存清单文件下载完毕,浏览器要将其内容与上次下载下来的版本进行比对。如果内容相同,则不需要再次下载清单中列出的资源。

上面所说的任何一点都可能在你开发和调试离线 web 程序时构成威胁。例如,你部署了一个版本的缓存清单文件,10分钟之后,突然意识到还得再增加一个资源。没问题!再把它加上,然后重新部署一下就好了。让我们看看究竟会发生什么:刷新页面,浏览器发现了manifest属性,于是派发checking事件,然后……什么都不干了。浏览器认为缓存清单没有修改。这意味着浏览器会停留在上面所说的第一步上,不会向下进行。当然,服务器知道文件被修改了,但是浏览器不会知道。为什么?因为,默认情况下,服务器会告诉浏览器,将静态文件缓存几个小时(通过 HTTP 的 Cache-Control 头)。这就是10分钟后浏览器做的事。

必须说清楚,这不是 bug,而是一个特性。事情就和预先设计的一样。如果服务器不能告诉浏览器缓存文件,web 就会成天到晚阻塞起来。如果不解释清楚,你可能就会花上几小时去找出为什么浏览器没有发现你上传的缓存清单已经修改了。(然后,如果你等的时间足够长,你的程序又神秘地可以运行了!因为 HTTP 缓存超期了。这和原本设计是一样的!杀了我吧!现在就杀了我吧!)

所以,我们建议你应该做一件事:重新配置你的 web 服务器,以便清单文件不受 HTTP 语义缓存影响。如果你使用的是基于 Apache 的服务器,你应该在 .htaccess 文件中添加如下两行:

ExpiresActive On
ExpiresDefault "access"

这将会禁止掉当前目录及其子目录所有文件的缓存。这可能不是在生产环境下所希望的,因此你应该只把这种修改指定 <Files> 指令,以便让它仅影响到你的缓存清单文件的目录;或者是创建一个仅包含 .htaccess 文件和清单文件的目录。通常,这种修改是因服务器而异的,你应该去查阅你所使用的服务器文档,找出如何控制 HTTP 缓存头。

一旦你禁止掉清单文件的 HTTP 缓存,你还是可以改变 appcache 中的一个资源,但使用的是服务器上同一个 URL。此时,上面的第二步又成阻碍了。如果缓存清单文件没有修改,浏览器不会意识到它之前缓存的资源有变化。考虑下面例子:

CACHE MANIFEST
# rev 42
clock.js
clock.css

如果你修改了 clock.css 然后重新部署,你不会看到任何改变,因为缓存清单没有修改。只要你修改离线 web 程序的任何一个资源,你就得修改清单文件。这可以简单地添加一个字符。最简单的方法是增加一行注释,这个注释实际是一个版本号。修改注释里面的版本号,服务器就会返回清单文件已被修改,然后浏览器就会察觉到文件内容的改变,就会重新下载清单文件中列出的所有资源。

CACHE MANIFEST
# rev 43
clock.js
clock.css

构建一个!

让我们再来看看那个跳棋程序。现在,我们把这个游戏变成可离线运行的。

为了实现这一目的,我们需要一个清单文件,列出游戏所有需要的所有资源。主要是一个 HTML 页面,没有图片,因为整个游戏界面都是使用 canvas API 绘制的。所有需要的 CSS 样式都在 HTML 页面的<style>标签中。为了更符合实际,我们将所有脚本放在一个单独的文件中。下面就是我们的清单文件:

CACHE MANIFEST
halma.html
../halma-localstorage.js

再来解释一下路径问题。我们在 examples/ 中创建了 offline/ 子目录,将缓存文件放在这里。由于我们的 HTML 页面需要增加离线运行的代码,所以我们创建了一个独立的页面,同样放在 offline/ 中。而脚本文件则不需要修改,所以放在 examples/ 中就可以了。下面是我们的文件结构:

/html5/localstorage-halma.html
/html5/halma-localstorage.js
/html5/offline/halma.manifest
/html5/offline/halma.html

在缓存文件 /html5/offline/halma.manifest 中,我们引用了两个文件。第一,HTML 文件的离线版本 /html5/offline/halma.html。由于这两个文件是在同一目录下,所以我们不需要在文件名前添加路径前缀。第二,JavaScript 文件在父目录,/html5/halma-localstorage.js,所以我们需要在清单中添加相对路径 ../halma-localstorage.js。这同我们在<img src>中写的相对路径是一致的。当然,我们也可以使用绝对路径(也就是以当前域名的根目录开始的),或者是绝对 URL 地址(可以是指向另外域的资源)。

现在,我们需要在 HTML 文件中增加manifest属性,指向缓存清单文件:

<!DOCTYPE html>
<html lang="en" manifest="halma.manifest">

这样我们就修改完毕了!当我们使用支持离线运行的浏览器第一次加载这个页面时,浏览器就会下载我们指定的那个缓存清单,并且开始下载所有需要的资源,将它们存储在离线应用程序缓存中。然后,当你再次访问页面时,离线应用程序就可以使用了。你可以离线运行游戏,并且我们已经添加了本地状态,因此你可以任意关闭,并且再次打开继续游戏。

Leave a Reply