求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
使用HTML 5 创建移动 Web 应用程序
 

作者:Michael Galpin , 发布于2011-07-06 , IBM

 

目录

第 1 部分:联合使用 HTML 5、地理定位 API 和 Web 服务来创建移动混搭程序

第 2 部分:使用 HTML 5 开启移动 Web 应用程序的本地存储

第 3 部分:使用 HTML 5 支持移动 Web 应用程序离线工作

第 4 部分:使用 Web Workers 来加速您的移动 Web 应用程序

第 5 部分: 使用 HTML 5 开发新的可视化 UI 特性

 

第 1 部分: 联合使用 HTML 5、地理定位 API 和 Web 服务来创建移动混搭程序

简介: 在这个由五个部分所组成的系列的第一部分中,您将接触到移动 Web 应用程序中最流行的新技术:地理定位。高端智能手机都内置 GPS,现在您将了解 Web 服务如何使用它。在本文中,您将学到如何使用各种地理定位标准,以及如何使用流行的 Web 服务来创建有趣的移动混搭程序。

HTML 5 是一项让人振奋的技术,这有着充分的理由。这将会是一次技术突破,因为它可以将桌面应用程序功能带入浏览器中。除了传统浏览器外,对于移动浏览器,其潜力甚至更大。不仅如此,最流行的移动浏览器甚至已经采用并实现了 HTML 5 规范中很多重要部分。在这个由五个部分组成的系列里,您将仔细了解 HTML 5 中的一些新技术,它们对移动 Web 应用程序开发具有重大影响。在本系列的每一部分中,您将动手开发一个展示 HTML 5 新特性的移动 Web 应用程序,这些特性可用于最新的移动 Web 浏览器上,如那些基于 iPhone 和 Android 的设备。

先决条件

本文中,您将使用最新的 Web 技术开发 Web 应用程序。这里大多数代码是 HTML、JavaScript 和 CSS — 所有 Web 开发的核心技术。您最需要的东西是用来测试的浏览器。对于本文,强烈建议您使用 Mozilla Firefox 3.5+,因为它是支持地理定位的桌面浏览器。当然,您还需要在移动浏览器上测试,还需要最新的 iPhone 和 Android SDK。在本文中使用的是 iPhone SDK 3.1.3 和 Android SDK 2.1。查看 参考资料 部分中的链接。

基础知识:了解一下

地理定位本身就是一项新技术。它能让您知道用户在哪。但是仅仅知道这点并报告给用户并不是十分有用。难道所有人都关心自己所在的确切经纬度?这时可以结合其他数据和服务,让这个位置信息变得有用,可以产生一些有趣的结果。这些服务都需要用户的经度和纬度作为输入。这就是所需要的,让我们看看是如何获得的。清单 1 显示的是这方面的标准 JavaScript API。

清单 1. 找到用户: getCurrentPosition

navigator.geolocation.getCurrentPosition(successCallback, 
errorCallback, options);

这是地理定位中重要的 API。对于一大类应用程序,只需用到这个 API。地理定位对象是标准导航器对象的一部分,它有几个方法,最常用的是 getCurrentPosition。访问用户位置是耗时的操作(就像在太空中找个卫星一样!),它还要取得用户的同意。因此这是一个异步操作。它的参数是回调函数:一个用于成功,一个用于失败。

成功函数将通过一个单独的 Position 类型的参数传递。这个对象有两个属性:一个时间戳的属性和称为坐标的 Coordinates 类型的属性。一个 Coordinates 对象有几个属性:

  • latitude
  • longitude
  • altitude
  • accuracy
  • altitudeAccuracy
  • heading
  • speed

这些参数不是在所有设备上都可用,除了 latitude、longitude 和 accuracy。如果支持地理定位 API,并且设备可以解析位置,就可以获取 latitude、longitude 和 accuracy。

失败 callback 函数将传递一个 PositionError 类型参数。PositionError 实例有两个参数:code 和 message。message 是设备特定的,可用于调试。code 有以下三个取值:

  • PERMISSION_DENIED (1)
  • POSITION_UNAVAILABLE (2)
  • TIMEOUT (3)

应用程序将根据 code 向用户友好显示失败消息。

请注意,W3C 规范还允许选择第三个参数。这包含花费多长时间取得用户位置的超时时间。尽管如此,像 iPhone 这样的设备目前还不支持,因此不建议使用。既然已经详细看过 API,看看实际如何使用的例子吧。

与 Twitter 集成

现在 hello world 混搭程序可通过某种方式使用 Twitter。在第一个例子中,将使用 Twitter 的搜索 API。它支持根据位置范围搜索微博。清单 2 显示的是本地 Twitter 搜索。

清单 2. 本地 Twitter 搜索

<!DOCTYPE html>
<html>
<head>
<meta name = "viewport" content = "width = device-width"/>
<title>Local Twitter Search</title>
<script type="text/javascript">
    function startSearch(){
        var gps = navigator.geolocation;
        if (gps){
            gps.getCurrentPosition(searchTwitter, 
                   function(error){
                alert("Got an error, code: " + error.code + " message: " 
+ error.message);
             });
        } else {
            searchTwitter();
        }
    }
    function searchTwitter(position){
        var query = "http://search.twitter.com/search.json?callback=showResults&q=";
        query += $("kwBox").value;
        if (position){
            var lat = position.coords.latitude;
            var long = position.coords.longitude;
            query += "&geocode=" + escape(lat + "," + long + ",50mi");
        }
        var script = document.createElement("script");
        script.src = query;
        document.getElementsByTagName("head")[0].appendChild(script);
    }
</script>
</head>
<body>
    <div id="main">
        <label for="kwBox">Search Twitter:</label>
        <input type="text" id="kwBox"/>
        <input type="button" value="Go!" onclick="startSearch()"/>
    </div>
    <div id="results">
    </div>
</body>
</html>

用户可以在文本框输入搜索条目。单击按钮调用 startSearch 函数。此处就使用地理定位 API。首先检查其是否可用。如果可用,就调用 getCurrentPosition API。如回调成功,就使用 searchTwitter 函数。如果 callback 函数失败,就传递一个显示失败信息的终止参数。

如果浏览器成功找到位置,就调用 searchTwitter 函数。这里使用传递给函数的位置来向 Twitter 搜索查询添加 geocode 参数。清单 2 搜索指定位置 50 英里内的帖子。为了调用 Twitter,要使用动态脚本标签,这是一项常称为 JSONP 的技术。Twitter 搜索 API 对此提供支持,它允许直接从浏览器调用 Twitter 搜索,不需要服务器。这由查询中的 callback 参数指出。请注意它设置成 showResults。这是所要调用的函数名称。在 清单 2 中未显示,因为这只是用来创建 UI,但在本文的源代码中已有包含(查看 下载)。 图 1 显示的是 清单 2 中代码的屏幕截图,这是在 iPhone 上运行的。

图 1. 从 iPhone 上搜索 Twitter

本应用程序和其他位置感知应用程序一样,只要一次获得位置。尽管如此,其他应用程序在用户移动时要保持追踪。这些应用程序需要使用其他更高级的地理定位 API。

更高级的内容:追踪

有时候应用程序不只需要知道用户的当前位置,还需要在用户每次改变位置时及时更新。有个用于此目的的 API,是 watchPosition。它与 getCurrentPosition 很相似,接收同样的参数。最大的不同是它返回 ID。这个 ID 可与最后的地理定位 API clearWatch 联合使用。该函数会用到从 watchPosition 获得的 ID。当您调用 watchPosition,浏览器将会一直向您传入的成功回调函数发送更新,直到调用 clearWatch。持续不断获取用户位置信息将会耗尽设备资源,所以请谨慎使用这些 API。现在看看例子。

与 Google 地图集成

本例中,您将使用 Google 地图 API。这些 API 已对使用移动设备进行了优化,尤其是对 iPhone 和 Android 平台。这使得它们对移动 Web 开发人员很有吸引力,尤其是位置感知应用程序。以下的应用程序示例将会在地图上显示用户位置,并且在每次用户改变位置时更新。清单 3 是映射代码。

清单 3. 使用 Geolocation 映射应用程序

<html> 
<head> 
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 
<title>I'm tracking you!</title> 
<script type="text/javascript" src="http://maps.google.com/maps/api/js?
     sensor=true"></script> 
<script type="text/javascript">
    var trackerId = 0;
    var geocoder;
    var theUser = {};
    var map = {};
    function initialize() {
        geocoder = new google.maps.Geocoder();
        if (navigator.geolocation){
            var gps = navigator.geolocation;
            gps.getCurrentPosition(function(pos){
                var latLng = new google.maps.LatLng(pos.coords.
latitude,pos.coords.longitude);
                var opts = {zoom:12, center:latLng, mapTypeId: 
google.maps.MapTypeId.ROADMAP};
                map = new google.maps.Map($("map_canvas"), opts);
                theUser = new google.maps.Marker({
                    position: latLng,
                    map: map,
                    title: "You!"
                });
                showLocation(pos);
            });
            trackerId = gps.watchPosition(function(pos){
                var latLng = new google.maps.LatLng(pos.coords.latitude,pos.
coords.longitude);
                map.setCenter(latLng);
                theUser.setPosition(latLng);
                showLocation(pos);
            });
        }
  }
</script> 
</head> 
<body style="margin:0px; padding:0px;" onload="initialize()"> 
    <div id="superbar">      
        <span class="msg">Current location: 
              <span id="location"></span>
          </span>
          <input type="button" value="Stop tracking me!" 
onclick="stopTracking()"/>
      </div>
  <div id="map_canvas" style="width:100%; height:90%; float:left; 
border: 1px solid black;">
  </div> 
</body> 
</html> 

