JavaScript笔记:离线应用与客户端存储

支持离线 Web 应用开发是 HTML5 的另一个重点。所谓离线 Web 应用,就是在设备不能上网的情况下仍然可以运行的应用。

HTML5 把离线应用作为重点,主要是基于开发人员的心愿。前端开发人员一直希望 Web 应用能够与传统的客户端应用同场竞技,起码做到只要设备有电
就能使用。

开发离线 Web 应用需要几个步骤。首先是确保应用知道设备是否能上网,以便下一步执行正确的操作。然后,应用还必须能访问一定的资源(图像、JavaScript、CSS 等),只有这样才能正常工作。最后,必须有一块本地空间用于保存数据,无论能否上网都不妨碍读写。HTML5 及其相关的 API 让开发离线应用成为现实。

离线检测

开发离线应用的第一步是要知道设备是在线还是离线,HTML5 为此定义了一个 navigator.onLine 属性,这个属性值为true表示设备能上网,值为false表示设备离线。这个属性的关键是浏览器必须知道设备能否访问网络,从而返回正确的值。实际应用中,navigator.onLine 在不同浏览器间还有些小的差异。

IE6+和 Safari 5+能够正确检测到网络已断开,并将 navigator.onLine 的值转换为 false。
Firefox 3+和 Opera 10.6+支持 navigator.onLine 属性,但你必须手工选中菜单项“文件 → Web开发人员(设置)→ 脱机工作”才能让浏览器正常工作。

除 navigator.onLine 属性之外,为了更好地确定网络是否可用,HTML5 还定义了两个事件:online 和 offline。当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件。这两个事件在 window 对象上触发。

支持离线检测的浏览器有 IE 6+(只支持 navigator.onLine 属性)、Firefox 3、Safari 4、Opera 10.6、 6 Chrome、iOS 3.2 版 Safari 和 Android 版WebKit。

应用缓存

HTML5 的应用缓存(application cache),或者简称为 appcache,是专门为开发离线 Web 应用而设计的。Appcache 就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存中保存数据,可以使用一个 描述文件(manifest file),列出要下载和缓存的资源。下面是一个简单的描述文件示例。

CACHE MANIFEST
#Comment
file.js
file.css

在最简单的情况下,描述文件中列出的都是需要下载的资源,以备离线时使用。

要将描述文件与页面关联起来,可以在html中的 manifest 属性中指定这个文件的路径,例如:

<html manifest="/offline.manifest">

以上代码告诉页面,/offline.manifest 中包含着描述文件。这个文件的 MIME 类型必须是text/cache-manifest。

