第2部分:HTML5移动Web应用程序的本地存储

本文是《HTML5移动WEB应用程序》系列文章第 3篇 (共 4 篇)

本文简介

常用缩略语

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

  • API: 应用程序编程接口
  • CSS: 层叠样式表
  • DOM: 文档对象模型
  • HTML: 超文本标记语言
  • HTTP: 超文本传输协议
  • JSON: JavaScript 对象表示法
  • JSONP: 带填充的 JSON
  • SDK: 软件开发工具包
  • UI: 用户界面
  • URL: 统一资源定位符
  • W3C: 万维网联盟

本地存储基础

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 和 sessionStoragesessionStorage 对象是一个只在会话期间存储数据的 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 事件将与窗口相关联。它具有几个有用的属性:keyoldValue 和 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部分:HTML5移动Web应用程序离线工作第1部分:使用 HTML 5、地理定位 API 和 Web 服务创建移动应用程序 >>
以上是第2部分:HTML5移动Web应用程序的本地存储的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>