一旦文档主体加载,就调用 initialize 函数。该函数检查浏览器是否支持地理定位。如果支持,就调用 getCurrentPosition,与 清单 2 中的前个例子一样。当它获取位置,就使用 Google 地图 API 创建地图。请注意如何使用纬度和经度来创建 google.maps.LatLng 实例。该对象用于使地图居中。下一步,创建地图上的标记来表示用户当前位置。该标记再次用到从地理定位 API 接收到的纬度和经度。
创建地图并放置标记后,开始追踪用户。捕获从 watchPosition 中返回的 ID。无论何时接收到新的位置,都将地图在新位置重新居中,并将标记移到新位置。清单 4 显示了需要了解的另两个函数。

清单 4. 地理编码和取消追踪函数

function showLocation(pos){
    var latLng = new google.maps.LatLng(pos.coords.latitude,pos.coords.longitude);
    if (geocoder) {
        geocoder.geocode({'latLng': latLng}, function(results, status) {
          if (status == google.maps.GeocoderStatus.OK) {
            if (results[1]) {
                $("location").innerHTML = results[1].formatted_address;
            } 
          } 
        });
      }        
}
function stopTracking(){
    if (trackerId){
        navigator.geolocation.clearWatch(trackerId);
    }
}

在 清单 3 中,当地图初始绘制及接收到用户位置更新时,调用 showLocation 函数。该函数如 清单 4 所示。它使用的是 google.maps.Geocoder 实例(在 清单 3 的 initialize 函数开始处创建)。这个 API 可执行地理编码或者接收地址将其转换为映射坐标(纬度和经度)。它还执行逆向地理编码 — 将映射坐标转换成实际位置。本例中,使用了地理定位 API 生成的坐标,并使用 Google 地图 API 对其逆向编码。结果显示在屏幕上。

清单 4 中最后一个函数是 stopTracking 函数,当用户单击 清单 3 的 HTML 生成的按钮时调用。此处当首次调用 watchPosition 函数时得到 trackerId。只要简单地将其传递给 clearWatch 函数,浏览器/设备就会停止获取用户位置并停止调用 JavaScript。图 2 显示的是正在使用的追踪应用程序的屏幕截图。

图 2. 追踪应用程序

当然,要实际测试追踪,需要改变位置。可以使用 Google App Engine,因为它能将您的 Web 应用程序上传到公共可达位置。然后可以直接在连接良好的移动设备中测试。一旦完成,您可以乘坐公共交通工具,或者是让别人开车带您四处走走,看看您的 Web 应用程序对不断改变的位置做出的响应。

结束语

本文演示了如何在移动 Web 应用程序中使用地理定位 API。GPS 听上去很吸引人,但相当复杂。尽管如此,如您所见,地理定位的 W3C 标准提供了很简单的 API。它直接获取用户位置并随时间变化追踪位置。并由此将坐标传递给很多支持位置的 Web 服务,或者是您自己正在开发的位置感知服务。在本系列关于 HTML 5 和移动 Web 应用程序的第 2 部分中,您将看到如何利用本地存储提升移动 Web 应用程序性能。

 

第 2 部分: 使用 HTML 5 开启移动 Web 应用程序的本地存储

简介: HTML 5 中一个最有用的新特性是本地存储的标准化。最终,Web 开发人员可以不再试图将所有客户端数据都填塞到 4 KB 的 Cookies 中。现在您可以利用一个简单的 API 将大量数据存储在客户机上。这是一个完美的缓存机制,可以大大提高应用程序的速度 —— 速度对于移动 Web 应用程序是一个至关重要的因素,因为它们相对于桌面应用程序来说,依赖的是慢得多的连接。在这个关于 HTML 5 的系列的第二篇文章中,将了解如何使用本地存储,如何调试它,以及使用它来改善 Web 应用程序的各种方式。

HTML 5 是一项被大肆宣扬的技术,但是它实至名归。它有望成为一个技术引爆点,将桌面应用程序功能引向浏览器。它不仅适用于传统浏览器,甚至也针对移动浏览器。更好的是,最流行的移动浏览器已经采纳和实现 HTML 5 规范的很多重要部分。
在这个五部分的系列中,我们将详细了解几个新技术,它们都是 HTML 5 的一部分,可以大大影响移动 Web 应用程序开发。在每一部分中,都将开发一个可以工作的移动 Web 应用程序,展示一个可以用于现代移动 Web 浏览器(比如 iPhone 和基于 Android 的设备上的浏览器)的 HTML 5 特性。

先决条件

在本文中,您将使用最新 Web 技术开发 Web 应用程序。这里的大多数代码只是 HTML、JavaScript 和 CSS — 任何 Web 开发人员的核心技术。需要的最重要的东西是用于测试代码的浏览器。本文中的大多数代码将运行在最新的桌面浏览器上,例外的情况会指出来。当然,还必须在移动浏览器上进行测试,您肯定希望最新的 iPhone 和 Android SDK 支持这些代码。本文中使用的是 iPhone SDK 3.1.3 和 Android SDK 2.1。参见 参考资料 中的链接。

本地存储基础

Web 开发人员多年来一直在尝试将数据存储在客户机上。HTTP Cookies 被滥用于此目的。开发人员将大量数据挤放在 HTTP 规范分配的 4KB 上。原因很简单。出于各种原因,交互式 Web 应用程序需要存储数据,并且将这些数据存储在服务器上通常效率低下、不安全或者不适当。多年来,这个问题有了好几种备选方法。各种各样的浏览器已经引入了专有存储 API。开发人员也利用了 Flash Player 中的扩展存储功能(通过 JavaScript 实现)。类似地,Google 为各种浏览器创建了 Gears 插件,并且它包含了存储 API。毫不奇怪的是,一些 JavaScript 库试图抹平这些差异。换句话说,这些库提供一个简单的 API,然后检查有哪些存储功能(可能是一个专有浏览器 API 或者是一个诸如 Flash 的插件)。

对 Web 开发人员来说幸运的是,HTML 5 规范最终包含了一个针对本地存储的标准,被广泛的浏览器所实现。事实上,该标准是最快被采纳的标准,在所有主要浏览器的最新版本中都受到支持:Microsoft?、Internet Explorer?、Mozilla Firefox、Opera、Apple Safari 和 Google Chrome。对于移动开发人员更为重要的是,它在基于 WebKit 的浏览器(诸如 iPhone 和使用 Android(版本 2.0 或更高版本)的手机中的浏览器)以及其他移动浏览器(比如 Mozilla 的 Fennec)中受到支持。记住这一点,我们来看一下这个 API。

Storage API

localStorage API 十分简单。实际上,根据 HTML 5 规范,它实现了 DOM Storage 接口。差别的原因是,HTML 5 指定两个不同的对象实现该接口:localStorage 和 sessionStorage。sessionStorage 对象是一个只在会话期间存储数据的 Storage 实现。更确切地说,只要没有可以访问 sessionStorage 的脚本正在运行,浏览器就可以删除 sessionStorage 数据。这是与 localStorage 相对的,后者跨多个用户会话。两个对象共享相同的 API,所以我将只着重介绍 localStorage。

Storage API 是一种经典的名/值对数据结构。您将使用的最常见的方法是 getItem(name) 和 setItem(name, value)。这些方法完全跟您预期的一样:getItem 返回与名称相关联的值,如果什么都不存在,则返回 null,而 setItem 要么是将名/值对添加到 localStorage,要么是取代现有值。还有一个 removeItem(name),顾名思意,它从 localStorage 删除一个名/值对(如果存在的话,否则什么都不做)。最后,对于在所有名/值对上迭代,存在两个 API。一个是长度属性,给出正在存储的名/值对的总数。对应地,一个 key(index) 方法从存储中使用的所有名称中返回一个名称。

利用这些简单的 API,可以完成大量任务,比如说个性化或跟踪用户行为。这些可以说对移动 Web 开发人员是重要的用例,但是还有一个更为重要的用例:高速缓存。利用 localStorage,可以在客户机的本地机器上容易地从服务器高速缓存数据。这让您无需等待可能缓慢的服务器回调,并且最小化了对服务器上数据的需求量。现在来看一个例子,演示了如何使用 localStorage 来获得这种高速缓存。

例子:利用本地存储实现高速缓存

本例建立在本系列第 1 部分中的例子之上,那时您最先开始了 t0 开发。那个例子展示了如何通过利用地理定位 API 取得用户的位置而执行 Twitter 的本地搜索。从那个例子开始,对它进行简化,并大大提高它的性能。首先,将那个例子简化成不带地理位置的 Twitter 搜索。清单 1 展示了简化的 Twitter 搜索应用程序。

清单 1. 最基本的 Twitter 搜索

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name = "viewport" content = "width = device-width"/>
<title>Basic Twitter Search</title>
<script type="text/javascript">
    function searchTwitter(){
        var query = "http://search.twitter.com/search.json?callback
=showResults&q=";
        query += $("kwBox").value;
        var script = document.createElement("script");
        script.src = query;
        document.getElementsByTagName("head")[0].appendChild(script);
    }
    // ui code deleted for brevity
    function showResults(response){
        var tweets = response.results;
        tweets.forEach(function(tweet){
            tweet.linkUrl = "http://twitter.com/" + tweet.from_user 
+ "/status/" + tweet.id;
        });
        makeResultsTable(tweets);
    }
</script>
<!--  CSS deleted for brevity -->
</head>
<body>
    <div id="main">
        <label for="kwBox">Search Twitter:</label>
        <input type="text" id="kwBox"/>
        <input type="button" value="Go!" onclick="searchTwitter()"/>
    </div>
    <div id="results">
    </div>
</body>
</html>

在这个应用程序中,使用了 Twitter 搜索 API 对 JSONP 的支持。用户提交搜索时,会动态添加一个脚本标记到页面并指定回调函数的名称,从而进行一次 API 调用。这允许您从 Web 页面进行一次跨域调用。一旦调用返回,回调函数(showResults)就会被调用。您添加一个链接 URL 到 Twitter 返回的每个 tweet,然后创建一个简单的表格用于显示这些 tweet。为了提速,您可以高速缓存从搜索查询得到的结果,然后在用户每次提交查询时使用这些缓存的结果。首先来看如何使用 localStorage 来本地存储 tweet。