虽然应用缓存的意图是确保离线时资源可用,但也有相应的 JavaScript API 让你知道它都在做什么。 这个 API 的核心是 applicationCache 对象(关于applicationCache,推荐博客:http://blog.csdn.net/fwwdn/article/details/8082433,和网站:https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Resources_in_an_application_cache,关于离线存储推荐博客:http://blog.csdn.net/fdipzone/article/details/12718945),这个对象有一个 status 属性,属性的值是常量,表示应用缓存的如下当前状态。

0:无缓存,即没有与页面相关的应用缓存。
1:闲置,即应用缓存未得到更新。
2:检查中,即正在下载描述文件并检查更新。
3:下载中,即应用缓存正在下载描述文件中指定的资源。
4:更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过 swapCache()来使用了。
5:废弃,即应用缓存的描述文件已经不存在了,因此页面无法再访问应用缓存。

应用缓存还有很多相关的事件,表示其状态的改变。以下是这些事件:

checking:在浏览器为应用缓存查找更新时触发。
error:在检查更新或下载资源期间发生错误时触发。
noupdate:在检查描述文件发现文件无变化时触发。
downloading:在开始下载应用缓存资源时触发。
progress:在文件下载应用缓存的过程中持续不断地触发。
updateready:在页面新的应用缓存下载完毕且可以通过 swapCache()使用时触发。
cached:在应用缓存完整可用时触发。

一般来讲,这些事件会随着页面加载按上述顺序依次触发。不过,通过调用 update()方法也可以手工干预,让应用缓存为检查更新而触发上述事件。

applicationCache.update();

update()一经调用,应用缓存就会去检查描述文件是否更新(触发 checking 事件),然后就像页面刚刚加载一样,继续执行后续操作。如果触发了 cached 事件,就说明应用缓存已经准备就绪,不会再发生其他操作了。如果触发了 updateready 事件,则说明新版本的应用缓存已经可用,而此时你需要调用 swapCache() 来启用新应用缓存。

EventUtil.addHandler(applicationCache, "updateready", function(){ 
    applicationCache.swapCache();
});

支持 HTML5 应用缓存的浏览器有 Firefox 3+、Safari 4+、Opera 10.6、Chrome、iOS 3.2+版 Safari 及 Android 版 WebKit。在 Firefox 4 及之前版本中调用 swapCache() 会抛出错误。

数据存储

随着 Web 应用程序的出现,也产生了对于能够直接在客户端上存储用户信息能力的要求。

这种想法很合乎逻辑,属于某个特定用户的信息应该存在该用户的机器上。无论是登录信息、偏好设定或其他数据, Web 应用提供者发现他们在找各种方式将数据存在客户端上。

这个问题的第一个方案是以 cookie 的形式出现的,cookie 是原来的网景公司创造的。一份题为“Persistent Client State: HTTP Cookes”(持久客户 端状态:HTTP Cookies)的标准中对 cookie 机制进行了阐述(该标准还可以在这里看到: http://curl.haxx.se/rfc/cookie_spec.html)。今天,cookie 只是在客户端存储数据的其中一种选项。

Cookie

HTTP Cookie,通常直接叫做 cookie,最初是在客户端用于存储会话信息的。该标准要求服务器对任意 HTTP 请求发送 Set-Cookie HTTP 头作为响应的一部分,其中包含会话信息。

例如,这种服务器响 应的头可能如下:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value

这个 HTTP 响应设置以 name 为名称、以 value 为值的一个 cookie,名称和值在传送时都必须是 URL 编码的。浏览器会存储这样的会话信息,并在这之后,通过为每个请求添加 Cookie HTTP 头将信息发送回服务器,如下所示:

GET /index.html HTTP/1.1
Cookie: name=value
Other-header: other-header-value

发送回服务器的额外信息可以用于唯一验证客户来自于发送的哪个请求。

限制:

cookie 在性质上是绑定在特定的域名下的。当设定了一个 cookie 后,再给创建它的域名发送请求时,都会包含这个 cookie。这个限制确保了储存在 cookie 中的信息只能让批准的接受者访问,而无法被其他域访问。

由于 cookie 是存在客户端计算机上的,还加入了一些限制确保 cookie 不会被恶意使用,同时不会占 据太多磁盘空间。每个域的 cookie 总数是有限的,不过浏览器之间各有不同。

当超过单个域名限制之后还要再设置 cookie,浏览器就会清除以前设置的 cookie。IE 和 Opera 会删除最近最少使用过的(LRU,Least Recently Used)cookie,腾出空间给新设置的 cookie。Firefox 看上去好像是随机决定要清除哪个 cookie。
所以考虑 cookie 限制非常重要,以免出现不可预期的后果。

浏览器中对于 cookie 的尺寸也有限制。大多数浏览器都有大约 4096B(加减 1)的长度限制。为了最佳的浏览器兼容性,最好将整个 cookie 长度限制在 4095B(含 4095)以内。尺寸限制影响到一个域下所有的 cookie,而并非每个 cookie 单独限制。

如果你尝试创建超过最大尺寸限制的 cookie,那么该 cookie 会被悄无声息地丢掉。
注意,虽然一个字符通常占用一字节,但是多字节情况则有不同。

cookie 的构成:

cookie 由浏览器保存的以下几块信息构成。

名称:一个唯一确定 cookie 的名称。cookie 名称是不区分大小写的,所以 myCookie 和 MyCookie 被认为是同一个 cookie。然而,实践中最好将 cookie 名称看作是区分大小写的,因为某些服务器会这样处理 cookie。cookie 的名称必须是经过 URL 编码的。

值:储存在 cookie 中的字符串值。值必须被 URL 编码。

域:cookie 对于哪个域是有效的。所有向该域发送的请求中都会包含这个 cookie 信息。这个值可以包含子域(subdomain,如 www.wrox.com),也可以不包含它(如.wrox.com,则对于 wrox.com的所有子域都有效)。如果没有明确设定,那么这个域会被认作来自设置 cookie 的那个域。

路径对于指定域中的那个路径,应该向服务器发送 cookie。例如,你可以指定 cookie 只有从 http://www.wrox.com/books/ 中才能访问,那么 http://www.wrox.com 的页面就不会发送 cookie 信息,即使请求都是来自同一个域的。

失效时间:表示 cookie 何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。默认情况下,浏览器会话结束时即将所有 cookie 删除;不过也可以自己设置删除时间。 这个值是个 GMT 格式的日期(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定应该删除 cookie 的准确时间。因此,cookie 可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则 cookie 会被立刻删除。

安全标志:指定后,cookie 只有在使用 SSL 连接的时候才发送到服务器。例如,cookie 信息只能发送给 https://www.wrox.com,而 http://www.wrox.com 的请求则不能发送 cookie。

每一段信息都作为 Set-Cookie 头的一部分,使用分号加空格分隔每一段,如下例所示:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com 
Other-header: other-header-value

该头信息指定了一个叫做 name 的 cookie,它会在格林威治时间 2007 年 1 月 22 日 7:10:24 失效,同时对于 www.wrox.com 和 wrox.com 的任何子域(如 p2p.wrox.com)都有效。

secure 标志是 cookie 中唯一一个非名值对儿的部分,直接包含一个 secure 单词。如下:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure 
Other-header: other-header-value

这里,创建了一个对于所有 wrox.com 的子域和域名下(由 path 参数指定的)所有页面都有效的 cookie。因为设置了 secure 标志,这个 cookie 只能通过 SSL 连接才能传输。

尤其要注意,域、路径、失效时间和 secure 标志都是服务器给浏览器的指示,以指定何时应该发送 cookie。这些参数并不会作为发送到服务器的 cookie 信息的一部分,只有名值对儿才会被发送。

JavaScript 中的 cookie:

在 JavaScript 中处理 cookie 有些复杂,因为其众所周知的蹩脚的接口,即 BOM 的 document. cookie 属性。

这个属性的独特之处在于,它会因为使用它的方式不同而表现出不同的行为。当用来获取属性值时, document.cookie 返回当前页面可用的(根据 cookie 的域、路径、失效时间和安全设置)所有 cookie 的字符串,一系列由分号隔开的名值对儿,如下例所示。

name1=value1;name2=value2;name3=value3
//所有名字和值都是经过 URL 编码的,所以必须使用 decodeURIComponent()来解码。

当用于设置值的时候,document.cookie 属性可以设置为一个新的 cookie 字符串。这个 cookie 字符串会被解释并添加到现有的 cookie 集合中。设置 document.cookie 并不会覆盖 cookie,除非设置的 cookie 的名称已经存在。设置 cookie 的格式如下,和 Set-Cookie 头中使用的格式一样。

name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure

这些参数中,只有 cookie 的名字和值是必需的。

document.cookie = "name=Nicholas";

这段代码创建了一个叫 name 的 cookie,值为 Nicholas。当客户端每次向服务器端发送请求的时 候,都会发送这个 cookie;当浏览器关闭的时候,它就会被删除。虽然这段代码没问题,但因为这里正好名称和值都无需编码,所以最好每次设置 cookie 时都像下面这个例子中一样使用 encodeURIComponent()。

document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nicholas");

由于 JavaScript 中读写 cookie 不是非常直观,常常需要写一些函数来简化 cookie 的功能。基本的 cookie 操作有三种:读取、写入和删除。

注:没有删除已有 cookie 的直接方法。所以,需要使用相同的路径、域和安全选项再次设置 cookie,并将失效时间设置为过去的时间。

子 cookie

为了绕开浏览器的单域名下的 cookie 数限制,一些开发人员使用了一种称为子 cookie(subcookie)的概念。子 cookie 是存放在单个 cookie 中的更小段的数据。也就是使用 cookie 值来存储多个名称值对儿。子 cookie 最常见的的格式如下所示:

name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5

子 cookie 一般也以查询字符串的格式进行格式化。然后这些值可以使用单个 cookie 进行存储和访问,而非对每个名称值对儿使用不同的 cookie 存储。最后网站或者 Web 应用程序可以无需达到单域名 cookie 上限也可以存储更加结构化的数据。

为了更好地操作子 cookie,必须建立一系列新方法。子 cookie 的解析和序列化会因子 cookie 的期望用途而略有不同并更加复杂些。例如,要获得一个子 cookie,首先要遵循与获得 cookie 一样的基本步骤, 但是在解码 cookie 值之前,需要按如下方法找出子 cookie 的信息。

var SubCookieUtil = {
    get: function (name, subName){
        var subCookies = this.getAll(name);
        if (subCookies){
              return subCookies[subName];
        } else {
            return null;
        }
    },
    getAll: function(name){
        var cookieName = encodeURIComponent(name) + "=",
            cookieStart = document.cookie.indexOf(cookieName),
            cookieValue = null,
            cookieEnd,
            subCookies, 
            i,
            parts,
            result = {};
        if (cookieStart > -1){
            cookieEnd = document.cookie.indexOf(";", cookieStart);
            if (cookieEnd == -1){
                cookieEnd = document.cookie.length;
            }
            cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd);
            if (cookieValue.length > 0){
                subCookies = cookieValue.split("&");
                for (i=0, len=subCookies.length; i < len; i++){
                    parts = subCookies[i].split("=");
                    result[decodeURIComponent(parts[0])] =
                         decodeURIComponent(parts[1]);
                }
                return result;
            }
        }
        return null;
    },

    set: function (name, subName, value, expires, path, domain, secure) {
        var subcookies = this.getAll(name) || {};
        subcookies[subName] = value;
        this.setAll(name, subcookies, expires, path, domain, secure);
    },
    setAll: function(name, subcookies, expires, path, domain, secure){
        var cookieText = encodeURIComponent(name) + "=",
            subcookieParts = new Array(),
            subName;
        for (subName in subcookies){
            if (subName.length > 0 && subcookies.hasOwnProperty(subName)){
                subcookieParts.push(encodeURIComponent(subName) + "=" +
                    encodeURIComponent(subcookies[subName]));
             } 
        }
        if (cookieParts.length > 0){
            cookieText += subcookieParts.join("&");
            if (expires instanceof Date) {
                cookieText += "; expires=" + expires.toGMTString();
            }
            if (path) {
                cookieText += "; path=" + path;
            }
            if (domain) {
               cookieText += "; domain=" + domain;
            }
            if (secure) {
               cookieText += "; secure";
            }
        } else {
           cookieText += "; expires=" + (new Date(0)).toGMTString();
        }
        document.cookie = cookieText;
    },

    unset: function (name, subName, path, domain, secure){
        var subcookies = this.getAll(name);
        if (subcookies){
            delete subcookies[subName];
            this.setAll(name, subcookies, null, path, domain, secure);
        }
    },
    unsetAll: function(name, path, domain, secure){
        this.setAll(name, null, new Date(0), path, domain, secure);
    }
    //这里省略了更多代码
};

获取子 cookie 的方法有两个:get()和 getAll()。其中 get()获取单个子 cookie 的值,getAll() 获取所有子 cookie 并将它们放入一个对象中返回,对象的属性为子 cookie 的名称,对应值为子 cookie 对应的值。get()方法接收两个参数:cookie 的名字和子 cookie 的名字。它其实就是调用 getAll()获 取所有的子 cookie,然后只返回所需的那一个(如果 cookie 不存在则返回 null)。

可以像下面这样使用上述方法:

//假设 document.cookie=data=name=Nicholas&book=Professional%20JavaScript
//取得全部子 cookie
var data = SubCookieUtil.getAll("data"); 
alert(data.name); //"Nicholas" 
alert(data.book); //"Professional JavaScript"
//逐个获取子 cookie
alert(SubCookieUtil.get("data", "name")); //"Nicholas" 
alert(SubCookieUtil.get("data", "book")); //"Professional JavaScript"

//仅删除名为 name 的子 cookie 
SubCookieUtil.unset("data", "name");
//删除整个 cookie 
SubCookieUtil.unsetAll("data");

IE用户数据

在 IE5.0 中,微软通过一个自定义行为引入了持久化用户数据的概念。用户数据允许每个文档最多 128KB 数据,每个域名最多 1MB 数据。要使用持久化用户数据,首先必须如下所示,使用 CSS 在某个元素上指定 userData 行为:

<div style="behavior:url(#default#userData)" id="dataStore"></div>

一旦该元素使用了 userData 行为,那么就可以使用 setAttribute() 方法在上面保存数据了。 为了将数据提交到浏览器缓存中,还必须调用 save()方法并告诉它要保存到的数据空间的名字。数据空间名字可以完全任意,仅用于区分不同的数据集:

var dataStore = document.getElementById("dataStore");
dataStore.setAttribute("name", "Nicholas");
dataStore.setAttribute("book", "Professional JavaScript");
dataStore.save("BookInfo");

在这段代码中,div元素上存入了两部分信息。在用 setAttribute() 存储了数据之后,调用了 save() 方法,指定了数据空间的名称为 BookInfo。下一次页面载入之后,可以使用 load() 方法指定同样的数据空间名称来获取数据:

dataStore.load("BookInfo");
alert(dataStore.getAttribute("name")); //"Nicholas"
alert(dataStore.getAttribute("book")); //"Professional JavaScript"

如果 getAttribute()调用了不存在的名称或者是尚未载入的名称,则返回 null。

你可以通过 removeAttribute() 方法明确指定要删除某元素数据,只要指定属性名称。删除之后, 必须像下面这样再次调用 save() 来提交更改。

dataStore.removeAttribute("name");
dataStore.removeAttribute("book");
dataStore.save("BookInfo");

对 IE 用户数据的访问限制和对 cookie 的限制类似。
要访问某个数据空间,脚本运行的页面必须来自同一个域名,在同一个路径下,并使用与进行存储的脚本同样的协议。和 cookie 不同的是,你无法将用户数据访问限制扩展到更多的客户。还有一点不同,用户数据默认是可以跨越会话持久存在的,同时也不会过期;数据需要通过 removeAttribute()方法专门进行删除以释放空间。

Web存储机制

Web Storage 最早是在 Web 超文本应用技术工作组(WHAT-WG)的 Web 应用 1.0 规范中描述的。这个规范的最初的工作最终成为了 HTML5 的一部分。

Web Storage 的目的是克服由 cookie 带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器。

Web Storage 的两个主要目标是:

提供一种在 cookie 之外存储会话数据的途径;
提供一种存储大量可以跨会话存在的数据的机制。

最初的 Web Storage 规范包含了两种对象的定义:sessionStorage 和 globalStorage。这两个对象在支持的浏览器中都是以 windows 对象属性的形式存在的,支持这两个属性的浏览器包括 IE8+、 Firefox 3.5+、Chrome 4+和 Opera 10.5+。

Storage 类型:

Storage 类型提供最大的存储空间(因浏览器而异)来存储名值对儿。Storage 的实例与其他对象类似,有如下方法。

clear():删除所有值;Firefox 中没有实现 。
getItem(name):根据指定的名字 name 获取对应的值。
key(index):获得 index 位置处的值的名字。
removeItem(name):删除由 name 指定的名值对儿。
setItem(name, value):为指定的 name 设置一个对应的值。

sessionStorage 对象:

sessionStorage 对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。存储在 sessionStorage 中的数据可以跨越页面刷新而存在,同时如果浏览器支持,浏览器崩溃并重启之后依然可用(Firefox 和 WebKit 都支持,IE 则不行)。

因为 seesionStorage 对象绑定于某个服务器会话,所以当文件在本地运行的时候是不可用的。存储在 sessionStorage 中的数据只能由最初给对象存储数据的页面访问到,所以对多页面应用有限制。

由于 sessionStorage 对象其实是 Storage 的一个实例,所以可以使用 setItem() 或者直接设置新的属性来存储数据。下面是这两种方法的例子:

//使用方法存储数据 
sessionStorage.setItem("name", "Nicholas");
//使用属性存储数据
sessionStorage.book = "Professional JavaScript";

sessionStorage 中有数据时,可以使用 getItem()或者通过直接访问属性名来获取数据。


//使用方法读取数据
var name = sessionStorage.getItem("name");
//使用属性读取数据
var book = sessionStorage.book;

要从 sessionStorage 中删除数据,可以使用 delete 操作符删除对象属性,也可调用 removeItem()方法。

//使用 delete 删除一个值——在 WebKit 中无效 
delete sessionStorage.name;
//使用方法删除一个值 
sessionStorage.removeItem("book");

sessionStorage 对象应该主要用于仅针对会话的小段数据的存储。如果需要跨越会话存储数据, 那么 globalStorage 或者 localStorage 更为合适。

globalStorage 对象:

作为最初的 Web Storage 规范的一部分,这个对象的目的是跨越会话存储数据,但有特定的访问限制。

要使用 globalStorage,首先要指定哪些域可以访问该数据。可以通过方括号标记使用属性来实现:

//保存数据
globalStorage["wrox.com"].name = "Nicholas";
//获取数据
var name = globalStorage["wrox.com"].name;

这个存储空间对于 wrox.com 及其所有子域都是可以访问的。可以像下面这样指定子域名:

//保存数据
globalStorage["www.wrox.com"].name = "Nicholas";
//获取数据
var name = globalStorage["www.wrox.com"].name;

某些浏览器允许更加宽泛的访问限制,比如只根据顶级域名进行限制或者允许全局访问。

虽然这样也支持,但是还是要避免使用这种可宽泛访问的数据存储,以防止出现潜在的安全问题。 考虑到安全问题,这些功能在未来可能会被删除或者是被更严格地限制,所以不应依赖于这类功能。当使用 globalStorage 的时候一定要指定一个域名。

如果你事先不能确定域名,那么使用 location.host 作为属性名比较安全。例如:

globalStorage[location.host].name = "Nicholas";
var book = globalStorage[location.host].getItem("book");

localStorage 对象:

localStorage 对象在修订过的 HTML 5 规范中作为持久保存客户端数据的方案取代了globalStorage。

与 globalStorage 不同,不能给 localStorage 指定任何访问规则;规则事先就设定好了。要访问同一个 localStorage 对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。这相当于 globalStorage[location.host]。

由于 localStorage 是 Storage 的实例,所以可以像使用 sessionStorage 一样来使用它。

//使用方法存储数据 
localStorage.setItem("name", "Nicholas");
//使用属性存储数据
localStorage.book = "Professional JavaScript";
//使用方法读取数据
var name = localStorage.getItem("name");
//使用属性读取数据
var book = localStorage.book;

存储在 localStorage 中的数据和存储在 globalStorage 中的数据一样,都遵循相同的规则:数据保留到通过 JavaScript 删除或者是用户清除浏览器缓存。

为了兼容只支持 globalStorage 的浏览器,可以使用以下函数。

function getLocalStorage(){
    if (typeof localStorage == "object"){
        return localStorage;
    } else if (typeof globalStorage == "object"){
        return globalStorage[location.host];
    } else {
        throw new Error("Local storage not available.");
    }
}

storage 事件:

对 Storage 对象进行任何修改,都会在文档上触发 storage 事件。当通过属性或 setItem()方 法保存数据,使用 delete 操作符或 removeItem()删除数据,或者调用 clear()方法时,都会发生该事件。这个事件的 event 对象有以下属性:

domain:发生变化的存储空间的域名。
key:设置或者删除的键名。
newValue:如果是设置值,则是新值;如果是删除键,则是 null。
oldValue:键被更改之前的值。

无论对 sessionStorage、globalStorage 还是 localStorage 进行操作,都会触发 storage 事件,但不作区分。

限制:

与其他客户端数据存储方案类似,Web Storage 同样也有限制。这些限制因浏览器而异。一般来说, 对存储空间大小的限制都是以每个来源(协议、域和端口)为单位的。换句话说,每个来源都有固定大小的空间用于保存自己的数据。考虑到这个限制,就要注意分析和控制每个来源中有多少页面需要保存数据。

有关 Web Storage 的限制,请参考 http://dev-test.nemikor.com/web-storage/support-test/

IndexedDB

Indexed Database API,或者简称为 IndexedDB,是在浏览器中保存结构化数据的一种数据库。 IndexedDB 是为了替代目前已被废弃的 Web SQL Database API(因为已废弃,所以本书未介绍)而出现 的。IndexedDB 的思想是创建一套 API,方便保存和读取 JavaScript 对象,同时还支持查询及搜索。

IndexedDB 设计的操作完全是异步进行的。因此,大多数操作会以请求方式进行,但这些操作会在后期执行,然后如果成功则返回结果,如果失败则返回错误。差不多每一次 IndexedDB 操作,都需要你注册 onerror 或 onsuccess 事件处理程序,以确保适当地处理结果。

在得到完整支持的情况下,IndexedDB 将是一个作为 API 宿主的全局对象。由于 API 仍然可能有变化,浏览器也都使用提供商前缀,因此这个对象在 IE10 中叫 msIndexedDB,在 Firefox 4 中叫 mozIndexedDB,在 Chrome 中叫 webkitIndexedDB。为了清楚起见,本节示例中将使用 IndexedDB, 而实际上每个示例前面都应该加上下面这行代码:

var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;

数据库:

IndexedDB 就是一个数据库,与 MySQL 或 Web SQL Database 等这些你以前可能用过的数据库类似。 IndexedDB 最大的特色是使用对象保存数据,而不是使用表来保存数据。一个 IndexedDB 数据库,就是 一组位于相同命名空间下的对象的集合。

使用 IndexedDB 的第一步是打开它,即把要打开的数据库名传给 indexDB.open()。如果传入的数据库已经存在,就会发送一个打开它的请求;如果传入的数据库还不存在,就会发送一个创建并打开它的请求。总之,调用 indexDB.open()会返回一个 IDBRequest 对象,在这个对象上可以添加 onerror 和 onsuccess 事件处理程序。先来看一个例子:

var request, database;
request = indexedDB.open("admin");
request.onerror = function(event){
    alert("Something bad happened while trying to open: " + event.target.errorCode);
};
request.onsuccess = function(event){
    database = event.target.result;
};

在这两个事件处理程序中,event.target 都指向 request 对象,因此它们可以互换使用。如果响应的是 onsuccess 事件处理程序,那么 event.target.result 中将有一个数据库实例对象(IDBData-base),这个对象会保存在 database 变量中。如果发生了错误,那 event.target.errorCode 中将保存一个错误码,表示问题的性质。

默认情况下,IndexedDB 数据库是没有版本号的,最好一开始就为数据库指定一个版本号。为此, 可以调用 setVersion()方法,传入以字符串形式表示的版本号。同样,调用这个方法也会返回一个请求对象,需要你再指定事件处理程序。

if (database.version != "1.0"){
    request = database.setVersion("1.0");
    request.onerror = function(event){
        alert("Something bad happened while trying to set version: " +
              event.target.errorCode);
    };
    request.onsuccess = function(event){
        alert("Database initialization complete. Database name: " + database.name + ", Version: " + database.version);
    };
} else {
    alert("Database already initialized. Database name: " + database.name + ", Version: " + database.version);
}

对象存储空间:

在建立了与数据库的连接之后,下一步就是使用对象存储空间。如果数据库的版本与你传入的版本不匹配,那可能就需要创建一个新的对象存储空间。在创建对象存储空间之前,必须要想清楚你想要保存什么数据类型。

假设你要保存的用户记录由用户名、密码等组成,那么保存一条记录的对象应该类似如下所示:

var user = {
    username: "007",
    firstName: "James",
    lastName: "Bond",
    password: "foo"
};

有了这个对象,很容易想到username属性可以作为这个对象存储空间的键。这个username必须全局唯一,而且大多数时候都要通过这个键来访问数据。这一点非常重要,因为在创建对象存储空间时, 必须指定这么一个键。

var store = db.createObjectStore("users", { keyPath: "username" });

其中第二个参数中的 keyPath 属性,就是空间中将要保存的对象的一个属性,而这个属性将作为存储空间的键来使用。

好,现在有了一个对存储空间的引用。接下来可以使用 add() 或 put() 方法来向其中添加数据。这两个方法都接收一个参数,即要保存的对象,然后这个对象就会被保存到存储空间中。这两个方法的区别在空间中已经包含键值相同的对象时会体现出来。在这种情况下,add() 会返回错误,而 put() 则会重写原有对象。简单地说,可以把 add() 想象成插入新值,把 put() 想象成更新原有的值。在初始化对象存储空间时,可以使用类似下面这样的代码:

//users 中保存着一批用户对象 
var i=0,
len = users.length;
while(i < len){
    store.add(users[i++]);
}

每次调用 add() 或 put() 都会创建一个新的针对这个对象存储空间的更新请求。如果想验证请求是否成功完成,可以把返回的请求对象保存在一个变量中,然后再指定 onerror 或 onsuccess 事件处理程序。

//users 中保存着一批用户对象 
var i=0,
    request,
    requests = [],
    len = users.length;
while(i < len){
    request = store.add(users[i++]);
    request.onerror = function(){
        //处理错误 
    };
    request.onsuccess = function(){ 
        //处理成功
    };
    requests.push(request);
}

创建了对象存储空间并向其中添加了数据之后,就该查询数据了。

事务

跨过创建对象存储空间这一步之后,接下来的所有操作都是通过事务来完成的。在数据库对象上调用 transaction() 方法可以创建事务。任何时候,只要想读取或修改数据,都要通过事务来组织所有操作。在最简单的情况下,可以像下面这样创建事务:

var transaction = db.transaction();

如果没有参数,就只能通过事务来读取数据库中保存的对象。最常见的方式是传入要访问的一或多个对象存储空间。

var transaction = db.transaction("users");

这样就能保证只加载 users 存储空间中的数据,以便通过事务进行访问。如果要访问多个对象存储空间,也可以在第一个参数的位置上传入字符串数组。

var transaction = db.transaction(["users", "anotherStore"]);

如前所述,这些事务都是以只读方式访问数据。要修改访问方式,必须在创建事务时传入第二个参 数,这个参数表示访问模式,用 IDBTransaction 接口定义的如下常量表示:READ ONLY(0)表示只读,READ WRITE(1)表示读写,VERSION CHANGE(2)表示改变。

IE10+和 Firefox 4+实现的是 IDBTransaction,但在 Chrome 中则叫 webkitIDBTransaction,所以使用下面的代码可以统一接口:

var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

有了这行代码,就可以更方便地为 transaction()指定第二个参数了。

var transaction = db.transaction("users", IDBTransaction.READ_WRITE);

这个事务能够读写 users 存储空间。

取得了事务的索引后,使用 objectStore() 方法并传入存储空间的名称,就可以访问特定的存储 空间。然后,可以像以前一样使用 add() 和 put() 方法,使用 get()可以取得值,使用 delete() 可以删除对象,而使用 clear() 则可以删除所有对象。get() 和 delete() 方法都接收一个对象键作为参数, 而所有这 5 个方法都会返回一个新的请求对象。例如:

var request = db.transaction("users").objectStore("users").get("007"); request.onerror = function(event){
    alert("Did not get the object!");
};
request.onsuccess = function(event){
    var result = event.target.result;
    alert(result.firstName);    //"James"
};

因为一个事务可以完成任何多个请求,所以事务对象本身也有事件处理程序:onerror 和 oncomplete。这两个事件可以提供事务级的状态信息。

transaction.onerror = function(event){ 
    //整个事务都被取消了
};
transaction.oncomplete = function(event){ 
    //整个事务都成功完成了
};

注意:通过 oncomplete 事件的事件对象(event)访问不到 get() 请求返回的任何数据。必须在相应请求的 onsuccess 事件处理程序中才能访问到数据。

使用游标查询:

使用事务可以直接通过已知的键检索单个对象。而在需要检索多个对象的情况下,则需要在事务内部创建游标。游标就是一指向结果集的指针。与传统数据库查询不同,游标并不提前收集结果。游标指针会先指向结果中的第一项,在接到查找下一项的指令时,才会指向下一项。

在对象存储空间上调用 openCursor()方法可以创建游标。与 IndexedDB 中的其他操作一样, openCursor() 方法返回的是一个请求对象,因此必须为该对象指定 onsuccess 和 onerror 事件处理程序。例如:

var store = db.transaction("users").objectStore("users"),
    request = store.openCursor();
request.onsuccess = function(event){ 
    //处理成功
};
request.onerror = function(event){ 
    //处理失败
};

在 onsuccess 事件处理程序执行时,可以通过 event.target.result 取得存储空间中的下一个对象。在结果集中有下一项时,这个属性中保存一个 IDBCursor 的实例,在没有下一项时,这个属性的值为 null。

IDBCursor 的实例有以下几个属性:

direction:数值,表示游标移动的方向。默认值为 IDBCursor.NEXT(0),表示下一项。 IDBCursor.NEXT_NO_DUPLICATE(1)表示下一个不重复的项,DBCursor.PREV(2)表示前一项,而 IDBCursor.PREV_NO_DUPLICATE 表示前一个不重复的项。

key:对象的键。

value:实际的对象。

primaryKey:游标使用的键。可能是对象键,也可能是索引键(稍后讨论索引键)。

要检索某一个结果的信息,可以像下面这样:

request.onsuccess = function(event){ 
    var cursor = event.target.result; 
    if (cursor){ //必须要检查
        console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
    } 
};

请记住,这个例子中的 cursor.value 是一个对象,这也是为什么在显示它之前先将它转换成 JSON 字符串的原因。

使用游标可以更新个别的记录。调用 update() 方法可以用指定的对象更新当前游标的 value。与其他操作一样,调用 update()方法也会创建一个新请求,因此如果你想知道结果,就要为它指定 onsuccess 和 onerror 事件处理程序。

如果调用 delete() 方法,就会删除相应的记录。与 update() 一样,调用 delete() 也返回一个请求。

request.onsuccess = function(event){
    var cursor = event.target.result,
        value,
        deleteRequest;
    if (cursor){ //必须要检查
        if (cursor.key == "foo"){
            deleteRequest = cursor.delete();
            deleteRequest.onsuccess = function(){
                //处理成功 
            };
            deleteRequest.onerror = function(){ 
                //处理失败
            }; 
        }
    } 
};

如果当前事务没有修改对象存储空间的权限,update() 和 delete() 会抛出错误。

默认情况下,每个游标只发起一次请求。要想发起另一次请求,必须调用下面的一个方法。

continue(key):移动到结果集中的下一项。参数 key 是可选的,不指定这个参数,游标移动到下一项;指定这个参数,游标会移动到指定键的位置。
advance(count):向前移动 count 指定的项数。

这两个方法都会导致游标使用相同的请求,因此相同的 onsuccess 和 onerror 事件处理程序也会得到重用。

下面的例子遍历了对象存储空间中的所有项:

request.onsuccess = function(event){
    var cursor = event.target.result;
    if (cursor){ 
        //必须要检查 12
        console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
        cursor.continue(); //移动到下一项 
    } else {
        console.log("Done!");
    }
};

调用 continue()会触发另一次请求,进而再次调用 onsuccess 事件处理程序。在没有更多项可以迭代时,将最后一次调用 onsuccess 事件处理程序,此时 event.target.result 的值为 null。

键范围:

使用游标总让人觉得不那么理想,因为通过游标查找数据的方式太有限了。键范围(key range)为使用游标增添了一些灵活性。

键范围由 IDBKeyRange 的实例表示。

支持标准 IDBKeyRange 类型的浏览器有 IE10+和 Firefox 4+,Chrome 中的名字叫 webkitIDBKeyRange。与使用 IndexedDB 中的其他类 型一样,你最好先声明一个本地的类型,同时要考虑到不同浏览器中的差异。

var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

有四种定义键范围的方式。第一种是使用 only() 方法,传入你想要取得的对象的键。

var onlyRange = IDBKeyRange.only("007");

这个范围可以保证只取得键为”007”的对象。使用这个范围创建的游标与直接访问存储空间并调用 get(“007”)差不多。

第二种定义键范围的方式是指定结果集的下界。下界表示游标开始的位置。例如,以下键范围可以 保证游标从键为”007”的对象开始,然后继续向前移动,直至最后一个对象。

//从键为"007"的对象开始,然后可以移动到最后
var lowerRange = IDBKeyRange.lowerBound("007");

如果你想忽略键为”007”的对象,从它的下一个对象开始,那么可以传入第二个参数 true:

//从键为"007"的对象的下一个对象开始,然后可以移动到最后
var lowerRange = IDBKeyRange.lowerBound("007", true);

第三种定义键范围的方式是指定结果集的上界,也就是指定游标不能超越哪个键。指定上界使用 upperRange()方法。下面这个键范围可以保证游标从头开始,到取得键为”ace”的对象终止。

//从头开始,到键为"ace"的对象为止
var upperRange = IDBKeyRange.upperBound("ace");

如果你不想包含键为指定值的对象,同样,传入第二个参数 true:

//从头开始,到键为"ace"的对象的上一个对象为止
var upperRange = IDBKeyRange.upperBound("ace", true);

无论如何,在定义键范围之后,把它传给 openCursor()方法,就能得到一个符合相应约束条件的游标。

var store = db.transaction("users").objectStore("users"),
        range = IDBKeyRange.bound("007", "ace");
        request = store.openCursor(range);
request.onsuccess = function(event){ 
    var cursor = event.target.result;
    if (cursor){ //必须要检查
        console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
        cursor.continue(); //移动到下一项 
    } else {
        console.log("Done!");
    }
};

这个例子输出的对象的键为”007”到”ace”。

设定游标方向:

实际上,openCursor()可以接收两个参数。第一个参数就是刚刚看到的 IDBKeyRange 的实例,第二个是表示方向的数值常量。

索引:

对于某些数据,可能需要为一个对象存储空间指定多个键。比如,若要通过用户 ID 和用户名两种方式来保存用户资料,就需要通过这两个键来存取记录。为此,可以考虑将用户 ID 作为主键,然后为用户名创建索引。

要创建索引,首先引用对象存储空间,然后调用 createIndex()方法,如下所示。

var store = db.transaction("users").objectStore("users"),
    index = store.createIndex("username", "username", { unique: false});

createIndex()的第一个参数是索引的名字,第二个参数是索引的属性的名字,第三个参数是一 个包含 unique 属性的选项(options)对象。
这个选项通常都必须指定,因为它表示键在所有记录中是否唯一。因为 username 有可能重复,所以这个索引不是唯一的。

createIndex() 的返回值是 IDBIndex 的实例。在对象存储空间上调用 index()方法也能返回同一个实例。
例如,要使用一个已经存在的名为”username”的索引,可以像下面这样取得该索引。

var store = db.transaction("users").objectStore("users"),
    index = store.index("username");
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值