先决条件
在本文中,您将使用最新 Web 技术开发 Web 应用程序。这里的大多数代码只是 HTML、JavaScript 和 CSS — 任何 Web 开发人员的核心技术。需要的最重要的东西是用于测试代码的浏览器。本文中的大多数代码将运行在最新的桌面浏览器上,例外的情况会指出来。当然,还必须在移动浏览器上进行测试,您肯定希望最新的 iPhone 和 Android SDK 支持这些代码。本文中使用的是 iPhone SDK 3.1.3 和 Android SDK 2.1。
为何要支持您的应用程序离线工作?
由于几个原因,离线 Web 应用程序对于用户和开发人员都有吸引力。许多开发人员希望能够编写一个能够在所有最流行的智能手机上运行的 Web 应用程序,而不是为每个平台编写本机应用程序。这对开发人员很方便,但并不意味这这是用户的愿望。为实现上述目标,移动 Web 应用程序必须能够提供本机移动应用程序能够提供的许多(或绝大部分)相同的特性。离线工作肯定是其中一个特性。有些应用程序非常依赖来自 Internet 的数据和服务 — 不管它们是移动 Web 还是本机应用程序。但是,应用程序不能仅仅因为用户的连接不好而完全失败。但这正是传统 Web 应用程序的症结所在。
离线功能使移动 Web 应用程序类似于本机应用程序。此外,离线功能还有其他好处。Web 浏览器总是缓存静态资源。它们依赖通过您的 Web 服务器发送的 HTTP 响应头部中的元数据来检索渲染页面所需的 HTML、JavaScript、CSS 和图像。如果渲染页面所需的所有资源都已缓存,那么页面就可以非常迅速地加载。但是,如果某个资源没有缓存,那么它将极大地降低页面载入速度。这种情况经常发生,实在是让人无法忍受。也许一个 CSS 文件拥有一个与其他所有文件都不同的 Cache-Control 头部,或者,也许是浏览器因为耗尽了已分配空间而无法缓存。
使用离线应用程序,您可以确保所有资源都会被缓存。浏览器将总是从缓存加载所有资源,尽管您也能够控制哪些资源从缓存加载。一种常见的 Ajax 技巧是将一个额外的时间戳参数添加到 Ajax GET 请求(或者,更糟糕的是在应该使用 GET 时使用 POST)来避免浏览器缓存一个响应。您无需使用这种技巧来支持离线 Web 应用程序。
离线应用程序听起来挺棒,那么创建一个离线应用程序一定很复杂,对吧?实际上,创建方法非常简单,只需完成下面三个步骤:
- 创建一个在线清单文件。
- 告知浏览器这个清单文件。
- 设置服务器上的 MIME 类型。
离线清单
创建过程涉及一个关键文件:您的应用程序的缓存清单。这个文件告知浏览器要缓存(或者,不缓存)的确切内容。这成为您的应用程序的事实来源。清单 1 展示了一个简单缓存清单示例。
清单 1. 简单缓存清单
- CACHE MANIFEST
- # Version 0.1
- offline.html
- /iui/iui.js
- /iui/iui.css
- /iui/loading.gif
- /iui/backButton.png
- /iui/blueButton.png
- /iui/cancel.png
- /iui/grayButton.png
- /iui/listArrow.png
- /iui/listArrowSel.png
- /iui/listGroup.png
- /iui/pinstripes.png
- /iui/redButton.png
- /iui/selection.png
- /iui/thumb.png
- /iui/toggle.png
- /iui/toggleOn.png
- /iui/toolbar.png
- /iui/whiteButton.png
- /images/gymnastics.jpg
- /images/soccer.png
- /images/gym.jpg
- /images/soccer.jpg
这个文件列示了您的应用程序正常工作所需的所有文件,其中包括 HTML 文件、JavaScript、 CSS 和图像。它还可以包括视频、PDFs、XML 文件,等等。注意,本示例中的所有 URLs 都是相对的。任何相对 URLs 必须相对于缓存清单文件。在本例中,缓存清单文件位于您的 Web 应用程序的根目录。比较 清单 2 中的目录结构和 清单 1 中的相对 URLs。
清单 2. Web 应用程序的文本版目录结构
- Name
- V images
- gymnastics.jpg
- soccer.png
- V iui
- backButton.png
- blueButton.png
- cancel.png
- grayButton.png
- iui.css-logo-touch-icon.png
- iui.css
- iui.js
- iuix.css
- iuix.js
- listArrow.png
- listArrowSel.png
- listGroup.png
- loading.gif
- pinstripes.png
- redButton.png
- selection.png
- thumb.png
- toggle.png
- toggleOn.png
- toolbar.png
- toolButton.png
- whiteButton.png
- manifest.mf
- offline.html
- > WEB-INF
您可能已经注意到,这个应用程序正在使用 iUI 框架。这个一个流行的 JavaScript+CSS 工具包,用于向移动 Web 应用程序提供本机 iPhone 应用程序观感。如 清单 1 和 清单 2 所示,这个框架使用几个图像来伴随它的 JavaScript 和 CSS 文件。但是,只要列示在清单中,所有这些文件都将被浏览器缓存并可在离线模式中使用。
清单 1 中另一个需要注意的关键点是版本信息,它并不是规范的一部分。事实上,它只是文件中的一个注释。但是,拥有这样的信息很关键,因为您可以使用该信息来告知浏览器您的应用程序有一个新版本。想想看,假如您更改了一些 HTML 或 JavaScript,或者甚至只是更换了一个图像。如果您不修改清单,则浏览器永远不会费心去加载修改后的资源的新版本。缓存清单没有过期一说,因此所有资源都将保持缓存状态,除非用户清空缓存或清单文件更改。浏览器将检查是否存在新的清单文件。要表明一个新的清单文件,您只需更改现有清单文件的部分或全部内容。返回到您的修改页面上的 HTML 的示例,如果您更改了 HTML 并更改了清单文件中的版本字符串,那么浏览器将知道资源已经被更改并再次下载它们。将版本号放置到注释中是管理这个生命周期的一种简单方法。
告知浏览器关于清单的信息
要启用您的 Web 应用程序的离线缓存,还需要向浏览器提供一些信息。Web 浏览器需要知道您想启用缓存,以及到哪里去找到您的缓存清单文件。清单 3 展示了一种非常简单的方法。
清单 3. 启用了离线功能的 Web 页面
- <!DOCTYPE html>
- <html>
- <html manifest="manifest.mf">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <meta name="viewport" content="width=device-width; initial-scale=1.0;
- maximum-scale=1.0; user-scalable=0;"/>
- <meta name="apple-touch-fullscreen" content="YES" />
- <link rel="apple-touch-icon" href="/iui/iui-logo-touch-icon.png" />
- <style type="text/css" media="screen">@import "/iui/iui.css";</style>
- <script type="application/x-javascript" src="/iui/iui.js"></script>
- <title>Let's do it offline</title>
- </head>
- <body>
- <div class="toolbar">
- <h1 id="pageTitle">Going offline</h1>
- <a id="backButton" class="button" href="#"></a>
- </div>
- <ul id="menu" title="Sports" selected="true">
- <li><a href="#gym"><img height="80" width="80"
- src="/images/gym.jpg" align="middle"/>
- <span style="display:inline-block;
- vertical-align:middle">Gymnastics</span></a></li>
- <li><a href="#soccer"><img src="/images/soccer.jpg"
- align="middle"/>
- <span style="display:inline-block;
- vertical-align:middle">Soccer</span></a></li>
- </ul>
- <div id="gym" title="Gymnastics" class="panel">
- <img src="/images/gymnastics.jpg" alt="Boys Gymnastics"/>
- </div>
- <div id="soccer" title="Soccer" class="panel">
- <img src="/images/soccer.png" alt="Boys Soccer"/>
- </div>
- </body>
- </html>
这个 HTML 最重要的一点是根 html 元素。注意,该元素拥有一个称为 manifest 的属性。正是这个属性告知浏览器这个页面可以离线工作。manifest 参数的值是到该 Web 页面的缓存清单文件的 URL。重申一遍,这个 URL 可以是一个完整 URL,尽管它在这里是一个相对(于指定 Web 页面的)URL。这里需要注意的另一点是该页面的 DOCTYPE。这是用于 HTML 5 Web 页面的规范 doctype。离线 Web 应用程序规范并不强制您使用这个 DOCTYPE;但是,建议您这样做。否则,某些浏览器可能不会将页面识别为 HTML 5 页面,并且可能会忽略缓存清单。这个 HTML 的剩余部分只是使用 iUI 的一个简单示例。图 1 展示了这个页面在 iPhone 模拟器上的外观。
图 1. 运行在 iPhone 模拟器上的离线 Web 应用程序
测试离线应用程序可能有点麻烦。如果可能,最简单的测试方式是将您的应用程序部署到一个 Web 服务器上。然后,您可以访问这个页面一次,关闭您的 Internet 连接,然后尝试再次访问。如果出现任何失败,那么您可能在缓存清单中遗漏了一些文件。在进行上述测试之前,您需要对您的 Web 服务器进行一些关键配置。
Web 服务器配置
清单 3 展示您通过使用您的 Web 页面的根 html 元素上的 manifest 属性来表明您的缓存清单的位置。但是,缓存清单规范规定,浏览器在下载和处理缓存清单时必须执行一个额外的验证步骤,即检查缓存清单的 MIME 类型,该类型必须为 text/cache-manifest。通常,这意味着您需要配置您的 Web 服务器来设置一个静态文件的 MIME 类型,或者,您必须编写一些代码来动态创建该文件并设置 MIME 类型。前者当然是更有效的方法,但是有时您需要使用后一种方法,比如您没有对服务器配置的控制权(比如在一个共享或托管环境中)。如果您对服务器拥有控制权且正在使用一个 Java™ 应用程序服务器,您可以在 Web 应用程序的 web.xml 文件中配置这个参数。清单 4 展示了一个配置示例:
清单 4. 配置 web.xml 来设置 MIME 类型
- <?xml version="1.0" encoding="utf-8"?>
- <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns="http://java.sun.com/xml/ns/javaee"
- xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
- http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
- <!-- Servlets go here -->
- <mime-mapping>
- <extension>mf</extension>
- <mime-type>text/cache-manifest</mime-type>
- </mime-mapping>
- <welcome-file-list>
- <welcome-file>index.html</welcome-file>
- </welcome-file-list>
- </web-app>
显然,这里的关键部分是 mime-mapping 元素。在本例中,您的设置的含义是:对于以 .mf 扩展名结尾的任意文件,将它们的 MIME 类型设置为 text/cache-manifest。当然,一种甚至更有效的方法是从专用于提供静态内容的服务器(比如一个 Apache Web 服务器)提供这样的文件。在一个典型 Apache 安装中,您只需修改 httpd/conf 目录中的 mime.types 文件,如 清单 5 所示。
清单 5. 在 mime.types 中设置 MIME 类型
- # This file controls what Internet media types are sent to the client for
- # given file extension(s). Sending the correct media type to the client
- # is important so they know how to handle the content of the file.
- # Extra types can either be added here or by using an AddType directive
- # in your config files. For more information about Internet media types,
- # please read RFC 2045, 2046, 2047, 2048, and 2077. The Internet media type
- # registry is at <http://www.iana.org/assignments/media-types/>.
- # MIME type Extensions
- text/cache-manifest mf
- # many more mappings...
在这两个示例中,您都使用 mf 作为您的清单文件的扩展名,因此该文件是 manifest.mf。这个扩展名可以任意选择,您可以使用 .manifest 或 .foo,只要这个清单文件的扩展名匹配在您的配置文件中的映射中使用的扩展名。注意,其他应用程序和 Web 服务器可能拥有不同的配置机制。现在您已经看到了使用 HTML 5 创建离线移动 Web 应用程序中的关键因素,下面我们来看一个更复杂的示例,探索离线移动 Web 应用程序的更多功能。
高级示例
在上一个示例中,所有内容都是静态的。能够以离线模式看到所有内容让人感觉不错,但更典型的应用程序需要从它的服务器和 Web 服务读取动态数据。为使您的示例更真实,可以拖入一些来自 Twitter 的数据。如果您阅读了本系列之前的文章,那么您将对此感到熟悉(参见 参考资料)。首先,在 清单 6 中查看这个示例修改后的 HTML。
清单 6. 修改后的 HTML
- <body onload="init()">
- <div class="toolbar">
- <h1 id="pageTitle">Going offline</h1>
- <a id="backButton" class="button" href="#"></a>
- </div>
- <ul id="menu" title="Sports" selected="true">
- <li><a href="#gym">
- <img height="80" width="80" src="/images/gym.jpg" align="middle"/>
- <span style="display:inline-block; vertical-align:middle">Gymnastics</span>
- </a></li>
- <li><a href="#soccer"><img src="/images/soccer.jpg" align="middle"/>
- <span style="display:inline-block; vertical-align:middle">Soccer</span>
- </a></li>
- <li id="online" style="display: none"><img src="/images/online.jpg"/></li>
- </ul>
- <ul id="gym" title="Gymnastics"></ul>
- <ul id="soccer" title="Soccer"></ul>
- </body>
主要区别是现在列示了 gym 和 soccer 元素,且这两个元素为空。您将分别使用来自 Twitter 的关于体操和足球的 tweets 来填充它们。还要注意一个 id 为 online 的列表项元素,该元素显示一个图像,用于向用户表明应用程序是在线还是离线。但是,这个元素默认隐藏,也就是说,默认模式是离线。 这个 body 元素规定:一旦这个 body 加载,就会调用一个 init() 函数。清单 7 展示了这个函数。
清单 7. 页面初始化 JavaScript
- function init(){
- if (navigator.onLine){
- searchTwitter("gymnastics", "showGymTweets");
- searchTwitter("soccer", "showSoccerTweets");
- $("online").style.display = "inline";
- }
- gymTweets = localStorage.getItem("gymnastics");
- if (gymTweets){
- gymTweets = JSON.parse(gymTweets);
- showGymTweets();
- }
- soccerTweets = localStorage.getItem("soccer");
- if (soccerTweets){
- soccerTweets = JSON.parse(soccerTweets);
- showSoccerTweets();
- }
- document.body.addEventListener("online", function() {
- $("online").style.display= "inline";
- applicationCache.update();
- applicationCache.addEventListener("updateready", function() {
- applicationCache.swapCache();
- }, false);
- }, false);
- document.body.addEventListener("offline", function() {
- $("online").style.display = "none";
- }, false);
- }
这个代码所做的第一件事就是检查您是在线还是离线。如果在线,则显示在线图像。更重要的是,如果您在线,那么它将通过调用searchTwitter 函数来从 Twitter 加载数据。同样,这是一种允许您使用 JSONP 直接从浏览器搜索 Twitter 的技术(在本系列前面的文章中解释 — 参见 参考资料)。接下来,试图从 localStorage 加载现有 tweets。如果您熟悉 localStorage,就会知道这是另一个能够在离线模式下很好地工作的 HTML 5 功能。参阅本系列第 2 部分进一步了解它(参见 参考资料)。注意,为进行新搜索(在检测到您处于在线时启动)并加载本地保存的 tweets,showGymTweets 和 showSoccerTweets 函数将被调用。它们是相似的函数,清单 8展示了 showGymTweets。
清单 8. 显示 Gym tweets
- function showGymTweets(response){
- var gymList = $("gym");
- gymList.innerHTML = "";
- if (gymTweets){
- if (response){
- gymTweets = response.results.reverse().concat(gymTweets);
- }
- } else {
- gymTweets = response.results.reverse();
- }
- showTweets(gymTweets, gymList);
- localStorage.setItem("gymnastics", JSON.stringify(gymTweets));
- }
这个函数能够显示本地存储的 tweets,来自 Twitter 的新 tweets,或者二者的结合(如果二者都存在)。最重要的是,它本地存储所有资源,构建您的本地 tweets 数据缓存。这是用于同时管理本地缓存的数据和来自服务器的实时数据的典型代码。它允许应用程序顺畅运行,无论在线还是离线。
返回到 清单 7,需要做的最后一件事是注册事件处理程序。这将告知您浏览器的在线或离线状态何时改变。至少,您可以更改在线图像,并在是否显示图像之间切换。在这个应用程序从离线转为在线的例子中,您访问 applicationCache 对象。这个对象表示按照缓存清单中的声明方式缓存的所有资源。在本例中,您调用它的 update 方法。该方法指示浏览器检查它是否检测到 applicationCache的一个更新。如前所述,浏览器首先检查缓存清单文件的一个更新。您添加另一个事件监听器来检查可用缓存的一个更新。如果存在更新,则调用 applicationCache 上的 swapCache 方法。该方法将重新加载在缓存清单文件中指定的所有文件。
谈到缓存清单文件,您需要对这个高级示例进行最后的完善。缓存清单文件需要按 清单 9 所示修改。
清单 9. 修改后的缓存清单
- CACHE MANIFEST
- # Version 0.2
- CACHE:
- offline.html
- json2.js
- /iui/iui.js
- /iui/iui.css
- /iui/loading.gif
- /iui/backButton.png
- /iui/blueButton.png
- /iui/cancel.png
- /iui/grayButton.png
- /iui/listArrow.png
- /iui/listArrowSel.png
- /iui/listGroup.png
- /iui/pinstripes.png
- /iui/redButton.png
- /iui/selection.png
- /iui/thumb.png
- /iui/toggle.png
- /iui/toggleOn.png
- /iui/toolbar.png
- /iui/whiteButton.png
- /images/gym.jpg
- /images/soccer.jpg
- /images/online.jpg
- NETWORK:
- http://search.twitter.com/
在这个示例中,您将一个显式 CACHE 区域添加到清单。清单可以拥有多个不同的区域,但是,如果它只有一个区域,那么这个区域就假定为 CACHE 且可以省略。这里之所以要使用显式区域,其原因是您还有一个 NETWORK 区域。这个区域向浏览器表明:来自指定域(这里是 search.twitter.com)的任何数据都应该从网络获取且从不缓存。由于您正在本地缓存来自 Twitter 的搜索结果,您肯定不希望浏览器再执行间接的查询缓存。现在,这段代码就位后,应用程序将总是从 Twitter 加载实时 tweets。但是,它将总是缓存那些 tweets 并使它们对用户可用,即使在用户的设备丧失连通性时。
结束语
自从 Mosaic 浏览器风靡以来,Web 应用程序已经走过了漫长的发展历程。移动 Web 应用程序发展得甚至更快。只使用 Wireless Markup Language (WML) 的 WAP 手机的日子走到头了。现在您对您的移动浏览器提出要求甚至比它们的桌面浏览器还要多。离线功能就是这样的要求之一。HTML 5 中的标准经历了很长时间,以简化开发人员离线支持他们的移动 Web 应用程序的工作。在本系列下一篇文章中,您将看到另一个 HTML 5 标准 —— Web Workers —— 如何极大地改善移动 Web 应用程序的性能。