基本的 Twitter 搜索将从 Twitter 搜索 API 提供一组 tweet。如果您可以本地保存这些 tweet,并将它们与生成它们的关键词搜索相关联,那么您就具有了一个有用的高速缓存。要保存 tweet,您只需要修改当对 Twitter 搜索 API 的调用返回时将被调用的 callback 函数。清单 2 展示了修改后的函数。

清单 2. 搜索和保存

function searchTwitter(){
    var keyword = $("kwBox").value;
    var query = "http://search.twitter.com/search.json?callback
=processResults&q=";
    query += keyword;
    var script = document.createElement("script");
    script.src = query;
    document.getElementsByTagName("head")[0].appendChild(script);
}
function processResults(response){
    var keyword = $("kwBox").value;
    var tweets = response.results;
    tweets.forEach(function(tweet){
        saveTweet(keyword, tweet);
        tweet.linkUrl = "http://twitter.com/" + tweet.from_user + "/status/" + tweet.id;
    });
    makeResultsTable();
    addTweetsToResultsTable(tweets);
}
function saveTweet(keyword, tweet){
    // check if the browser supports localStorage
    if (!window.localStorage){
        return;
    }
    if (!localStorage.getItem("tweet" + tweet.id)){
        localStorage.setItem("tweet" + tweet.id, JSON.stringify(tweet));
    }
    var index = localStorage.getItem("index::" + keyword);
    if (index){
        index = JSON.parse(index);
    } else {
        index = [];
    }
    if (!index.contains(tweet.id)){
        index.push(tweet.id);
        localStorage.setItem("index::"+keyword, JSON.stringify(index));
    } 
}

从第一个函数 searchTwitter 开始。这在用户提交搜索时被调用。相对于 清单 1 做了改动的惟一的地方是 callback 函数。不只是在 tweet 返回时显示它们,您还需要处理它们(除了显示,还要保存它们)。因此,您指定一个新的 callback 函数 processResults。您针对每个 tweet 调用 saveTweet。您还传递被用于生成搜索结果的关键词。这是因为您想要将这些 tweet 与该关键词相关联。

在 saveTweet 函数中,首先进行检查,确保 localStorage 真正受到浏览器的支持。正如前面所提到的,localStorage 在桌面和移动浏览器中都受到广泛支持,但是在使用这样的新特性时进行检查总是一个好主意。如果它不受支持,那么您简单地从函数返回。显然不会保存任何东西,但是也不会报错 — 应用程序在这种情况下只是不会具有高速缓存。如果 localStorage 受到支持,那么首先进行检查,看这个 tweet 是否已经存储。如果没有存储,那么使用 setItem 本地存储它。接下来,检索一个对应于关键词的索引对象。这只是一组与关键词相关联的 tweet 的 ID。如果 tweet ID 还不是索引的一部分,那么添加它并更新索引。

注意,在 清单 3 中保存和加载 JSON 时,您使用了 JSON.stringify 和 JSON.parse。JSON 对象(或者更确切地说,是 window.JSON)是 HTML 5 规范的一部分,作为一个总是存在的 原生 对象。stringify 方法将把任何 JavaScript 对象转换成一个序列化的字符串,而 parse 方法则进行相反的操作,它从序列化的字符串表示还原 JavaScript 对象。这是很必要的,因为 localStorage 只存储字符串。但是,原生 JSON 对象并不被广泛实现为 localStorage。例如,它不出现在 iPhone(在撰写本文时是版本 3.1.3)的最新 Mobile Safari 浏览器上。它在最新 Android 浏览器上受支持。您可以容易地检查它是否在那里,如果不在,就加载一个额外的 JavaScript 文件。您可以通过访问 json.org Web 站点(参见 参考资料),获得原生使用的相同 JSON 对象。要本地查看这些序列化的字符串是什么样的,可以使用各种浏览器工具检查 localStorage 中为给定站点存储的内容。图 1 展示了一些高速缓存的 tweet,它们存储在本地,使用 Chrome 的 Developer Tools 进行查看。

图 1. 本地高速缓存的 tweet

Chrome 和 Safari 都内置了开发人员工具,可以用于查看任何保存在 localStorage 中的数据。这对于调试使用 localStorage 的应用程序非常有用。它以纯文本形式展示本地存储的键/值对。既然您已经开始保存来自 Twitter 的搜索 API 的 tweet,以便它们可以被用作高速缓存,所以您只需开始从 localStorage 读取它们即可。下面来看这是如何做到的。

快速本地数据加载

在 清单 2 中,您看到了一些例子使用 getItem 方法从 localStorage 读取数据。现在当一个用户提交搜索时,您可以检查高速缓存命中情况,并立即加载缓存的结果。当然,您仍将针对 Twitter 搜索 API 进行查询,因为人们一直在产生 tweet 并添加到搜索结果。但是,通过只寻找还没在高速缓存中的结果,现在您也有了让查询更为高效的方式。清单 3 展示了更新后的搜索代码。

清单 3. 首先进行本地搜索

function searchTwitter(){
    if ($("resultsTable")){
        $("resultsTable").innerHTML = ""; // clear results
    }
    makeResultsTable();
    var keyword = $("kwBox").value;
    var maxId = loadLocal(keyword);
    var query = "http://search.twitter.com/search.json?callback=processResults&q=";
    query += keyword;
    if (maxId){
        query += "&since_id=" + maxId;
    }
    var script = document.createElement("script");
    script.src = query;
    document.getElementsByTagName("head")[0].appendChild(script);
}
function loadLocal(keyword){
    if (!window.localStorage){
        return;
    }
    var index = localStorage.getItem("index::" + keyword);
    var tweets = [];
    var i = 0;
    var tweet = {};
    if (index){
        index = JSON.parse(index);
        for (i=0;i<index.length;i++){
            tweet = localStorage.getItem("tweet"+index[i]);
            if (tweet){
                tweet = JSON.parse(tweet);
                tweets.push(tweet);
            }
        }
    }
    if (tweets.length < 1){
        return 0;
    }
    tweets.sort(function(a,b){
        return a.id > b.id;
    });
    addTweetsToResultsTable(tweets);
    return tweets[0].id;
}

您将注意到的第一件事情是,当一个搜索被提交时,您首先调用新的 loadLocal 函数。该函数返回一个整数,即高速缓存中找到的最新 tweet 的 ID。loadLocal 函数接受一个 keyword 作为参数,该关键词也被用于在 localStorage 高速缓存中寻找相关 tweet。如果具有一个 maxId,那么使用它来修改对 Twitter 的查询,添加 since_id 参数。您在告诉 Twitter API 只返回比该参数中给定的 ID 新的 tweet。潜在地,这可以减少从 Twitter 返回的结果数量。您任何时候都可以为移动 Web 应用程序优化服务器调用,因为它可以真正改善慢速移动网络上的用户体验。现在更仔细地来看一下 loadLocal。

在 loadLocal 函数中,您利用了存储在前面 清单 2 中的数据结构。通过使用 getItem,您首先加载与关键词相关联的索引。如果没找到任何索引,那么就没有缓存的 tweet,所以就没有展示的东西,并且没有可对查询进行的优化(您返回一个 0 值以指示这一点)。如果找到一个索引,那么您从它得到 ID 列表。这些 tweet 中的每一个都被本地高速缓存,所以您只需再次使用 getItem 方法,从高速缓存加载每一个 tweet。加载的 tweet 然后被排序。使用 addTweetsToResultsTable 函数来显示 tweet,然后返回最新 tweet 的 ID。在本例中,得到新 tweet 的 代码直接调用更新 UI 的函数。您可能会对此感到惊讶,因为它在存储和检索 tweet 的代码与显示它们的代码之间创建了耦合,全都通过 processResults 函数。使用存储事件会提供一种备选的、更少耦合的方法。

存储事件

现在扩展示例应用程序,展示最可能具有缓存结果的前 10 个搜索条目。这可能代表用户最常提交的搜索。清单 4 展示了一个用于计算并显示前 10 个搜索条目的函数。

清单 4. 计算前 10 个搜索条目

function displayStats(){
    if (!window.localStorage){ return; }
    var i = 0;
    var key = "";
    var index = [];
    var cachedSearches = [];
    for (i=0;i<localStorage.length;i++){
        key = localStorage.key(i);
        if (key.indexOf("index::") == 0){
            index = JSON.parse(localStorage.getItem(key));
            cachedSearches.push ({keyword: key.slice(7), numResults: index.length});
        }
    }
    cachedSearches.sort(function(a,b){
        if (a.numResults == b.numResults){
            if (a.keyword.toLowerCase() < b.keyword.toLowerCase()){
                return -1;
            } else if (a.keyword.toLowerCase() > b.keyword.toLowerCase()){
                return 1;
            }
            return 0;
        }
        return b.numResults - a.numResults;
    }).slice(0,10).forEach(function(search){
        var li = document.createElement("li");
        var txt = document.createTextNode(search.keyword + " : " + search.numResults);
        li.appendChild(txt);
        $("stats").appendChild(li);
    });
}

该函数充分展示了 localStorage API。您首先得到存储在 localStorage 中的条目的总数,然后再迭代这些条目。如果条目是索引,那么您就解析该对象并创建一个表示您要处理的数据的对象:与索引相关联的关键词和索引中 tweet 的数量。该数据存储在一个叫做 cachedSearches 的数组中。接下来,排序 cachedSearches,将具有最多结果的搜索排在第一位,如果两个搜索具有相同数量的缓存结果,就再使用一个不区分大小写的字母排序。然后对于前 10 个搜索,为每个搜索创建 HTML,并将它们附加到一个排好序的列表。让我们在页面初次加载时调用该函数,如 清单 5 所示。

清单 5. 初始化页面

window.onload = function() {
    displayStats();
    document.body.setAttribute("onstorage", "handleOnStorage();");
}

第一行在页面加载时调用 清单 4 中的函数。第二次加载是变得更有趣的地方。您在这里为 onstorage 事件设置一个事件处理程序。每当 localStorage.setItem 函数执行完成,该事件就会激活。这将允许您重新计算前 10 个搜索。清单 6 展示了该事件处理程序。

