html5移动web应用的本地存储
在本文中,您将使用最新 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 进行查看。
一系列本地高速缓存的 tweet 的屏幕截图(带有 Key 和 Value 字段)
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 应用程序的另一个新的令人振奋的功能 —— 离线 —— 是很关键的