第九章 Web Storage API
本章,我们来探索HTML5的Web Storage(有时候也称为DOMStorage)API,看看如何方便地在web请求之间持久化数据。在Web Storage API出现之前,远程Web服务器需要存储客户端和服务器间交互使用的所有相关数据。随着Web Storage API的出现,开发者可以将需要跨请求重复访问的数据直接存储在客户端的浏览器中,还可以再关闭浏览器很久后再次打开时恢复数据,以减小网络流量。
本章首先介绍Web Storage 和cookie的区别,然后探讨存储和取出数据的方法。接着,我们会介绍localstorage和sessionstorage的区别,存储接口的特性、功能以及如何处理Web Storage事件。最后。我们会简要介绍Web SQL Database API和一些实用功能。
9.1 HTML5 Web Storage概述
为了解释Web Storage API,最好先回顾其前身,也就是名字很有趣cookie(小甜饼)。浏览器的cookie,其名字源自一种由来已久的编程技术,即在程序间传递小数据值的magic cookie。Cookie很神奇,它是一个在服务器和客户端来回传送文本值的内置机制。服务器可以给予其放在cookie中的数据在不同Web页面间追踪用户的信息。用户每次访问某个域时,cookie数据都会被来回传送。例如,cookie可以存储会话表示附近,使得web服务器能够通过cookie中存储的同服务器端购物车数据库对应的唯一ID,来识别哪个购物车属于当前用户。这样,用户在页面间切换时,购物车可以同步更新,保持一致。Cookie的另一个用途是将本地个性化数据存储在应用程序中,以便后续网页加载时使用。
Cookie的值也可用于用户不感兴趣的操作,例如跟踪用户浏览过的网页以挖掘广告目标客户。正因为如此,一些用户要求浏览器内置手动阻止或删除特定网站cookie的功能。
喜欢也好,讨厌也罢,早在20世纪90年代中期,cookie就得到了Netscape浏览器的支持。Cookie也是少数几个自web早起到现在浏览器厂商一直支持的功能之一。随着数据在服务器和浏览器之间传递,cookie可以在多个请求中追踪数据。尽管cookie无处不在,但它还是 有一些众所周知的缺点。
- cookie的值的大小受限,一般来说,一个cookie只能设置大约4KB的数据,这意味着它不能接受像文件或邮件那样的大数据。
- 只要有请求涉及cookie,cookie就要在服务器和浏览器间来回传送。一方面,这意味着cookie数据在网络上是可见的,它们在不加密的情况下有安全风险;另一方面,也意味着无论加载哪个相关URL,cookie中的数据都会消耗网络带宽。因此,从目前情况来看,相对较小的cookie意义更大。
许多情况下,即使不使用网络或远程服务器也能达到同样的目的,这正是HTML5 Web
Storage API的由来。使用这一简单的API,开发者可以将数据存储在JavaScript对象中,对象在页面加载时保存,并且容易获取。通过使用sessionStorage或LocalStorage。在打开新窗口或新标签页以及重新启动拥浏览器时。开发人员可以选择是否激活这些数据。存储的数据不会在网络上传输,重新浏览网页时也容易获取到。此外,使用Web Storage API 可以保存高达数兆字节的大数据。因此,Web Storage适用于存储超出cookie大小限制的文档和文件数据。
9.2 HTML5 Web Storage的浏览器支持情况
在HTML5的各项特性中,Web Storage的浏览器支持度是非常好的。其实,目前所有主流浏览器版本都在一定程度上支持Web Storage。从表9-1中可以看出,很多浏览器已经支持Web Storage了。
表9-1 支持HTML5 Web Storage的浏览器
浏览器 | 细节 |
Chrome | 3.0及以上版本支持 |
Firefox | 3.0及以上版本支持 |
Internet explorer | 8.0及以上版本支持 |
Opera | 10.5及以上版本支持 |
Safari | 4.0及以上版本支持 |
HTML5 Web Storage因其广泛的支持度而成为web应用中最安全的API之一。尽管如此,最好还是像往常一样,在使用之前先检测浏览器是否支持Web Storage。9.3.1小节中将介绍如何以编辑方式检查浏览器是否支持Web Storage。
9.3 使用 HTML5 Web Storage API
HTML5 Web Storage简单易用。我们先介绍数据的简单储存和获取,然后分析localstorage和sessionstorage之间的差异。最后,简单了解一下 Web Storage API的高级特性,例如数据变化时的事件通知等。
9.3.1 检查浏览器的支持性
在Web Storage API中,特定域下的storage数据库可直接利用window对象访问。因此,确定用户的浏览器是否支持Web Storage API,只要检查它是否存在window.localstorage或window.sessionstorage就行了。代码清单9-1可以检测浏览器是否支持Web Storage API,程序运行后将显示一条浏览器是否支持Web Storage API的消息。这段代码输入code/storage文件下的browser-test.html文件。除外之外,开发人员还可以使用moderniz来处理一些一些可能会导致错误的情况,如chrome匿名模式不支持storage等。
代码清单9-1 检测浏览器是否支持web storage
function checkStorageSupport() { //sessionStorage if (window.sessionStorage) { alert('This browser supports sessionStorage'); } else { alert('This browser does NOT support sessionStorage'); } //localStorage if (window.localStorage) { alert('This browser supports localStorage'); } else { alert('This browser does NOT support localStorage'); } }
程序运行结果如图9-1所示。
许多浏览器不支持从文件系统直接访问文件式的sessionstorage。所示,在运行本章示例之前。你应该确保是从web服务器上获取页面。例如,可以通过下面的命令来运行位于code/storage文件夹中的Python HTTP应用服务器:
python -m SimpleHTTPServer 9999
运行上述指令后,就可以访问http://localhost:9999上的文件了,例如http://localhost:9999/bowser-test.html。
提示:对于很多API来说,特定的浏览器可能的浏览器可能只支持其部分功能,但是因为 Web Storage API非常小,所以它已经得到了相当广泛地支持。不过出于安全考虑,即使浏览器本身支持web storage,用户仍然可自行选择是否将其关闭。
9.3.2 设置和获取数据
现在,我们将重点放在sessionstorage的功能上,你将学会如何设置和获取网页中的简单数据。设置数据值很简单,只需执行一条语句即可,下面给出了完整的语句声明:
window.sessionStorage.setItem(‘myFirstKey’, ‘myFirstValue’);
在上面的存储访问语句中,需要注意三点。
- 实现Web Storage API的对象时window对象的子对象,因此window.sessionstorage包含了开发人员需要调用的函数。
- setitem方法需要一个字符串类型的“键”和一个字符类型的“值”来作为参数。虽然官方的web storage API支持传递非字符串数据,但是目前浏览器可能还不支持其他数据类型。
- 调用结果是将字符串myfirstvalue设置到sessionstorage中,这些数据随后可以通过键myfirstkey获取
获取数据需要调用getitem函数。例如,如果我们把下面的声明语句添加到前面的示例中:
alert(window.sessionStorage.getItem(‘myFirstKey’));
浏览器将创建一个Javascript警告框来显示文本myfirstvalue。可以看出,使用Web Storage API设置和获取数据非常的简单。
不过,访问storage对象还有更简单的方法。你可以使用expando属性设置数据。使用这种方法,可完全避免调用setitem和getitem。而只是根据键值的配对关系,直接在sessionstorage对象上设置和获取数据。使用这种方法,我们的设置数据调用代码可以改写为:
window.sessionStorage.myFirstKey = ‘myFirstValue’;
同样,获取数据的代码可以可以改写为:
alert(window.sessionStorage.myFirstKey);
这只是Web Storage API的基础知识。现在,我们已经了解了如何在应用程序中使用sessionStorage。但有读者可能还存在疑问, sessionStorage 对象有什么特别之处?毕竟,JavaScript本来也允许开发人员设置和获取几乎任何对象的属性。其实,二者之间最大的不同在于作用域。你可能还没有意识到,在我们的示例中,设置和获取的调用不必出现在同一个网页中。只要网页是同源的(包括规则、主机和端口),基于相同的键。我们都能够在其他网页中获得设置在sessionStorage上的数据。在对同一页面后续多次加载的情况下也是如此。大部分开发者对页面重新加载时丢失脚本数据的问题已经习以为常了,但通过 Web Storage API保存的数据不再如此了。 重新加载页面后这些数据仍然还在。
9.3.3 封堵数据泄露
数据能够保存多久呢? 对于设置到sessionStorage中的对象,只要浏览器窗口(或标签)不关闭它们就会一直存在。当用户关闭窗口或浏览器,sessionStorage数据将被消除。sessionStorage中的数据在某种程度上有点像便条,其中的数据不会保存很久。所以开发人员不应该把真正有价值的东西放在里面。因为不能保证不论什么时候查询这些数据都会存在。
那么,为什么还要在Web应用程序中使用sessionStorage呢?sessionStorage
非常适合用于短时存在的流程中。如对话框和向导。如果数据需要存储在多个页面中,同时又不希望用户下一次访问应用程序时重新部署,则可将这些数据存储在sessionStorage中.以前,这类数据可能需要通过表单和cookie提交,并在页面加载时来回传递,而使用Storage可以避免这种开销。
Web Storage API还有另外一种特殊用法,它解决了一个一直困扰着诸多web应用程序的问题:数据作用域,以购买机票的购物应用程序为例。在这个应用程序中,诸如理想的触发返回日期这样的用户偏好数据,可能会在浏览器的服务器间使用cookie来回传递,为的是在用户使用应用时(如挑选座位和选餐),服务器应用程序能记住用户先前选择的偏好数据。
不过,用户打开多个窗口时很常见的,他们可能在查看旅游产品时同时打开多个窗口,比较不同代理商同一时起飞的航班。这会导致cookie系统出现问题,因为如果一个用户在比较价格和是否有票等情况时在浏览器窗口之间来回切换。他们很可能会在其中一个窗口设置cookie值,而在其他窗口中意外地将这些值应用到URL,相同的另一个网页的后续操作菏泽一现象也被称为数据泄露,其产生的根本原因在于cookie能够被同源网页共享。图9-2揭示了出现数据泄露的情况。
而使用sessionStorage能够跨页面(使用该应用的页面)暂存如启程日期这样的临时数据,又不会将其泄漏到用户仍在浏览其他航班信息的窗口中。这样,不同的偏好信息就会被隔离在预订相应航班的窗口中。
9.3.4 localStorage与sessionStorage
有时候,一个应用程序会用到多个标签页或窗口中的数据,或多个视图共享的数据。在这种情况下,比较恰当的做法是使用HTML5 Web Storage的另一种实现方式:localStorage。照描画虎,你应该已经猜到如何使用localStorage了。localStorage和sessionStorage在编程上唯一的区别是访问它们的名称不同,分别是通过localStorage 和sessionStorage对象来访问它们的。二者在行为上的差异主要是数据的保存时长及它们的共享方式。表9-2展示了二者的区别。
表9-2 sessionStorage和localStorage的区别
sessionStorage | localStorage |
数据会保存到储存它的窗口或标签页关闭时 | 数据的生命期比窗口或浏览器的生命期长 |
数据只在构建他们的窗口或者标签页内可见 | 数据可被同源的每个窗口或者标签共享 |
9.3.5 Web Storage API的其他特性和函数
HTML5 Web Storage API是HTML5中最简单的API之一。我们已经知道了如何基于sessionStorage和localStorage来显示和隐式地设置和获取数据的方法。接下来,我们将探讨该API的所有其他特性和函数。
在使用了sessionStorage或localStorage对象的文档中,我们可以通过window对象来获取它们。除了名字和数据的生命周期外,它们的功能完全相同且均实现了代码清单9-2 所示的storage接口。
代码清单9-2 storage接口
interface Storage { readonly attribute unsigned long length; getter DOMString key(in unsigned long index); getter any getItem(in DOMString key); setter creator void setItem(in DOMString key, in any data); deleter void removeItem(in DOMString key); void clear(); };
下面详细介绍借口中的特性和函数
- length特性表示目前Storage对象中存储的键一值对的数量。请记住,Storage对象是同源的,这意味着Storage对象的项数(和长度)只反映同源情况下的项数。
- key(index)方法允许获取一个指定位置的键。一般而言,最有用的情况是遍历特定Storage对象的所有键。键的索引从零开始,即第一个键的索引是0,最后一个键的索引是index(长度-1)。获取到键后,你可以用来它获取其相应的数据。除非键本身或者在它前面的键被删除,否则其索引值会在给定Storage对象的生命周期内一直保留。
- getitem(key)函数是根据给定的键返回相应数据的一种方式。另一种方式是Storage对象当做数组,而将键作为数组的索引。在这两种情况下,如果Storage中不存在的指定键,则返回null。
- 与getitem(key)函数类似,getItem(key,value)函数能够将数据存入指定键对应的位置,如果值已存在,则替换原值。需要注意的事设置数据可能会出错。如果用户已关闭了网站的存储,或者存储已大道其最大容量,那么此时设置数据将抛出QUOTA_EXCEEDED_ERR错误,如图9-3所示。因此,在需要设置数据的场合,务必保证应用程序能够处理此类异常。
- removeItem(key)函数的作用当然是删除数据项了,如果数据存储在键参数下,则调用此函数会将相应的数据项删除。如果键参数没有对应数据,则不执行任何操作。
提示:跟某些集合或数据框架不同,删除数据项时不会将原有数据作为结果返回。在删除操作前请确保已经存储了相应数据的副本。 - 最后是clear()函数,它能删除存储列表中的所有数据。空的Storage对象调用clear()方法也是安全的,此时的调用不执行任何操作。
磁盘空间配额
“HTML5”规范中建议浏览器允许每组同源页画使用5MB的空间。当达到空间配额时,浏览器应提示用户分配更多空间,同时还应提供查看每组同源页面已用的方法。
实际的实现方式多少有些出入。有些浏览器在不告知用户的前提下,允许页面使用更多的空间,有些浏览器则向用户提示已经扩展了空间。而另外一些则只是抛出QUOTA_EXCEEDED_ERR错误。如图9-3所示。图9-3所示示例的测试文件testquota.html位于code/storage文件失中。“
——Peter
9.3.6 更新web Storage后的通信
某些复杂情况下,多个网页、标签页或者Worker都需要访问存储的数据。此时,应用程序可能会在存储数据被修改后触发一系列操作,对于这种情况,HTML5 Web Storage API内建了一套事件通知机制,它可以将数据更新通知发送给感兴趣的监听着。无论监听窗口本身是否存储过数据,与执行存储操作的窗口同源的每个窗口的window对象上都会触发Web Storage事件。
提示:同源窗口间可以使用Web Storage事件进行通信,9.6节将对此做更加更如的探讨。
像下面这样,添加事件监听器即可接受同源窗口的Storage事件:
window.addEventListener("storage", displayStorageEvent, true);
代码中事件类型参数是storage,表明我们感兴趣的事storage事件。这样一来,只要有同源的Storage事件发生(包含sessionStorage和localStorage触发的事件),已注册的所有事件侦听器作为事件处理程序就会接收到相应的Storage事件。Storage事件的借口形式如代码清单9-3所示。
代码清单9-3 StorageEvent接口
interface StorageEvent : Event { readonly attribute DOMString key; readonly attribute any oldValue; readonly attribute any newValue; readonly attribute DOMString url; readonly attribute Storage storageArea; };
StorageEvent对象是传入事件处理程序的第一个对象,他包含了与存储变化有关的所有必要信息。
- key属性包含了存储中被更新或删除的键。
- oldvalue属性包含了更新前键对于的数据,newvalue属性包含更新后的数据。如果是新添加的数据,则oldvalue属性值为null。如果是被删除的数据,则newvalue属性值为null。
- url属性指向storage事件发生的源。
- storageArea属性石一个引用,它指向值发生改变的localStorage或sessionStorage。如此一来,处理程序就可以方便地查询到Storage中的当前值,或者基于其他Storage 的改变而执行其他操作。
代码清单9-4是一个简单的事件处理程序,它以警告框的形式来显示在当前页面上触发的Storage事件的详细信息。
代码清单9-4 显示storage事件内容的事件处理程序
// display the contents of a storage event function displayStorageEvent(e) { var logged = "key:" + e.key + ", newValue:" + e.newValue + ", oldValue:" + e.oldValue +", url:" + e.url + ", storageArea:" + e.storageArea; alert(logged); } // add a storage event listener for this origin window.addEventListener("storage", displayStorageEvent, true);
9.3.7 探索Web Storage
由于HTML5 Web Storage在功能上同cookie非常相似,所以最新浏览器在二者处理方式上的雷同也就不足为奇了。存储在localStorage或sessionStorage中的数据在簸新的浏览器中可以像cookie一样浏览,如图9-4所示。
Chrome提供的界面允许用户根据需要删除存储数据,而在访问网页时。用户可以轻易地浏览到网站记录了哪些数据。苹果公司的Safari浏览器和Chrome一样,都是基于WebKit渲染引擎,它也像Chrome那样统一显示cookie和storage。图9-5 显示了Safari储存面板。
Opera浏览器则更进一步,它不仅允许用户浏览器和删除储存数据,还允许创建数据,如图9-6所示。
随着众多浏览器开发商的努力。Web Storage的应用范围变得日益广阔。可以预见Web Storage提供给用户和开发人员的特性和工具也会越来越多,越来越强大。
9.4 构建HTML5 Web Storage应用
现在,让我们将前面所学知识应用到Web应用程序中。随着应用变得越来越复杂,无需服务器交互而管理尽可能多的数据变得越来越重要。将数据存储在本地客户端,避而从本地而不是远程获取数据,既可降低网络流量,又可提升浏览器响应能力。
一个困扰开发人员的常见问题是,当用户从应用程序的一个页面切换到另一个页面时如何管理数据。传统实现方式是由服务器存储数据,当用户在网页间相换时来回传递数据。还有一种做法是应用程序尽可能地址用户停留在一个动态更新的网页上。不过。用户更习惯于在页面阔切换,当用户返回到应用程序的某个页面时。如果能够快速在取数据并加以显示,对于增强用户体验来说无疑是非常好的方式。
在示例应用程序中,我们将演示用户在一个网站的页面间切换时,如何将应用程序的临时数据存储在本地,以及如何从每个页面的storage中快速加载。我们将对前面章节中的示例进行改造。在第4章中,我们发现收集用户的当前位置很容易。随后,在第6章中,我们演示了如何获取位置数据并将其发送到远程服务器,以供所有感兴趣的用户浏览。本届,我们将更进一步:监听由Websocket广播的位置数据并将其储存在本地storage中,以便用户在页面切换时可以立即获取应用程序所需的数据。
试想,我们的跑步俱乐部程序可以获取参赛选手的实时位置信息,这些信息由选手的移动设备广播出来。通过Websocket 服务器实现共享。由于参赛选手在比赛中不断上传新的位置信息,所以对于Web应用程序来说,要实时显示每个参赛选手的位置还是比较简单的。优秀的网站会将参赛选手的位置信息存入缓存,当用户在网站的页面间切换时,可以快速显示位置信息。这正是我们接下来要构建的。
为了达到这一目的,我们需要引进一个示例网站,它应该可以保存和恢复参赛选手数据。我们已经做好了一个包含3个页面的跑步比赛网站,并将其放在了在线资源的code/storage文件夹下。当然,你可以选择任意网站来做演示。关键是网站要包含多个页面, 且能让用户在它们之间轻松切换。我们会在这些网页中插入少量动态内容来表示实时排行榜,排行榜的内容实际上就是一张所有参赛选手距离终点还有多远的距离列表。图9-7 显示了跑步网站包含的三个页面。
三个页面的共同点是都包含了一张排行榜。排行榜中的每项都会显示参赛选手的名字以及他到终点的距离。加载任一页面时。都会为排行榜建立一个到比赛广播服务器的阶Web Socket连接,监听参赛选手的位置信息。反过来,参赛选手将向同一个广播服务器发送他们目前的位置信息,并最终将位置信息实时传递到页面。
所有这些都与前面章节中提到的Geolocation和WebSocket有关。事实上,这里大部分的演示代码是本书前面的示例代码。不过,这个示例与前面的示例有一个关键的不同点:当数据到达页面后,我们会将其存储在sessionStorage中,以供后期获取。然后,无论何时用户转向一个新页面,在新的webSocket连接创建之前,存储的数据都将被获取和显示。这样一来,在页面之间传输临时数据就不必依赖cookie或Web服务器了。
正如我们在第6 章中所做的那样,我们将参赛选手的位置信息以容易读取和解析的格式通过Web传送。参赛选手的位置信息是一个字符串,使用分号(;)作为数据块的分隔符,将姓名、纬度和经度分隔开。例如,参赛选手Racer X位于纬度37.20,经度-121.53,可以使用下面的字符串:
;Racer X;37.20;-121.53
现在,我们来分析代码。每个网页都包含功能相同的JavaScript代码,具体功能包括连接到WebSocket服务器、处理和显示排行榜信息以及使用sessionStorage保存和恢复排行榜。因此,在构建真实的应用程序时, lavaScript库中可以考虑包含这些代码。
首先,我们将构建一些通用函数。.要计算任意参赛选手当前位置与终点间的距离,我们需要代码清单9-5的程序。
代码清单9-5 距离计算程序
// functions for determining the distance between two // latitude and longitude positions function toRadians(num) { return num * Math.PI / 180; } function distance(latitude1, longitude1, latitude2, longitude2) { // R is the radius of the earth in kilometers var R = 6371; var deltaLatitude = toRadians((latitude2-latitude1)); var deltaLongitude = toRadians((longitude2-longitude1)); latitude1 = toRadians(latitude1), latitude2 = toRadians(latitude2); var a = Math.sin(deltaLatitude/2) * Math.sin(deltaLatitude/2) + Math.cos(latitude1) * Math.cos(latitude2) * Math.sin(deltaLongitude/2) * Math.sin(deltaLongitude/2); var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); var d = R * c; return d; } // latitude and longitude for the finish line in the Lake Tahoe race var finishLat = 39.17222; var finishLong = -120.13778;
这些函数我们已经非常熟悉,早在第6章就用过了,distance函数用来计算两点之间的距离。函数的细节和它在比赛中能否最准确地表示距离都不重要,重要的是它可以用在我们的示例中以演示效果。
在终点线,我们确定了比赛终点位置的经度和纬度。从代码中可以看到,我们将其与输入的参赛选手位置坐标进行比较。以确定参赛选手到终点线的距离,从而得到他们在这场比赛中的排名顺序。
现在,让我们来看看用于显示网页的HTML代码片段。
<h2>Live T216 Leaderboard</h2> <p id="leaderboardStatus">Leaderboard: Connecting...</p> <div id="leaderboard"></div>
虽然HTML网页中大部分内容与我们的阐述目的无关,但在这几行中,我们声明了ID为leaderboardStatus和leaderboard的命名元素。leaderboardStatus命名元素用于显示WebSocket的连接信息,我们将在leaderboard命名元素中插入表示位置信息的div元素。调用代码清单9-6所示的通用函数,可以从WebSocket 消息获得这些位置信息。
代码清单9-6 位置信息实用函数
// display the name and distance in the page function displayRacerLocation(name, distance) { // locate the HTML element for this ID // if one doesn't exist, create it var incomingRow = document.getElementById(name); if (!incomingRow) { incomingRow = document.createElement('div'); incomingRow.setAttribute('id', name); incomingRow.userText = name; document.getElementById("leaderboard").appendChild(incomingRow); } incomingRow.innerHTML = incomingRow.userText + " is " + Math.round(distance*10000)/10000 + " km from the finish line"; }
这是一个简单的显示函数,它接收参赛选手的名字及其到终点的距离。如9-8是index.html网页上排行榜展示效果。
参赛选手的名字有两个用途:它不仅显示在参赛选手的状态信息中,还用来引用存储参赛选手状态信息的唯一div元素。如果与参赛选手相对应的div元素存在,那么我们可以使用标准语句document.getElementById()来查找到它。如果与参赛选手相对应的div元素不存在,我们将创建相应的div元素并将其插入到leaderboard命名元素中。无论哪种情况,我们随后都会根据参赛选手到终点线的最新距离更新div元素,实现它在页面中的实时更新。如果你已经阅读过第6章,那么对我们在这里构建的示例程序应该已经很熟悉了。
我们的下一个函数是消息处理器,它将在WebSocket服务器返回数据时被调用, 如代码清单9-7所示。
代码清单9-7 WebSocket消息处理函数
// callback when new position data is retrieved from the websocket function dataReturned(locationData) { // break the data into ID, latitude, and longitude var allData = locationData.split(";"); var incomingId = allData[1]; var incomingLat = allData[2]; var incomingLong = allData[3]; // update the row text with the new values var currentDistance = distance(incomingLat, incomingLong, finishLat, finishLong); // store the incoming user name and distance in storage window.sessionStorage[incomingId] = currentDistance; // display the new user data in the page displayRacerLocation(incomingId, currentDistance); }
这个函数的参数是一个前面介绍过的格式化字符串,它将参赛选手的名字、经度和纬度以分号分隔,我们的第一步是使用JavaScript的split()函数将它分成各个部分,以便对incomingid、Incominglat、incominglong单独处理。
然后,它将参赛选手的经度和纬度、终点线的经度和纬度信息传递给之前定义的distance()函数,并将计算得到的距离存储在currentDistance变量中。
现在,我们有了一些需要存储的数据,来看一下如何实现HTML5 Web Storage的调用。
// store the incoming user name and distance in storage window.sessionStorage[incomingId] = currentDistance;
上面这行代码中,我们使用sessionStorage对象来存储参赛选手到终点线的当前距离,其对应的键是参赛选手的名字(或ID)。换句话说,我们在sessionStorage中设置了一组数据。数据以参赛选手的名字作为键,以参赛选手到终点的距离作为值。我们很快就会用上这些保存在Storage中的数据,当用户在网站的页面间切换时。数据会从Storage中恢复。在函数的末尾。我们调用了已经定义过的displaylocation()函数,以确保最新的位置能够显示在当前页面中。
现在,我们来看看Storage示例中的最后一个函数一一用户任何时候访问页面都会触发的加载函数。如代码清单9-8所示。
代码清单9-8 初始化页面加载程序
// when the page loads, make a socket connection to the race broadcast server function loadDemo() { // make sure the browser supports sessionStorage if (typeof(window.sessionStorage) === "undefined") { document.getElementById("leaderboardStatus").innerHTML = "Your browser does not support HTML5 Web Storage"; return; } var storage = window.sessionStorage; // for each key in the storage database, display a new racer // location in the page for (var i=0; i < storage.length; i++) { var currRacer = storage.key(i); displayRacerLocation(currRacer, storage[currRacer]); } // test to make sure that Web Sockets are supported if (window.WebSocket) { // the location where our broadcast WebSocket server is located url = "ws://websockets.org:7999/broadcast"; socket = new WebSocket(url); socket.onopen = function() { document.getElementById("leaderboardStatus").innerHTML = "Leaderboard:Connected!"; } socket.onmessage = function(e) { dataReturned(e.data); } } }
代码清单9-8所示的函数相对较长,以后还会遇到很多这么长的代码。让我们逐步分析loaddemo()函数。如代码清单9-9所示,我们首先进行基本的错误检查,通过检测浏览器页面的window对象中是否存在sessionStorage对象,来判断浏览器是否支持Web Storage。如果无法访问sessionStorage,则更新leaderboardStatus来提示用户,并提前结束加载程序。本示例中,当浏览器不支持Storage时,我们不提供备选方案。
代码清单9-9 检查浏览器是否支持sessionStorage
// make sure the browser supports sessionStorage if (typeof(window.sessionStorage) === "undefined") { document.getElementById("leaderboardStatus").innerHTML = "Your browser does not support HTML5 Web Storage"; return; }
提示:在实际项目中,如果浏览器不支持Storage。很可能要重写程序,以便在页面切换时 放弃保存持久化数据,并在页面加载时显示空白排行榜。不过,这里就不需要了,因 为我们的目标只是演示Storage如何优化用户的体验和减轻网络负担。
页面加载时,要做的下一件事情是获取保存在Storage 中的所有参赛选手到终点的距离结果,这些结果已经在网站的页面上出现过了。回想一下,网站中的每个页面都运行了一段相同的脚本,从而使用户浏览不同页面时都能看到排行榜。这时,其他页面上的排行榜可能已经向Storage中写入了数据,所以,调用代码清单9-10中的代码,在页面加加载时直接获取数据并加以显示即可。只要用户不关闭窗口、标签页或浏览器,不清除sessionStorage,保存过的数据就会在用户浏览时一直有效。
代码清单9-10 显示保存的参赛选手数据
var storage = window.sessionStorage; // for each key in the storage database, display a new racer // location in the page for (var i=0; i < storage.length; i++) { var currRacer = storage.key(i); displayRacerLocation(currRacer, storage[currRacer]); }
上面的代码是整个示例应用中的核心部分。我们首先查询了sessionStorage的长度,也就是Storage中有多少个键。然后,调用storage .key()获取每一个键,并将其存储在currRacer 变量中。最后。调用storage[currRacer]方法,传入currRacer 变量,获取当前键所对应的值。键及其对应的值分别代表参赛选手的名字和他到终点的距离,这些信息都是在访问之前的页面时存储的。
一且获取了已经储存的参赛选手姓名和距离。我们就可以使用displayRacerLocation() 函数将它们显示出来。页面加载时,一切都发生在电光火石间,网页会在瞬间用先前传递过来的数据填充排行榜。
提示:我们的示例应用程序只依赖于将数据存储到sessionStorage的应用。如果应用程序需要与其他数据共享Storage对象,那么你需要使用更加细致的键存储策略,而不再是将键简单地存放在根级别下。我们将在9.6节中了解另一种存储策略。
最后一块工作是使用WebSocket将页面连接到跑步比赛广播服务器上,如代码清单9-11所示。
代码清单9-11 连接到WebSocket广播服务
// test to make sure that WebSocket is supported if (window.WebSocket) { // the location where our broadcast WebSocket server is located url = "ws://websockets.org:7999/broadcast"; socket = new WebSocket(url); socket.onopen = function() { document.getElementById("leaderboardStatus").innerHTML = "Leaderboard: Connected!"; } socket.onmessage = function(e) { dataReturned(e.data); } }
正如我们在第6章中所傲的那样,首先检查window.WebSocket对象是否存在,以确定浏览器是否支持WebSocket。。一旦确认,我们就连接到运行着WebSocket 服务器的URL. 服务器会广播以分号分隔格式组织的参赛选手位置信息。在socket.onmessage回调函数接收到消息时,我们会调用先前讨论的dataReturned()函数来处理和显示它。此外,我们还使用了Socket. onopen 方法,以将简单的诊断信息重新到leaderboardStatus 命名元素中,来表明成功打开了套接字。
下面该轮到页面加载了。脚本中最后声明的一块代码用于注册事件监听器,这样一来,网页加载成功后就会自动调用loadDemo() 函数:
// add listeners on page load and unload window.addEventListener("load", loadDemo, true);
前面提到多次了,只有当页面完全加载后,才能通过事件监听器调用loadDemp() 函数。
不过,要怎么做才能将参赛选手的数据从比赛路线传输到WebSocket广播服务器,继而传入我们的页面呢?这就要用到第6章所演示的跟踪器示例了。很简单,只需要将连接URL指向前面列出的广播服务器即可。毕竟,字符串格式都是一样的。代码清单9-12是我们构建的一个非常简单的参赛选手广播来源页面,能够达到类似目的。理论上,这个页面可以运行在参赛选手的移动设备上。尽管文件本身不包括任何HTML5 Web Storage代码,但在支持WebSocke和Geolocation的浏览器上,它却是一条传输格式化数据的便捷方式。racerBroadcast.html文件可以从本书的示例源代码文件中找到。
代码清单9-12 racerBroadcast.html文件内容
<!DOCTYPE html> <html> <head> <title>Racer Broadcast</title> <link rel="stylesheet" href="styles.css"> </head> <body onload="loadDemo()"> <h1>Racer Broadcast</h1> Racer name: <input type="text" id="racerName" value="Racer X"/> <button onclick="startSendingLocation()">Start</button> <div><strong>Geolocation</strong>: <p id="geoStatus">HTML5 Geolocation not started.</p></div> <div><strong>WebSocket</strong>: <p id="socketStatus">HTML5 Web Sockets are <strong>not</strong> supported in your browser.</p></div> <script type="text/javascript"> // reference to the Web Socket var socket; var lastLocation; function updateSocketStatus(message) { document.getElementById("socketStatus").innerHTML = message; } function updateGeolocationStatus(message) { document.getElementById("geoStatus").innerHTML = message; } function handleLocationError(error) { switch(error.code){ case 0: updateGeolocationStatus("There was an error while retrieving your location: " + error.message); break; case 1: updateGeolocationStatus("The user prevented this page from retrieving a location."); break; case 2: updateGeolocationStatus("The browser was unable to determine your location: " + error.message); break; case 3: updateGeolocationStatus("The browser timed out before retrieving the location."); break; } } function loadDemo() { // test to make sure that Web Sockets are supported if (window.WebSocket) { // the location where our broadcast WebSocket server is located url = "ws://websockets.org:7999/broadcast"; socket = new WebSocket(url); socket.onopen = function() { updateSocketStatus("Connected to WebSocket race broadcast server"); } } } function updateLocation(position) { var latitude = position.coords.latitude; var longitude = position.coords.longitude; var timestamp = position.timestamp; updateGeolocationStatus("Location updated at " + timestamp); // Schedule a message to send my location via WebSocket var toSend = ";" + document.getElementById("racerName").value + ";" + latitude + ";" + longitude; setTimeout("sendMyLocation('" + toSend + "')", 1000); } function sendMyLocation(newLocation) { if (socket) { socket.send(newLocation); updateSocketStatus("Sent: " + newLocation); } } function startSendingLocation() { var geolocation; if(navigator.geolocation) { geolocation = navigator.geolocation; updateGeolocationStatus("HTML5 Geolocation is supported in your browser."); }else { geolocation = google.gears.factory.create('beta.geolocation'); updateGeolocationStatus("Geolocation is supported via Google Gears"); } // register for position updates using the Geolocation API geolocation.watchPosition(updateLocation, handleLocationError, {maximumAge:20000}); } </script> </body> </html>
它与第6 章的跟踪器示例几乎一模一样,不再赘述。主要的区别在于,这个文件包含了参赛选手名字的文本输入框:
Racer name: <input type="text" id="racerName" value="Racer X"/>
参赛选手的名字会作为数据字符串的一部分发送到广播服务器:
var toSend = ";" + document.getElementById("racerName").value + ";" + latitude + ";" + longitude;
如果要测试的话,可以在像Googlechrome这样支持HTML5 Web Storage、Geolocation和WebSocket的浏览器中打开两个窗口。首先,在第一个窗口中加载跑步俱乐部的index.html 页面。你会看到它使用WebSocket连接到了赛事广播网站,然后等待参赛选手数据通知。其次,在第二个窗口中打开racerBroadcast.html文件。同样,页面打开后也会连接到WebSocket 广播网站。输入参赛选手的各字,然后点击”start” 按钮。你会看到WebSocket服务器将这名参赛选手的位置信息发送出去,然后它会在浏览器另一个窗口的排行榜中出现。显示效果如图9-9 所示。
可以通过页面左侧的”Signup” 和”About the Race”链接导航到跑步俱乐部的其他页面。由于这些页面都加载了我们的脚本,它们会立即使用已有的参赛选手数据加载和填充页面中的排行榜。 (广播页面)会发出更多的参赛选手状态通知,这样一来,不管我们切换到网站的哪个页面。都能看到这些信息。
现在,代码已经全部完成了,让我们回顾一下都做了些什么。我们构建了一个适合包含在共享JavaScript库中的简单功能块,它连接到一个WebSocket 广播服务器并监听参赛选手数据更新信息。收到更新信息后,脚本会在页面中显示位置数据,并将其使用HTML5 Web Storage存储起来。当页面加载时,它会检查所有已经存储的参赛选手数据,从而保证用户在浏览网站时所有状态都是正确的。这么做有哪些优势呢?
- 减少网络流量:赛事信息存储在本地的浏览器中。一旦获取到赛事信息, 每个页面加载时都会从本地获取数据,而不是使用cookie或服务器请求获取。
- 即时显示数据:因为页面的动态部分(当前的排行榜状态)是本地数据,所以浏览器网页本身可以缓存而不是从网络上加载。无需耗费任何网络加载所需的时间就可迅速显示这些数据。
- 临时存储:数据在比赛结束后就没用了。因此,我们把它存在了sessionStorage 中, 这意味着窗口或标签页关闭后它就会被丢弃,不再占用任何空间。
无懈可击
“我们在示例中只用了寥寥几行脚本代码就完成了很多事。但是。不要以为在现实中开发可供公开访问约两站也会这么简单。在实际应用中直接使用这里的示例代码是不现实的。
例如,我们的信息格式不支持重名的参赛选手名字, 所以最好每个参赛选手有一个唯一的标识符。我们计算到终点的距离是”直线距离“,不能用于真正的越野赛事。更本地化、更多的错误检查以及更注重细节会让你的网站适用于所有参赛选手。这也算是个免责声明吧 。”
——Brian
示例中用到的技术可以应用到各种类型的数据上,像聊天、电子邮件、体育计分都是很好的示例,都可以像我们所演示的那样,使用localStorage或sessionStorage在页面间缓存并显示信息。如果应用程序需要定期在浏览器和服务器间发送特定的用户数据、不防考虑使用HTML5 Web Storage来简化流程。
9.5 浏览器数据库存储展望
键一值对形式的Storage API在数据持久化方面已经很强大了,但是如果为Storage建立查询索引又会如何呢?HTML5的应用同样可以访问索引数据库。数据库API的具体细节仍在完善。并有多个方案。Web SQL Database是其中之一,并已经在Safari、Chrome 和Opera中实现了。表9-3显示了浏览器对于Web SQL Database的支持情况。
表9-3 浏览器对于Web SQL Database的支持情况
浏览器 | 细节 |
Chrome | 3.0及以上的版本均支持 |
Firefox | 不支持 |
Internet explore | 不支持 |
Opera | 10.0及以上的版本均支持 |
Safari | 3.2及以上的版本均支持 |
Web SQL Database允许应用程序通过一个异步JavaScript接口访问SQLite数据库。虽然它既不是常见web平台的一部分。也不是THML5规范最终推荐的数据库API,但当针对如Safari移动版这样的特定平台时,SQL API会很有用。在任何情况下, SQL API在浏览器中的数据库处理能力都是无可比拟的。跟其他Storage API一样,浏览器能够限制同源页面可用Storage的大小,并且当用户数据被清除时, Storage中的数据也会被清除。
Web SQL Database
“虽然Web SQL.Database已经在Safari、Chrome和opera中实现,但是Firefox中并没有实现它,而且它在WHATWG维基中也被列为‘停滞‘状态。 HTML5规范中定义了一个执行SQL命令的API,接收字符串的输入,遵从SQLite对SQL语句的解析规范。由于标准认定直接执行SQL语句不可取,Web SQL Database已被往新的规范一一索引数据库(lndexed Database. 原为WebSimpleDB) 所取代。索引数据库史简便,而且也不依赖于特定的SQL数据库版本。目前浏览器正在逐步实现对索引数据库的支持。”
——Frank
因为Web SQL Database事实上已被各大浏览器厂商支持,所以我们将介绍一个基本示例,其中省略了对Web SQL Database API 细节的完整说明。示例演示了Web SQL Database API 的基本用法。它打开名为mydb的数据库,创建racers表(如果表不存在的话),向其中填充预定义的名字。图9-10是在Safari的web inspector面板中显示的带有racers表的数据库。
我们通过数据库名打开相应数据库。与数据库交互时, window.openDatabase()函数会返回Database对象。Opendatabase()函数将数据库名作为必选参散,将版本说明作为可选参数。打开数据库后,应用程序代码就可以对其进行读写操作了。 transaction.executesql()函数用来在事务上下文中执行SQL语句。在这个简单示例中,我们将使用executeSQL() 函数创建一张数据表。并向其中插入参赛选手的名,随后通过查询数据库的方式获取数据,进而创建一张用于显示的HTML表格。图9-11显示了输出的HTML 文件,其中包含从数据表中检索出来的姓名列表。
数据库操作可能需要花点时间才能完成。不过,在获得查询结果集之前。查询操作会在后台运行,以避免阻塞脚本的执行. executeSQL ()的第三个参数是回调函数,查询得到的事务和结果集将作为参数供此回调函数使用。
代码清单9-13 是存放在code/storage文件夹下的sql.html文件的完整代码。
代码清单9-13 使用Web SQL Database API
<!DOCTYPE html> <title>Web SQL Database</title> <script> // open a database by name var db = openDatabase('db', '1.0', 'my first database', 2 * 1024 * 1024); function log(id, name) { var row = document.createElement("tr"); var idCell = document.createElement("td"); var nameCell = document.createElement("td"); idCell.textContent = id; nameCell.textContent = name; row.appendChild(idCell); row.appendChild(nameCell); document.getElementById("racers").appendChild(row); } function doQuery() { db.transaction(function (tx) { tx.executeSql('SELECT * from racers', [], function(tx, result) { // log SQL result set for (var i=0; i<result.rows.length; i++) { var item = result.rows.item(i); log(item.id, item.name); } }); }); } function initDatabase() { var names = ["Peter Lubbers", "Brian Albers", "Frank Salim"]; db.transaction(function (tx) { tx.executeSql('CREATE TABLE IF NOT EXISTS racers (id integer primary key autoincrement, name)'); for (var i=0; i<names.length; i++) { tx.executeSql('INSERT INTO racers (name) VALUES (?)', [names[i]]); } doQuery(); }); } initDatabase(); </script> <h1>Web SQL Database</h1> <table id="racers" border="1" cellspacing="0" style="width:100%"> <th>Id</th> <th>Name</th> </table>
9.6 进阶功能
尽管有些技巧我们在示例中用不上,但并不妨碍它们在多种类型的HTML5 Web应用中发挥作用。本节,我们就来介绍一些简单使用的进阶功能。
9.6.1 JSON对象的储存
虽然HTML5 Web Storage规范允许将任意类型的对象保存为键一值对形式,实际情况却是一些浏览器将数据限定为文本字符串类型。不过,既然现代浏览器原生支持JSON (JavaScript Object Notation)。我们额外有了一种解决这个问题的可行方案。
JSON是一种将对象与字符串可以相互表示的数据转换标准。十余年来,JSON一直是通过HTTP将对象从浏览器传送到服务器一张常用格式。现在,我们可以通过序列化复杂对象将JSON数据保存在storage中,以实现复杂数据类型的持久化。脚本如代码清单9-14所示。
代码清单9-14 储存JSON对象
<script> var data; function loadData() { data = JSON.parse(sessionStorage["myStorageKey"]) } function saveData() { sessionStorage["myStorageKey"] = JSON.stringify(data); } window.addEventListener("load", loadData, true); window.addEventListener("unload", saveData, true); </script>
上面的脚本中包含了用于绑定页面加载事件和卸载事件的事件监听器。在这里,处理程序会分别调用loadData()和saveData()函数。
在loadData()函数中。首先查询sessionStorage,获取与键相对应的值,并将这个值传给JSON. Parse()函数。作为JSON. Parse()函数参数的值实际上是用于表示某个对象的字符串,函数会据此重新构建一个原始对象的副本。通过事件绑定,页面每次加载时都会调用loadData()函数。
与之类似,saveData ( )函数操作data全局变量,它调用JSON.stringify() ,以将保存在data变量中的对象副本转换为字符串形式,进而将该字符串保存回storage中。将saveData() 函数绑定到页面卸载事件后,我们就能确保用户每次关闭浏览器或窗口时,都会调用saveData()函数。
上述两个函数的实际结果是,任何保存在storage中的对象,不管它是否为复杂对象类型。我们都能做到在用户浏览时加载,在用户离开时保存。根据这种处理非文本数据的方法,开发人员可以进一步再扩展。
9.6.2 共享窗口
正如前面提到的,在任何同源浏览器窗口中触发HTML5 Web Storage事件的能力具有深远的意义。这意味着即使不是所有的窗口都使用了Storage对象,也可以基于storage在窗口间发送消息。这同样也意味着,我们可以在窗口间共享同源的数据。
让我们通过示例代码进行说明。要监听跨窗口消息,只需在脚本中绑定storage事件处理程序即可。假设在http://www.example.com/storageLog.html 网页中包含代码清单9-15所示的代码(这个示例文件storageLog.html也在code/storage文件夹中)。
代码清单9-15 使用storage进行跨窗口通信
// display records of new storage events function displayStorageEvent(e) { var incomingRow = document.createElement('div'); document.getElementById("container").appendChild(incomingRow); var logged = "key:" + e.key + ", newValue:" + e.newValue + ", oldValue:" + e.oldValue + ", url:" + e.url + ", storageArea:" + e.storageArea; incomingRow.innerHTML = logged; } // add listeners on storage events window.addEventListener("storage", displayStorageEvent, true);
为storage 事件类型绑定事件监听器后,窗口就能收到其他页面修改storage后发出的更新通知了。例如,浏览器窗口打开了页面http://www.example.con/browser-text.html,此时同源页面修改了Storage中的值,那么storageLog.html页面将会收到更新通知。
为了将消息发送到接收窗口,发送窗口只需修改storage对象,然后新旧数据值就会作为通知的一部分发送出去。举例来说,如果使用localStorage.setItem()更新了storage数据,同源的storagelog.html页面的disp1ayStorageEvent()函数将会收到一个事件。通过匹配事件名称和数据,两个页面就可以通信了,这在HTML5出现之前是非常困难的。图9-12显示了正在运行的storageLog.html页面。记录的是它接收到的storage事件。
9.7 小结
本章中,我们讲解了如何用HTML5 Web Storage代替浏览器cookie在跨窗口 、跨标签页甚至是(用localStorage) 跨浏览器重启的情况下,保存本地数据。随后,我们了解了如何使用sessionStorage将数据适当地隔离到各个窗口,以及如何基于storage事件实现跨窗口数据共享。 在本章的示例中,我们演示了当用户浏览网站时,使用storage在页面间跟踪数据的实用方法,这个方法很容易应用于其他数据类型。我们还演示了在页面加载或卸载时,如何存储非文本类型的数据,以保存和恢复跨页访问时网页的状态。
下一章,我们将介绍如何利用HTML5创建离线应用。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论