清单 6. Storage 事件处理程序

function handleOnStorage() {
    if (window.event && window.event.key.indexOf("index::") == 0){
        $("stats").innerHTML = "";
        displayStats();
    }
}

onstorage 事件将与窗口相关联。它具有几个有用的属性:key、oldValue 和 newValue。除了这些自解释的属性之外,它还有一个 url(更改值的页面的 URL)和 source(包含更改值的脚本的窗口)。如果用户具有多个到应用程序的窗口或选项卡或者甚至是 iFrames,那么这最后两个属性就更有用,但是没有哪一个在移动应用程序中特别常见。回到 清单 6,您真正需要的惟一的属性是 key 属性。您使用该属性来看它是不是一个已修改的索引。如果是的,那么您重新设置前 10 名列表,并通过再次调用 displayStats 函数而重新绘制它。该技术的优点是,其他函数都不需要了解前 10 名列表,因为它是自包含的。

前面 我提到过,DOM Storage(它包含 localStorage 和 sessionStorage)总体来说是一个被广泛采纳的 HTML 5 特性。但是,存储事件对于这一点来说是一个例外 — 至少在桌面浏览器上如此。在撰写本文时,仅有的支持存储事件的桌面浏览器是 Safari 4+ 和 Internet Explorer 8+。在 Firefox、Chrome 和 Opera 中不受支持。但是在移动领域,情况稍有好转。iPhone 和 Android 浏览器的最新版都完全支持存储事件,并且这里给出的代码都能在这些浏览器中完美地运行。

结束语

作为一名开发人员,突然在客户机上拥有巨额的存储空间,您会觉得自己获得了很大的解放。对于长期的 Web 开发人员来说,为做到他们多年来一直想做、却苦于找不到好的方式来做的事情带来了转机。对于移动开发人员来说,则更为振奋人心,因为它真正开启了数据的本地高速缓存。除了大大改善应用程序的性能之外,本地高速缓存对于推进移动 Web 应用程序的另一个新的令人振奋的功能 —— 离线 —— 是很关键的。这将是本系列下一篇文章的主题。

 

第 3 部分: 使用 HTML 5 支持移动 Web 应用程序离线工作

简介: 移动应用程序的部分魅力是无论您走到哪里,都可以带着您的应用程序及其数据。但移动应用程序面临的一个现实是,有时候,移动设备没有到 Internet 的工作连接。这似乎是移动 Web 应用程序的一个不可逾越的障碍。然而,Web 应用程序已经发展为能够离线工作。在本文中,您将了解如何使您的移动 Web 应用程序离线工作,并检测您的应用程序何时从离线转为在线或从在线转为离线。

HTML 5 是一项被大肆宣扬的技术,但是它实至名归。它有望成为一个技术引爆点,将桌面应用程序功能引向浏览器。它不仅适用于传统浏览器,甚至也针对移动浏览器。更好的是,最流行的移动浏览器已经采纳和实现了 HTML 5 规范的很多重要部分。在这个五部分的系列中,我们将详细了解几个新技术,它们都是 HTML 5 的一部分,可以大大影响移动 Web 应用程序开发。在每一部分中,都将开发一个可以工作的移动 Web 应用程序,展示一个可以用于现代移动 Web 浏览器(比如 iPhone 和基于 Android 的设备上的浏览器)的 HTML 5 特性。

先决条件

在本文中,您将使用最新 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. 简单缓存清单

vCACHE 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 应用程序的性能。

 

第 4 部分: 使用 Web Workers 来加速您的移动 Web 应用程序

简介: 一直以来,Web 应用程序被局限在一个单线程世界中。这的确限制了开发人员在他们的代码中的作为,因为任何太复杂的东西都存在冻结应用程序 UI 的风险。通过将多线程引入 Web 应用程序,Web Workers 扭转了这一不利局面。这对于大部分应用程序逻辑都位于客户端的移动 Web 应用程序来说尤其有用。在本文中,您将了解如何使用 Web Workers 并发现哪些任务最适合它们。您还将看到如何使用其他 HTML 5 技术才能提高使用那些技术的效率。

开始

在本文中,您将使用最新的 Web 技术开发 Web 应用程序。这里的大部分代码只是 HTML、JavaScript 和 CSS — 所有 Web 开发人员的核心技术。所需的最重要的工具是用于进行测试的浏览器。本文大部分代码将在最新桌面浏览器上运行,但也有一些例外,我们将在文章中进行说明。当然,您也必须在移动浏览器上测试,为此,您需要最新的 iPhone 和 Android SDKs。本文将使用 iPhone SDK 3.1.3 和 Android SDK 2.1。本文的样例还将使用一个代理服务器来从浏览器访问远程服务。这个代理服务器是一个简单的 Java? servlet,但也可以使用以 PHP、Ruby 以及其他语言编写的代理轻松替换。参见 参考资料 获取链接。

移动设备上的多线程 JavaScript

对于大多数开发人员来说,多线程或并发编程并不新鲜。但是,JavaScript 并不是一种支持并发编程的语言。JavaScript 的创建者认为,对于 JavaScript 这样旨在 Web 页面上执行简单任务的语言来说,并发编程容易出现问题,而且没有必要。然而,由于 Web 页面已经发展成为 Web 应用程序,使用 JavaScript 完成的任务的复杂程度已经大大增加,向 JavaScript 提出了与其他语言同等的要求。与此同时,使用其他支持并发编程的语言工作的开发人员经常面临伴随线程和 mutexes 这样的并发原语而来的超高复杂性的困扰。实际上,最近像 Scala、Clojure 和 F# 这样的几种新语言已经发展,它们都有可能简化并发性。

Web Worker 规范不只是向 JavaScript 和 Web 浏览器添加并发性,而且是以一种智慧的方式添加,这种方式将增加开发人员的能力,但不会向他们提供一种会导致问题的工具。 例如,多年来,桌面应用程序开发人员一直在使用多线程来支持他们的应用程序访问多个 I/O 资源,以避免在等待这些资源时冻结 UI。然而,当这些多线程更改共享的资源(包括 UI)时,这样的应用程序通常会出现问题,因为这种行为可能会导致应用程序冻结或崩溃。有了 Web Workers,这种情况就不会发生。衍生线程不能访问主 UI 线程访问的资源。事实上,衍生线程中的代码甚至不能与主 UI 线程执行的代码位于同一个文件中。

您甚至必须提供相应的外部文件作为构造函数的一部分,如 清单 1 所示。

这个进程使用三个资源:

在主线程上执行的 Web 页面 JavaScript(我称其为页面脚本)。 Worker 对象,这是用于执行 Web Worker 函数的 JavaScript 对象。 将在新衍生的线程上执行的脚本。我称其为 Worker 脚本。 让我们首先看看 清单 1 中的页面脚本。

清单 1.在页面脚本中使用一个 Web Worker

var worker = new Worker("worker.js");
worker.onmessage = function(message){
    // do stuff
};
worker.postMessage(someDataToDoStuffWith);

在 清单 1 中,您可以看到使用 Web Workers 的三个基本步骤。首先,您创建一个 Worker 对象并向它传递将在新线程中执行的脚本的 URL。Worker 将执行的所有代码都必须包含在一个 Worker 脚本中,该脚本的 URL 将被传递到这个 Worker 的构造函数中。这个 Worker 脚本的 URL 受到浏览器的同源策略的限制 — 它必须来自加载这个页面的同一个域,该页面已加载正在创建这个 Web Worker 的页面脚本。

下一步是使用 onmessage 函数指定一个回调处理器函数。这个回调函数将在该 Worker 脚本执行后调用。message 是从该 Worker 脚本返回的数据,您可以随意处理该消息。回调函数在主线程上执行,因此它能访问 DOM。Worker 脚本在一个不同的线程内运行且不能访问 DOM,因此,您需要将来自这个 Worker 脚本的数据返回主线程,在那里,您可以安全地修改 DOM 来更新您的应用程序的 UI。这是 Web Workers 的无共享设计的关键特性。

清单 1 中的最后一行展示如何通过调用 Worker 的 postMessage 函数来启动它。这里,您传递一条消息(重申一下,它只是数据)给 Worker。当然,postMessage 是一个异步函数;您调用它,它就立即返回。

现在,检查这个 Worker 脚本。清单 2 中的代码是来自 清单 1 的 worker.js 文件的内容。

清单 2. 一个 Worker 脚本

importScripts("utils.js");
var workerState = {};
onmessage = function(message){
     workerState = message.data;
      // do stuff with the message
    postMessage({responseXml: this.responseText});
}

可以看到,这个 Worker 脚本拥有自己的 onmessage 函数。该函数在您从主线程调用 postMessage 时调用。从页面脚本传来的数据被传递到 message 对象中的 postMessage 函数。您通过检索 message 对象的 data 属性来访问该数据。当您处理完 Worker 脚本中的数据时,调用 postMessage 函数将数据返回主线程。主线程也可以通过访问它接收到的消息的 data 属性来访问该数据。

至此,您已经见识了 Web Workers 的这个简单、但强大的语义。接下来,您将了解如何应用这个语义来加速移动 Web 应用程序。在此之前,有必要先讨论一下设备支持。毕竟,这些是移动 Web 应用程序,且处理不同浏览器之间的功能的区别对于移动 Web 应用程序开发很重要。

设备支持

从 Android 2.0 开始,Android 浏览器就拥有了对 HTML 5 Web Worker 规范的全面支持。在撰写本文之时,最新的 Android 设备(包括非常流行的 Motorola Droid)已配置了 Android 2.1。另外,此特性在运行 Maemo 操作系统的 Nokia 设备上的 Mozilla Fennec 浏览器以及 Windows Mobile 设备上受到完全支持。这里需要引起注意的遗漏是 iPhone。iPhone OS 3.1.3 和 3.2 版(在 iPad 上运行的 OS 的版本)并不支持 Web Workers。但是,此特性已在 Safari 上受到支持,因此,此特性在运行在 iPhone 上的 Mobile Safari 浏览器上出现应该只是一个时间问题。鉴于 iPhone 的主导地位(尤其是在美国),最好不要依赖 Web Workers 的存在,且不要只在您检测到它们的存在时才使用它们来增强您的移动 Web 应用程序。意识到这一点后,我们来看看如何使用 Web Workers 来加速您的移动 Web 应用程序。

使用 Workers 改善性能

智能手机浏览器上的 Web Worker 支持很不错,而且一直在不断改进。这就提出了一个问题:什么时候需要在移动 Web 应用程序中使用 Workers?答案很简单:需要完成耗时的任务的任何时候。有些示例展示了如何将 Workers 用于执行密集的数学计算,比如计算 1 万位数的圆周率。很可能您永远也不需要在 Web 应用程序上执行这样一个计算,在移动 Web 应用程序上执行这种计算的几率则更小。但是,从远程资源检索数据则相当常见,这也是本文示例的关注点。

在这个示例中,您将从 eBay 检索一个 Daily Deals(每天都在变化的交易)列表。这个交易列表包含关于每笔交易的简短信息。更详细的信息可以通过使用 eBay 的 Shopping API 获取。当用户浏览这个交易列表选择感兴趣的商品时,您将使用 Web Workers 来预取这个附加信息。要从您的 Web 应用程序访问所有这些 eBay 数据,您需要通过使用一个泛型代理(generic proxy)来处理浏览器的同源策略。一个简单的 Java servlet 将用于这个代理,它包含在本文的代码中,但不在这里单独展示。相反,我们将把注意力集中在处理 Web Workers 的代码上。清单 3 展示了这个交易应用程序的基本 HTML 页面。

清单 3. 交易应用程序 HTML

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta name = "viewport" content = "width = device-width">
    <title>Worker Deals</title>
    <script type="text/javascript" src="common.js"></script>
  </head>
  <body onload="loadDeals()">
    <h1>Deals</h1>
    <ol id="deals">
    </ol>
    <h2>More Deals</h2>
    <ul id="moreDeals">
    </ul>
  </body>
</html>

可以看出,这是一段非常简单的 HTML;它只是一个 shell。您使用 JavaScript 检索数据并生成 UI。这是移动 Web 应用程序的优化设计,因为它允许将所有代码和静态标记缓存到设备上,用户只需等待来自服务器的数据。注意,在 清单 3 中,一旦那个 body 加载,您就调用 loadDeals 函数,在那里,您将加载 清单 4 中的应用程序的初始数据。

清单 4. loadDeals 函数

				
var deals = [];
var sections = [];
var dealDetails = {};
var dealsUrl = "http://deals.ebay.com/feeds/xml";
function loadDeals(){
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
               var i = 0;
               var j = 0;
               var dealsXml = this.responseXML.firstChild;
               var childNode = {};
               for (i=0; i< dealsXml.childNodes.length;i++){
                   childNode = dealsXml.childNodes.item(i);
                   switch(childNode.localName){
                   case 'Item': 
                       deals.push(parseDeal(childNode));
                       break;
                   case "MoreDeals":
                       for (j=0;j<childNode.childNodes.length;j++){
                           var sectionXml= childNode.childNodes.item(j);
                           if (sectionXml && sectionXml.hasChildNodes()){
                               sections.push(parseSection(sectionXml));
                           }
                       }
                       break;    
                   default:
                       break;
                   }
               }
               deals.forEach(function(deal){
                   var entry = createDealUi(deal);
                   $("deals").appendChild(entry);
               });
               loadDetails(deals);
               sections.forEach(function(section){
                   var ui = createSectionUi(section);
                   $("moreDeals").appendChild(ui);
                   loadDetails(section.deals);
               });
        }
    };
    xhr.open("GET", "proxy?url=" + escape(dealsUrl));
    xhr.send(null);
}

清单 4 展示了 loadDeals 函数,以及应用程序中使用的全局变量。您使用了一个 deals 数组和一个 sections 数组。它们是相关交易的附加组(例如,Deals under $10)。还有一个名为 dealDetails 的映射,其键是 Item IDs(来自于交易数据),其值是从 eBay Shopping API 获取的详细信息。

您首先调用一个代理,该代理又将调用 eBay Daily Deals REST API。这将把交易列表作为一个 XML 文档提供给您。您解析用于进行 Ajax 调用的 XMLHttpRequest 对象的 onreadystatechange 函数中的文档。您还使用其他两个函数,parseDeal 和 parseSection,来将 XML 节点解析为更易于使用的 JavaScript 对象。这些函数可以在可下载的代码样例(参见 下载 部分)中找到,但由于它们只是令人厌烦的 XML 解析函数,因此我在这里没有包括它们。最后,在解析了 XML 后,您还使用了另外两个函数,createDealUi 和 createSectionUi,来修改 DOM。此时,这个 UI 如 图 1 所示。

图 1. Mobile Deals UI

如果您返回 清单 4,就会注意到在加载主交易之后,您对这些交易的每个部分都调用了 loadDetails 函数。在这个函数中,您通过使用 eBay Shopping API 加载每个交易的附加细节 — 但前提是浏览器支持 Web Workers。清单 5 展示了 loadDetails 函数。

清单 5. 预取交易细节

function loadDetails(items){
    if (!!window.Worker){
        items.forEach(function(item){
            var xmlStr = null;
            if (window.localStorage){
                xmlStr = localStorage.getItem(item.itemId);
            }
            if (xmlStr){
                var itemDetails = parseFromXml(xmlStr);
                dealDetails[itemDetails.id] = itemDetails;
            } else {
                var worker = new Worker("details.js");
                worker.onmessage = function(message){
                    var responseXmlStr =message.data.responseXml;
                    var itemDetails=parseFromXml(responseXmlStr);
                    if (window.localStorage){
                        localStorage.setItem(
                                        itemDetails.id, responseXmlStr);
                    }
                    dealDetails[itemDetails.id] = itemDetails;
                };
                    worker.postMessage(item.itemId);
            }
        });
    }
}

在 loadDetails 中,您首先检查全局作用域(window 对象)中的 Worker 函数。如果该函数不在那里,那么无需做任何事。反之,您首先检查 XML 的 localStorage 以获取这个交易的细节。这是移动 Web 应用程序常用的本地缓存策略,本系列第 2 部分(参见 参考资料 部分的链接)详细介绍过这种策略。

如果 XML 位于本地,那么您在 parseFromXml 函数中解析它并将交易细节添加到 dealDetails 对象。反之,则衍生一个 Web Worker 并使用 postMessage 向其发送 Item ID。当这个 Worker 检索到数据并将数据发布回主线程后,您解析 XML,将结果添加到 dealDetails,然后将 XML 存储到 localStorage 中。清单 6 展示了这个 Worker 脚本:details.js。

清单 6. 交易细节 Worker 脚本

importScripts("common.js");
onmessage = function(message){
    var itemId = message.data;
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
            postMessage({responseXml: this.responseText});
        }
    };
    var urlStr = generateUrl(itemId);
    xhr.open("GET", "proxy?url=" + escape(urlStr));
    xhr.send(null);
}

这个 Worker 脚本非常简单。您使用 Ajax 调用代理,该代理又调用 eBay Shopping API。当您收到来自代理的 XML 后,使用一个 JavaScript 对象文字(object literal)将其发送回主线程。注意,即使您能够使用来自一个 Worker 的 XMLHttpRequest,但所有信息都将返回它的 responseText 属性,而不是它的 responseXml 属性。这是因为这个 Worker 脚本范围内没有 JavaScript DOM 解析器。注意,generateUrl 函数来自 common.js 文件(见 清单 7)。您使用 importScripts 函数导入 common.js 文件。

清单 7. Worker 导入的脚本

				
function generateUrl(itemId){
    var appId = "YOUR APP ID GOES HERE";
    return "http://open.api.ebay.com/shopping?callname=GetSingleItem&"+
        "responseencoding=XML&appid=" + appId + "&siteid=0&version=665"
            +"&ItemID=" + itemId;
}

现在,您已经知道如何(为支持 Web Workers 的浏览器)填充交易细节,我们返回 图 1 研究一下如何在应用程序中使用这种方法。注意,每笔交易旁边都有一个 Show Details 按钮,单击该按钮修改这个 UI,如 图 2 所示。

 

图 2. 显示的交易细节

这个 UI 将在您调用 showDetails 函数时显示。清单 8 展示了这个函数。

清单 8. showDetails 函数

				
function showDetails(id){
    var el = $(id);
    if (el.style.display == "block"){
        el.style.display = "none";
    } else {
        el.style.display = "block";
        if (!el.innerHTML){
            var details = dealDetails[id];
            if (details){
                var ui = createDetailUi(details);
                el.appendChild(ui);
            } else {
                var itemId = id;
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function(){
                    if (this.readyState == 4 && 
                                      this.status == 200){
                        var itemDetails = 
                                        parseFromXml(this.responseText);
                        if (window.localStorage){
                            localStorage.setItem(
                                              itemDetails.id, 
                                              this.responseText);
                        }
                        dealDetails[id] = itemDetails;
                        var ui = createDetailUi(itemDetails);
                        el.appendChild(ui);
                    }
                };
                var urlStr = generateUrl(id);
                xhr.open("GET", "proxy?url=" + escape(urlStr));
                xhr.send(null);                        
            }
        }
    }
}

您收到了即将显示的交易的 ID 并切换是否显示它。当该函数第一次调用时,它将检查细节是否已经存储到 dealDetails 映射中。如果浏览器支持 Web Workers,那么这些细节已经存储且它的 UI 已经创建并添加到 DOM 中。如果这些细节还没有加载,或者,如果浏览器不支持 Workers,那么您需要执行一个 Ajax 调用来加载此数据。这就是这个应用程序无论在有无 Workers 时都同样能正常工作的原因。这意味着,如果 Workers 受到支持,那么数据就已被加载且 UI 将立即响应。如果没有 Workers,UI 仍将加载,只是需要花费几秒钟时间。

结束语

对于 Web 开发人员来说,Web Workers 听起来就像一种外来的新技术。但是,如本文所述,它们是非常实用的应用程序。这对于移动 Web 应用程序来说尤其正确。这些 Workers 可用于预取数据或执行其他预先操作,从而提供一个更加实时的 UI。这对于需要通过网速可能较慢的网络加载数据的移动 Web 应用程序来说尤其正确。结合使用这种技术和缓存策略,您的应用程序的快捷反应将使您的用户感到惊喜!

 

第 5 部分: 使用 HTML 5 开发新的可视化 UI 特性

简介: HTML 5 针对移动 Web 应用程序引入了大量新特性,其中包括一些可视化特性,它们通常会带来强烈的视觉冲击。Canvas 是最引人注目的新 UI 功能,能在浏览器中提供完整的 2D 图形。在本文中,您将学习如何使用 Canvas 以及 HTML 5 中的其他一些新视觉元素,对于移动用户来说,这些元素更微妙,但更与众不同。

先决条件

在本文中,您将使用最新的 Web 技术开发 Web 应用程序。这里的大部分代码只是 HTML、JavaScript 和 CSS — 所有 Web 发人员的核心技术。所需的最重要的工具是用于进行测试的浏览器。本文大部分代码将在最新桌面浏览器上运行,但也有一些例外,我们将在文章中进行说明。当然,您也必须在移动浏览器上测试,为此,您需要最新的 iPhone 和 Android SDKs。本文将使用 iPhone SDK 3.1.3 和 Android SDK 2.1。参见 参考资料 获取链接。

通过 Canvas 实现图形效果

多年以来,Web 开发人员一直在抱怨 Canvas。现在,为何还有人会抱怨浏览器中的一个原生画图 API 呢?毕竟,它允许您创建某种图形界面,否则您将需要某种浏览器插件(每位移动 Web 开发人员都知道,插件在一些最流行的移动浏览器上并不可用)。Web 开发人员抱怨 Canvas 的原因是:尽管现在它可用于 Firefox 和 Safari 已经很多年,但最流行的桌面浏览器 Microsoft?Internet Explorer? 一直不支持它。甚至 Internet Explorer 9 的早期版本也不支持 Canvas。因此,多年来,Canvas 一直是最大的技术笑话。您可能会发现,这些令人惊叹的 Canvas 样例遍布整个 Internet,但您不能仅仅因为 Internet Explorer 不支持它就将它用于大多数 Web 应用程序。幸运的是,对于移动 Web 开发人员来说,Canvas 没有这样的限制。您瞄准的所有基于 Webkit 的浏览器都能实现 Canvas 并极大地优化其性能。

Canvas API 是一个低级画图 API,它支持创建直线、曲线、多边形和圆圈,并用彩色、渐变色等填充它们。在 Canvas 上,您可以创建文本,执行各种各样的几何转换。可以想见,这样的 API 的用途是无限的。我们来看一个使用 Canvas 来创建一个图形报告的应用程序。图 1 展示了该应用程序的屏幕截图:一个显示每年结果的柱状图。

图 1. 运行在 Android 浏览器上的基于 Canvas 的报告应用程序

图 1 所显示的并不是浏览器中的一幅静态图像。这个报告图形是使用这个 Canvas API 实时生成的。清单 1 展示了创建这个报告的 HTML。

清单 1. 报告 HTML

<!DOCTYPE html>
<html>
<head>
    <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" />
    <title>HTML 5 Reports</title>
    <script type="text/javascript">
        function init(){
            var data = [{year : "2007",sales : 49},
                {year : "2008",sales : 131},
                {year : "2009",sales : 294}, 
                {year : "2010",sales : 405}];
            var report = {x : "year",
                    y : "sales",
                    values : data};
            graph(report, 350, 300);
        }
    </script>
</head>
<body onload="init()">
    <canvas id="graph"></canvas>
</body>
</html>

这个清单展示了基本的 HTML 结构。这个文档的主体拥有一个单一的 canvas 标记。在 init 函数(在文档主体加载时调用)中,您定义静态数据(报告数据)并将其传递到 graph 函数。尽管这里将报告定义为静态数据,但很容易将其想象为使用 Ajax 通过网络动态下载。report 函数包含所有有趣的代码,我们来看看 清单 2。

清单 2. graph 函数

function graph(report, maxWidth, maxHeight){
    var data = report.values;
    var canvas = document.getElementById("graph");
    var axisBuffer = 20;
    canvas.height = maxHeight + 100;
    canvas.width = maxWidth;
    var ctx = canvas.getContext("2d");

    var width = 50;
    var buffer = 20;
    var i = 0;
    var x = buffer + axisBuffer;
    ctx.font = "bold 12px sans-serif";
    ctx.textAlign = "start";
    for (i=0;i<data.length;i++){
        ctx.fillStyle = "rgba(0, 0, 200, 0.9)";
        ctx.fillRect(x, maxHeight - (data[i][report.y] / 2), 
                 width, (data[i][report.y] / 2));
        ctx.fillStyle = "rgba(0, 0, 0, 0.9)";
        ctx.fillText(data[i][report.x], x + (width / 4), maxHeight + 15);
        x += width + buffer;
    }

    // draw the horizontal axis
    ctx.moveTo(axisBuffer, maxHeight);
    ctx.lineTo(axisBuffer+maxWidth, maxHeight);
    ctx.strokeStyle = "black";
    ctx.stroke();

    // draw the vertical axis
    ctx.moveTo(axisBuffer,0);
    ctx.lineTo(axisBuffer,maxHeight);
    ctx.stroke();

    // draw gridlines
    var lineSpacing = 50;
    var numLines = maxHeight/lineSpacing;
    var y = lineSpacing;
    ctx.font = "10px sans-serif";
    ctx.textBaseline = "middle";
    for (i=0;i<numLines;i++){
        ctx.strokeStyle = "rgba(0,0,0,0.25)";
        ctx.moveTo(axisBuffer, y);
        ctx.lineTo(axisBuffer + maxWidth,y);
        ctx.stroke();
        ctx.fillStyle = "rgba(0,0,0, 0.75)";
        ctx.fillText(""+(2*(maxHeight -y)), 0, y);
        y += lineSpacing; 
    }
}

在这个函数的第一部分中,您建立了创建这个报告需要的对象,比如画布的宽度和高度,填充变量等。您还创建了画布上下文对象,因为这是用于进行所有实际画图的对象。然后,您通过迭代报告数据,绘制 图 1 中的柱状图。首先,您设置 fillStyle 属性。这就像设置一个颜色一样简单,您使用 CSS 时也可能会这样做。在本例中,使用 rgba 标记来设置颜色,以及 alpha 值(这是颜色的透明度,稍后我们讨论 奇妙的 CSS3 世界 时还将介绍这个参数)。设置 fillStyle 属性后,使用 fillRect API 来为数据点创建柱状图。这里,您指定这个矩形的起点 (x,y) 以及它的高度和宽度。接下来,您重新定义 fillStyle,因为您想打印一些文本,作为报告的一部分。您使用 fillText API 来在画布上绘制文本。您对每个数据点都执行这个操作,为每个数据点创建一个柱状图,其下带有一个标签。

接下来,您需要绘制这个图形的其他部分 — 轴线和网格线。首先,绘制水平和垂直轴线。对于每条轴线,使用 moveTo API 来设置开始绘制直线的起始点。然后使用 lineTo API 来从这个起始点到传递到这个 lineTo 调用中的端点之间绘制一条直线。注意,这并不实际绘制一条直线;相反,您调用 stroke API 来绘制这条直线。绘制水平和垂直轴线后,沿着它们的标签绘制网格线,方法是均匀间隔它们,然后使用相同的 moveTo、lineTo 和 stroke 组合来绘制这些直线。

这就是以编程方式创建这个报告图形所需的所有代码。在这个示例中,您已经看到了许多最重要的、也是最常使用的画布 API,但还有其他几个 API(比如用于绘制曲线)。您可以使用这些 API 完成一些令人惊讶的任务,这些任务可以在任何基于 Webkit 的浏览器上完成。如果您的目标不是绘制图形,HTML 5 仍旧有很多格式为 Cascading Style Sheets (CSS) 3.0 的新视觉图像。

奇妙的 CSS3 世界

谈到 HTML 5,您可能会马上想到 HTML 标记。当然,HTML 5 肯定包含新标记,我们将在下一小节中查看其中的部分新标记。在上一小节中,您看到了如何使用一个 <canvas> 标记来在 DOM 中创建一个画布对象。但是,大部分代码是 JavaScript。HTML 仅仅是 HTML 5 的一部分 — JavaScript 和 CSS 也是同样重要的部分。HTML 5 中的许多新用户界面元素由 CSS 标准的最新版 CSS 3.0 提供。在 图 2 中,一个使用几种 CSS 3.0 新技术的 Web 页面出现在基于 Android 的手机和 iPhone 上。

图 2. 移动设备的新 CSS 功能

图 2 在一个基于 Android 的设备和 iPhone 上展示了许多新 CSS 功能。左边的图像来自一个基于 Android 的设备。它比右边的图像更大,原因是它来自一个 Motorola Droid,它比用于右边的图像的 iPhone 3GS 拥有的分辨率更高的屏幕。因此,您在 Droid 上将看到更多的页面内容。但是,您可能会看到,标题 “The Gettysburg Address” 有一个倒影,该倒影在 iPhone 上逐渐淡出,但是在 Droid 上并不淡出,而是最终模糊下一个标题。这只是一个细节提醒:即使基于 Android 的设备和 iPhone 都拥有基于 Webkit 的浏览器,但它们之间也有细微的差别,因此您在测试时必须更加细心。现在看一下生成这个漂亮页面的代码(见 清单 3),首先从页面顶端开始。

清单 3. 页面上半部分的代码

<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript">
        function $(id){
            return document.getElementById(id);
        }
        function init(){
            var i=0;
            var row = {};
            var cell = {};
            var topics = ["nth-child", "gradients", "alpha", "text effects", 
                          "reflections", "transformations"];
            for (i=0;i<topics.length;i++){
                row = document.createElement("tr");
                cell = document.createElement("td");
                cell.appendChild(document.createTextNode(topics[i]));
                row.appendChild(cell);
                $("dtable").appendChild(row);
            }
        }
    </script>
    <style type="text/css">
        header > h1{
            color: yellow;
            background: -webkit-gradient(linear, left top, left bottom, 
                             from(blue), to(white))
        }
        tr:nth-child(4n+1) { color: navy; }
        tr:nth-child(4n+2) { color: green; }
        tr:nth-child(4n+3) { color: maroon; }
        tr:nth-child(4n+4) { color: purple; }

        input[type="text"]{
            background: rgba(150, 30, 30, 0.5);
        }
    </style>
</head>
<body onload="init()">
    <header>
        <h1>The World of CSS3</h1>
        <div>What kind of CSS3 does your browser support?</div>
    </header>
    <table id="dtable"></table>
    <div id="formSection">
        <label for="name">What's your name?</label>
        <input type="text" id="name"></input>
        <button id="rtBtn" onclick="rotate()">Rotate</button>
    </div>
</body>
</html>

清单 3 中的代码绘制标题 “Gettysburg Address” 上方的所有 UI。 您将稍后看到这个页面的下半部分的代码。

首先应该检查页面标题。如果您查看清单 3 底部附近的 HTML 页面主体,您将看到这个标题实际上位于一个 header 标记中 — 这是 HTML 5 中的一个新 HTML 元素。

现在看看 style 元素(位于 清单 3 中的 HTML 主体上方)。这个文本的样式使用带有选择器 header > h1 的 CSS 设置。这个规则将文本颜色设置为黄色,同时将其背景设置为蓝色和白色。背景应用有渐变。这是您看到的前缀为 -webkit 的 CSS 特性的首个示例。您可能会猜测,这个前缀使这个 CSS 专用于基于 Webkit 的浏览器。但是,在多数情况下,这些特性是 CSS 3.0 标准的一部分,但它们处于这个标准中还在不断轻微变化的部分。多数情况下,Webkit 浏览器和基于 Mozilla Firefox 的浏览器都已经实现了这些特性。如果您的开发也需要针对 Mozilla 浏览器(比如 Firefox 的移动版本 Fennec,该版本在欧洲的 Nokia 智能手机上迅速流行起来),那么您通常可以将 -webkit 前缀更改为 -moz。

下一步是使用名为 dtable 的表显示一个主题列表。如 图 2 所示,当您显示这个表的内容时,相邻两行的颜色各不相同。这个任务使用下一个 CSS 部分 — tr:nth-child 声明 — 来完成。可以在任意重复的元素上使用 nth-child 声明。您传递一个用作谓词的公式,检查它是否是一个有效的元素规则。在本例中,您声明表单行号为 4n+1(1、5、9,等等)的行的颜色为海军蓝,表单行号为 4n+2(2、6、10,等等)的行的颜色为绿色,以此类推,其余的行分别为栗色和紫色。过去,您经常需要对表、列表等组件实现类似的视觉效果,但通常是通过繁琐的 JavaScript 来实现。

最后的视觉元素是红色的文本字段,带有标签 What's your name? 和按钮 Rotate。这个文本字段的红色是使用一个特定于类型的输入选择器实现的。换句话说,这是一个 CSS 规则,只适用于类型为 text 的输入元素。现在,您可能想知道 Rotate 按钮到底作用何在?看看 清单 4 中的代码就明白了,该代码调用一个名为 rotate 的函数。

清单 4. 使用 CSS的 JavaScript rotate 函数

function rotate(){
    $("formSection").style["-webkit-transform"] = "rotateZ(-5deg)";
    $("formSection").style["-webkit-transition"] = 
          "-webkit-transform 2s ease-in-out";
    $("rtBtn").innerHTML = "Undo";
    $("rtBtn").onclick = function() {
        $("formSection").style["-webkit-transform"] = "";
        $("rtBtn").innerHTML = "Rotate";
        $("rtBtn").setAttribute("onclick", "rotate()");
    }
}

这个 rotate 函数使用 JavaScript 来更改应用到名为 formSection 的 div 的 CSS。(注意:您正在将 $() 用作 document.getElementById() 的一个别名。)要旋转这个 div,将它的 -webkit-transform 样式设置为 rotateZ(-5deg),将其逆时针旋转 5 度。接下来,将 -webkit-transform 样式设置为 -webkit-transform 2s ease-in-out。这使旋转耗时两秒钟,缓慢启动,加速,然后在最后再次减速。在 图 3 中,左边显示 What's your name? 字段旋转之前的初始位置;右边显示该字段部分旋转后的视觉效果及其 Undo 按钮。

图 3. 旋转 HTML 元素

参见 参考资料 中的链接查看这个效果在 Chrome、Safari 4 和 Opera 等兼容 HTML 5 的浏览器上的实际运行情况。

现在我们转到 图 2 中的页面的下半部分。这里您看到几个有趣的图像、文本效果以及布局示例。清单 5 显示了相关代码。

清单 5. 图 2 的下半部分代码

<!DOCTYPE html>
<html>
<head>
    <style type="text/css">
        h2 {
            -webkit-text-fill-color: blue;
            -webkit-text-stroke-color: yellow;
            -webkit-text-stroke-width: 1.5px;
            background: -webkit-gradient(radial, 430 50, 0, 430 50, 200, from(red), 
to(#000));
            -webkit-box-reflect:below 5px -webkit-gradient(linear, left top, left 
bottom, from(transparent), color-stop(0.5, transparent), to(white));
        }
        h3{
            color: rgba(0,0,255,0.75);
            background: rgba(255,255,0,0.5);
        }
        .xyz{
            text-shadow: #6374AB 4px -4px 2px;
            white-space: nowrap;
            width: 14em; 
            height: 2em; 
            overflow: hidden;
            text-overflow: ellipsis; 
            border: 1px solid #bbb; 
            border-radius: 9px;             
            background-color: #fff;
        }
        .abc {
            border: 1px solid #000;
            border-radius: 9px;        
            -webkit-column-count:4;
            -webkit-column-rule: 1px solid #a00;
            -webkit-column-gap: 0.75em;
        }
    </style>
</head>
<body onload="init()">
    <h2>The Gettysburg Address</h2>
    <h3>Abraham Lincoln, Gettysburg, PA. November 19, 1863</h3>
    <div class="xyz">
        Four score and seven years ago our fathers brought forth on this 
        continent a new nation, conceived in liberty, and dedicated to 
            the proposition that all men are created equal.
    </div>
    <div class="abc">
        Now we are engaged in a great civil war, testing whether that 
            nation, or any nation, so conceived and so dedicated, can long 
            endure. We are met on a great battle-field of that war. We have 
            come to dedicate a portion of that field, as a final resting 
            place for those who here gave their lives that that nation might 
            live. It is altogether fitting and proper that we should do this.
    </div>
</body>
</html>

我们来逐个看看这个代码中的元素。首先,为 “The Gettysburg Address” 创建了一个标题,并以几种方式设置其样式。

使用 -webkit-text-fill-color、-webkit-text-stroke-color 和 -webkit-text-stroke-width 样式来创建 “黄中带蓝” 的效果。
通过设置背景样式 -webkit-gradient 来在文本后面放置一个红黑色背景。注意,这是一个放射状渐变,而此前您看到的是一个线性渐变。这两种渐变在智能手机上的效果都很好。

通过设置 -webkit-box-reflect 样式对标题应用一个倒影。设置这个样式以在标题下方 5 个像素处反射标题,并对倒影应用一个渐变效果。这里的渐变效果使倒影看起来似乎正在淡出。如果回顾一下 图 2,您将看到,Android 浏览器不支持这个对倒影应用一个渐变的组合:它只是呈现不带任何渐变的倒影。

继续移动到下一个标题,对它应用一个非常简单的样式:文本一种颜色,背景另一种颜色。这两种颜色都使用 rgba 函数来指定 “红-绿-蓝” 值,以及一个 alpha 透明度值。值 1.0 完全不透明,值 0.0 则为透明。

清单 5 中的下一部分是这篇短文的第一段。文本周围有一个边界,您使用新的 border-radius 样式来实现 4 个圆角。现在,您在 Web 上到处都能看到这样的圆角,它们通常使用图像来实现。与使用 CSS 3.0 来实现相比,这种方法真是太老土了。通过使用 text-shadow 样式来向这个段落的文本应用一个阴影。最后,注意段落区域受到了父 div 的高度和宽度的限制,文本太大了。与在一些较老的浏览器中看到的那样直接截除文本相反,通过设置 text-overflow 样式可以获得一个美观的省略号(...)效果。

最后,来到文本的最后部分。它的周围也有一个边界,但是注意,它出现在 4 个带有列分隔符的列中。为此,设置 -webkit-column-count 样式,并设置配套的 -webkit-column-rule 样式来获取这些分隔符。可以想见,如果没有这些新的 CSS 3.0 特性,像这样格式化文本将会是多么的繁琐!当您创建简单的 headers 和 footers(它们二者也是 HTML 5 中的新元素)时,这也可能是一个很有用的特性。看一下它们以及由 HTML 5 引入的其他一些新标记。

新语义

HTML 5 向 HTML 标记添加了许多新元素。其中一些元素将导致浏览器提供一些新的呈现处理。其他一些元素将添加一些稍后可以通过 JavaScript 变得可用的额外特性。但是,另外一些元素则不具备上述两种功能。它们与 <span>、<div> 和 <p> 看起来一样,且拥有相同的编程接口。但是,它们将添加额外的语义含义。这些新语义对于页面的非视觉用户(包括使用屏幕阅读器这样的辅助技术的用户)和搜索引擎爬虫这样的计算机程序很重要。这些新标记还向开发人员提供一些挂钩,帮助他们编写表现力更强的 CSS 选择器。图 4 展示了一个使用一些新语义元素的 Web 页面。

图 4. iPhone 上的一些新 HTML 5 元素

图 4 中的这个示例拥有一个 header 元素、几个 nav 元素、一个 article 元素、一个 section 元素、以及一个 aside 元素。这些元素不会导致任何特殊呈现。它们只是添加语义值,您可以使用它们来编写向它们提供视觉处理来匹配该语义值的 CSS。图 4 中显示的图片的代码如 清单 6 所示。

清单 6. HTML 5 中的新语义元素

<!DOCTYPE html>
<html>
<head>
<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" />
<title>Get the latest markup</title>
</head>
<body>
    <header style="border: 1px dotted #000;border-radius: 3px;">
        <hgroup align="center">
            <h1>Real documents have headers</h1>
            <h2>Even if they don't say so</h2>
        </hgroup>
        <hgroup>
        <nav style="-webkit-column-count:3;-webkit-column-rule: 1px solid #a00;">
            <a href="new-css.html">CSS3</a><br/>
            <a href="report.html">Canvas</a><br/>
            <a href="elements.html">Markup</a>
        </nav>
        </hgroup>
    </header>
    <article>
       <h1>There are a lot of new markup elements in HTML5</h1>
       <time datetime="2010-05-16" pubdate>Sunday, May 16</time>
       <section>Did you notice that we just had two H1's? 
       But it's cool!</section>
       <aside style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" >
            If this page was really popular, I'd put an ad here and make some
            serious cash
        </aside>
    </article>
</body>
</html>

您可以看到此前提到过的几个新元素。注意,您还应用了一些新的 CSS 样式来在 header 周围创建一个圆角框,并为 nav 创建几个分隔符。您还在 aside 上使用了文本溢出样式。这里的关键点是:无需额外的工作,您就可以创建更有意义的标记,然后,您可以像使用了 <div> 和 <span> 一样显示该标记。要查看拥有更多视觉和编程效果的新 HTML 5 元素的一个示例,请看 图 5。(查看图 5 的 文本版本。)

图 5. 在 iPhone 上使用 HTML 5 创建的表单

图 5 中的屏幕使用了在 HTML 5 中可用的许多新表单元素。在很多情况下,这些元素看起来就像现有的元素,但您可以期望浏览器添加这些更丰富的表单元素的更好的视觉表示。为展示大致效果,图 6 显示了上述表单元素在 Opera 桌面浏览器中的效果。(查看图 6 的 文本版本。)

图 6. Opera 上的 HTML 5 表单元素

Opera 一直是实现 HTML 5 特性的先行者,这对于新的表单元素而言尤其如此。现在,看看生成 清单 4 和 清单 5 的代码,以便您更好地理解 Opera 之所以采用其呈现方式的原因。清单 7 显示了这个代码。

清单 7. 代码形式的 HTML 5 表单元素

<form id="settings">
<fieldset id="inputs" style="border: 1px solid #000;border-radius: 6px;">
    <legend>Settings</legend>
    <label for="name">Username</label>
    <input id="name" name="name" type="text" required autofocus /><br/>
    <label for="name">Name</label>
    <input id="name" name="name" type="text" 
          placeholder="First and last name" required /><br/>
    <label for="email">Email</label>
    <input id="email" name="email" type="email"           
           placeholder="example@domain.com" required /><br/>
    <label for="phone">Phone</label>
    <input id="phone" name="phone" type="tel" 
          placeholder="Eg. +447500000000" required /><br/>
    <label for="dob">Date of birth</label>
    <input id="dob" name="dob" type="date" required/>
    <fieldset style="border: 1px dotted #000; border-radius: 6px">
        <legend>Preferred Contact Method</legend>
        <ol>
            <li>
                <input id="emailMeth" name="contactMethod" 
                            type="radio">
                <label for="emailMeth">Email</label>
            </li>
            <li>
                <input id="phoneMeth" name="contactMethod" 
                            type="radio">
                <label for="phoneMeth">Phone</label>
            </li>
        </ol>
    </fieldset>
    <label for="climate">Preferred external temperature</label>
    <input id="climate" name="climate" type="range" min="50" 
          max="100" step="5" value="70"/><br/>
    <label for="color">Favorite color</label>
    <input id="color" name="color" type="color"/><br/>
    <label for="referrer">Where'd you hear about us?</label>
    <input type="url" name="refUrl" id="referrer" list="urls"/>
    <datalist id="urls">
        <option label="TechCrunch" value="http://www.techcrunch.com/">
        <option label="ReadWrite Web" value="http://www.readwriteweb.com/">
        <option label="Engadget" value="http://www.engadget.com/">
        <option label="Ajaxian" value="http://www.ajaxian.com/">
    </datalist><br/>
    <button type="button" onclick="checkInputs()">Save</button>
</fieldset>
</form>

清单 7 中的 form 元素展示了 HTML 5 的许多新特性。注意两个新属性:required 和 autofocus。required 属性用于表单验证(下面将详细介绍)过程中,autofocus 属性允许选择页面上的元素以获取焦点。还要注意几个拥有 placeholder 文本的元素。这是许多网站多年来一直在使用的一个模式 — 将某个示例或解释性文本放置到一个文本框中 — 但开发人员总是必须修改(hack)该代码。图 4 展示了 iPhone 如何整洁地实现这个模式。

下面,您将看到一些支持输入元素的新类型,比如 email, phone, date, range, color, 和 url。现在,这些类型在 iPhone 和 Android 浏览器上总是呈现为文本字段,但那只是使用语义不太准确的 HTML 4.0 创建它们的样子。图 5 展示了它们未来的可能外观。date 输入在 Opera 上展示其新 UI(一个弹出式日历)前必须获得焦点。这对于 图 7 中的 url 输入也同样适用,但原因不在于它是 url 输入,而是它拥有一个 list 属性。该属性指向 datalist 元素,该元素包含该字段支持的值。当您聚焦该字段时,您将看到来自 datalist 的可能值(本例中是几个 URL)。图 7 展示了 date 和 datalist 特性。

图 7. 带有日期和数据列表的 HTML 5 输入

随着 Webkit 持续快速发展,可以预见,许多输入类型将允许更有用的视觉表示。Opera 将为您带来更好的未来。许多这些输入字段也提供验证,HTML 5 拥有一组完整的新验证 APIs。这些特性目前还没有在 iPhone 或基于 Android 的设备上实现,但已经在它们的桌面对等设备上实现,因此,预计可以很快在移动浏览器上实现这些特性。清单 8 展示了这些新的验证 APIs 的使用情况。

清单 8. HTML 5 验证 APIs 的应用

function checkInputs(){
    var inputs = document.getElementById("inputs").childNodes;
    var len = inputs.length;
    var i = 0;
    var input = null;
    var errors = [];
    for (i=0;i<len;i++){
        input = inputs.item(i);
        if (input.nodeName == "INPUT"){
            if (input.validity){
                if (!input.checkValidity()){
                    errors.push(input.validationMessage);
                }
            }
        }
    }
    var errMsg = "There are " + errors.length + " errors";
    var notify = function(){
        var notification = 
            webkitNotifications.createNotification(null, "Errors!", errMsg);
        notification.show();
    };
    if (window.webkitNotifications){
        if (webkitNotifications.checkPermission()){
            webkitNotifications.requestPermission(notify);
        } else {
            notify();
        }
    } else {
        alert(errMsg);
    }
}

每个输入元素 拥有一个 validity 属性。可以使用这个属性,或者,也可以使用返回 ture 或 false 的 checkValidity() 函数以及 validationMessage 属性来获取一条本地错误消息。在本文撰写之时,多数最新的桌面浏览器并不能为 validationMessage 返回任何一致或标准的消息,因此,它的用途有限。validity 对象可用于检查不同类型的错误,比如 valueMissing、rangeOverflow、rangeUnderflow、patternMismatch 和 tooLong。例如,如果元素拥有一个必要属性而用户留空,那么 validity.valueMissing 将为 true。

最后,注意,在 清单 8 中,您在明确所有的验证错误之后,将尝试使用 webkitNotifications。这些通知与您的桌面计算机上的通知类似,可用于最新版 Chrome 中。因此,您同样可以期望它们将很快应用于 iPhone 和 Android 浏览器。API 的使用很简单明了。惟一需要注意的是,您需要检查用户是否已提供您的站点许可来使用这个 API。如果没有,您必须请求许可;反之,则传递想要被调用的函数。

结束语

在本文中,您快速浏览了 HTML 5 中许多与 UI 相关的新特性:从新元素到新样式再到绘图画布。这些特性(除了文末提到的几个例外之外)都可用于 iPhone 和基于 Android 设备上发现的基于 Webkit 的浏览器上。Blackberry 和 Nokia 智能手机等其他流行平台即将装配强大的浏览器,以便利用本文介绍的这些技术。作为一位移动 Web 开发人员,您有机会使用这些强大的视觉特性来瞄准广泛的用户,这些特性比桌面浏览器上使用 HTML、CSS 和 JavaScript 实现的任何特性都要强大。在本系列前 4 篇文章中,您了解到可用于这些精彩的新移动浏览器上的许多其他新技术(比如地理定位和 Web Workers)。移动 Web 并不是您多年来一直为之编程的 Web 的简化版,而是更强大的、拥有无限可能性的版本。

参考资料

学习

 

相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

 
分享到
 
 
     


十天学会DIV+CSS(WEB标准)
HTML 5的革新:结构之美
介绍27款经典的CSS框架
35个有创意的404错误页面
最容易犯的13个JavaScript错误
设计易理解和操作的网站
更多...   


设计模式原理与应用
从需求过渡到设计
软件设计原理与实践
如何编写高质量代码
单元测试、重构及持续集成
软件开发过程指南


东软集团 代码重构
某金融软件服务商 技术文档
中达电通 设计模式原理与实践
法国电信 技术文档编写与管理
西门子 嵌入式设计模式
中新大东方人寿 技术文档编写
更多...