本章内容:进行离线检测、使用离线缓存、在浏览器中保存数据
‘
支持离线 Web 应用开发时 HTML5 的另一个重点。HTML5 把离线应用作为重点,主要是基于开发人员的心愿。
开发离线Web应用程序。
- 首先要确保应用知道设备是否能上网,以便下一步执行正确的操作。
- 然后,应用还必须能访问一定的资源(图像、JavaScript、CSS 等),只有这样才能正常工作。
- 最后,必须有一块本地空间用于保存数据,无论能否上网都不妨碍读写。
一、离线检测
开发离线应用的第一步是要知道设备是在线还是离线,HTML5 为此定义了一个 navigator.onLine 属性,这个属性值为 true 表示设备能上网,值为 false 表示设备离线。
navigator.onLine 在不同浏览器间还有些小差距
- IE6+、Safari5+、能够正确检测到网络已断开,并将 navigator.onLine 的值转换为 false
- Firefox 3+ 和 Opera 10.6+支持 navigator.onLine 属性,但你必须手工选中菜单项“文件-> Web开发人员(设置) -> 脱机工作” 才能让浏览器正常工作
以下是检测该属性状态的示例:
if (navigator.onLine) { // 正常工作
// todo
} else { // 离线状态
//todo
}
为了更好地确定网络是否可用,HTML5 还定义了两个事件:online 和 offline。当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件。这两个事件在 window 对象上触发。
window.ononline = function() {
// todo
}
window.onoffline = function() {
// todo
}
在页面加载后,最好先通过 navigator.onLine 取得初始的状态。然后,就是通过上述两个事件来确定网络连接状态是否变化。当上述事件触发时,navigator.onLine 属性的值也会改变,不过必须手工轮询这个属性才能检测到网络状态的变化。
二、应用缓存
HTML5 的应用缓存(application cache),或者简称为 appcache,是专门为开发离线 Web 应用而设计的。Appcache 就是从浏览器的缓存中分出来的一块缓存区。想要在这个缓存中保存数据,可以使用一个 描述文件(manifest file),列出要下载和缓存的资源。
下面是一个简单的描述文件示例。
CACHE MANIFEST
# Comment
file.js
file.css
在最简单的情况下,描述文件中列出的都是需要下载的资源,以备离线时使用
要将描述文件与页面关联起来,可以在 中的 manifest 属性中指定这个文件的路径,例如:
这个文件的 MIME 类型必须是 text/cache-manifest
同时有相应的 JavaScript API 让你知道它都在做什么。这个 API 的核心是 applicationCache 对象,这个对象有一个 status 属性
属性的值是常量,表示应用缓存的如下当前状态:
- 0:无缓存,即没有与页面相关的应用缓存。
- 1:闲置,即应用在下载描述文件并检查更新。
- 2:检查中,即正在下载描述文件并检查更新。
- 3:下载中,即应用缓存正在下载描述文件中指定的资源。
- 4:更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过 swapCache() 来使用了。
- 5:废弃,即应用缓存的描述文件已经不存在了,因此页面无法在访问应用缓存。
应用缓存还有很多相关的事件,表示其状态的改变。以下是这些事件。
- checking:在浏览器为应用缓存查找更新时触发。
- error:在检测更新或下载资源期间发生错误时触发
- noupdate:在检查描述文件发现文件无变化时触发
- downloading:在开始下载应用缓存资源时触发
- progress:在文件下载应用缓存的过程中持续不断地触发
- updateready:在页面新的应用缓存下载完毕且可以通过 swapCache() 使用时触发。
- cached:在应用缓存完整可用时触发
一般来讲,这些事件会随着页面加载按上述顺序依次触发。不过,通常调用 update() 方法也可以手工干预,让应用缓存为检查更新而触发上述事件。
applicationCache.update()
update() 一经调用,应用缓存就会去检查描述文件是否更新(触发 checking 事件),然后就像页面刚加载一样,继续执行后续操作。如果触发了 cached 事件,就说明应用缓存已经准备就绪,不会再发生其他操作了。如果触发了 updateready 事件,则说明新版本的应用缓存已经可用,而此时你需要调用 swapCache() 来启用新应用缓存
applicationCache.onupdateready = function() {
applicationCache.swapCache()
}
三、数据存储
随着 Web 应用程序的出现,也产生了对于能够直接在客户端上存储用户信息能力的要求。这个问题的第一个方案是以 cookie 的形式出现的,cookie是原来的网景公司创造的。一份题为“Persistent Client State:HTTP Cookies”(持久客户端状态:HTTP Cookies)的标准中对 cookie 机制进行了阐述。今天,cookie 只是在客户端存储数据的其中一种选项。
3.1、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
发送回服务器的额外信息可以用于唯一验证客户来自于发送的哪个请求。
3.1.1、限制
cookie 在性质上时绑定在特定的域名下的,这个限制确保了储存在 cookie 中的信息只能让批准的接受者访问,而无法被其他域访问。
每个域的 cookie 总数有限,不过浏览器之间各有不同:
- IE6以及更低版本限制每个域名 最多 20个 cookie
- IE7 和之后版本每个域名最多 50个。IE7最初是支持每个域名 最大 20个 cookie,之后被微软一个补丁更新了
- Firefox 限制每个域最多 50个cookie
- Opera 限制每个域最多 30个 cookie
- Safari 和 Chrome 对于每个域的 cookie 数量限制没有硬性规定。
当超过单个域名限制之后还要再设置 cookie,浏览器就会清除以前设置的 cookie。IE 和 Opera 则会删除最近最少使用的 cookie。Firefox 看上去好像是 随机决定要清除哪个 cookie,所以考虑 cookie限制非常重要,以免出现不可预期的后果。
浏览器中对于 cookie 的尺寸也有限制。大多数浏览器都有 大约 4096B(加减一)的长度限制。尺寸限制影响到一个域下面所有的 cookie,如果你尝试创建超过最大尺寸限制的cookie,那么该 cookie 会被悄无声息地丢掉。
3.1.2、cookie 的构成
cookie 有浏览器保存的以下几块信息构成
- 名称:一个唯一确定的cookie名称。cookie 名称是不区分大小写的,然而,实践中最好将 cookie 名称看作是区分大小写的,因为某些服务器会这样处理cookie。cookie的名称必须经过 URL 编码。
- 值:储存在 cookie 中的字符串值。值必须被 URL 编码
- 域:cookie 对于哪个域是有效的。所有项该域发送的请求中都会包含这个 cookie 信息。这个值可以包含子域,如果没有明确设定,那么这个域会被认作来自设置 cookie的那个域。
- 路径:对于指定域中的那个路径,应该向服务器发送 cookie。
- 失效时间:表示 cookie 何时应该被删除的时间戳(GMT日期格式)。默认情况下,浏览器会话结束时即将所有 cookie 删除;
- 安全标志:指定后,cookie 只有在使用 SSL 连接到时候才发送到服务器。
每一段信息都作为 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
因为设置了 secure 标志,这个 cookie 只能通过 SSL 连接才能传输。
域、路径、失效时间、secure 标志都是服务器给浏览器的指示,以指定何时应该发送 cookie。这些参数并不会作为发送服务器的 cookie 信息的一部分,只有明值对儿才会被发送。
3.1.3、JavaScript 中的 cookie
在 JavaScript 中处理 cookie 有些复杂,即 BOM的 document.cookie属性,返回当前页面可用的 所有 cookie 的字符串,一系列由分号隔开的名值对儿。
name1=value1;name2=value2;name3=value3
所有名字和值都是经过URL 编码的,所以必须使用 decodeURIComponent() 来解码
当用于设置值的时候,document.cookie 属性可以设置为一个新的 cookie 字符串。这个cookie 字符串会被添加到 cookie 集合中。
设置 cookie 的格式如下,和 Set-Cookie 头中使用的格式一样。
name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure
这些参数中,只有 cookie 的名字和值是必需的
document.cookie = 'name=Lee'
最好每次设置 cookie 时都想下面这个例子中一样使用 encodeURIComponent()
document.cookie = encodeURIComponent('name') + '=' + encodeURIComponent('Lee');
由于 JavaScript 中读写 cookie 不是非常直观,常常需要写一些函数来简化 cookie 的功能。基本 cookie 操作有三种:读取、写入和删除。
var CookieUtil = {
get: function(name) {
var cookieName = encodeURIComponent(name) + '=',
cookieStart = document.cookie.indexOf(cookieName), // 查找 查询键的位置
cookieValue = null
if (cookieStart > -1) { // 存在对应 键
var cookieEnd = document.cookie.indexOf(';', cookieStart) // 查找对应键 的值结束位置
if (cookieEnd == -1) { // 属于 最后一个键值对
cookieEnd = document.cookie.length
}
cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd)) // 返回指定部分的字符串
}
return cookieValue
},
set: function(name, value, expires, path, domain, secure) {
var cookieText = encodeURIComponent(name) + '=' + encodeURIComponent(value)
if (expires instanceof Date) {
cookieText += '; expires=' + expires.toGMTString()
}
if (path) {
cookieText += '; path=' + path
}
if (domain) {
cookieText += '; domain' + domain
}
if (secure) {
cookieText += '; secure'
}
document.cookie = cookieText
},
unset: function(name, path, doamin, secure) {
this.set(name, '', new Date(0), path, domain, secure) //设置过期时间
}
}
可以像下面这样使用上述方法
// 设置 cookie
CookieUtil.set('name', 'Nicholas')
CookieUtil.set('book', 'Professional JavaScript')
// 读取 cookie 的值
console.log(CookieUtil.get('name')) //Nicholas
console.log(CookieUtil.get('book')) // Professional JavaScript
// 删除 cookie
CookieUtil.unset('name')
CookieUtil.unset('book')
// 设置 cookie,包括它的路径、域、失效日期
CookieUtil.set('name', 'Nicholas', '/books/projs/', 'www.wrox.com', new Date('January 1. 2010'))
// 删除刚刚设置的 cookie
CookieUtil.unset('name', '/books/projs', 'www.wrox.com')
// 设置安全的 cookie
CookieUtil.set('name', 'Nicholas', null, null, null, true)
3.1.4、子 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,必须建立一系列新方法。
var SubCookieUtil = {
get: function(name, subName) {
var subCookies = this.getAll(name) // 获取子 cookie 对象
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,
len,
parts,
result = {}
if (cookieStart > -1) { // 存在 子 cookie
cookieEnd = document.cookie.indexOf(';', cookieStart) // 子 cookie 结束的位置
if (cookieEnd == -1) { // 父cookie 是最后一对键值
cookieEnd = document.cookie.length // 子 cookie 结束的位置即为 cookie的长度
}
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
}
}
}
可以像下面这样使用 上述方法:
// 假设 cookie 为以下值
document.cookie = 'data=name=Nicholas&book=Professional%20JavaScript'
// 取得全部子cookie
var data = SubCookieUtil.getAll('data')
console.log(data.name) // Nicholas
console.log(data.book) // Professional JavaScript
// 逐个获取 子 cookie
console.log(SubCookieUtil.get('data', 'name')) // Nicholas
console.log(SubCookieUtil.get('data', 'book')) // Professional JavaScript
要设置 子 cookie,也有两种方法:set() 和 setAll()。
以下代码展示了他们的构造
var SubCookieUtil = {
set: function(name, subName, value, expires, path, domain, secure) {
var subcookies = this.getAll(name) || {} // 获取 cookie 键值对象
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) { // 遍历 子 cookie 对象
if (subName.length > 0 && subcookies.hasOwnProperty(subName)) {
// 以键值对字符串的形式 保存在数组中
subcookieParts.push(encodeURIComponent(subName) +'='+ encodeURIComponent(subcookies[subName]))
}
}
if (subcookieParts.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
}
// ...省略了更多代码
}
可以按照以下方式 来使用 set() 和 setAll() 方法
// 假设 cookie 为以下值
document.cookie = 'data=name=Nicholas&book=Professional%20JavaScript'
// 设置两个 cookie
SubCookieUtil.set('data', 'name', 'Nicholas')
SubCookieUtil.set('data', 'book', 'Professional JavaScript')
// 设置全部子 cookie 和失效日期
SubCookieUtil.setAll('data', {name: 'Nicholas', book: 'Professional JavaScript'}, new Date('January 1, 2020'))
// 修改名字的值,并修改 cookie的失效日期
SubCookieUtil.set('data', 'name', 'Michael', new Date('February 1, 2020'))
子 cookie 的最后一组方法是用于删除 子 cookie 的。普通 cookie 可以将失效时间设置为过去的时间的方法来删除,但是子 cookie 不能这样做。首先必须获取包含在 某个 cookie 中的所有子 cookie,然后仅删除需要删除的那个子 cookie,然后再将余下的子 cookie的值 保存为 cookie的值。
var SubCookieUtil = {
// ...省略了更多代码
unset: function(name, subName, path, domain, secure) {
var subCookies = this.getAll(name) // 获取 子 cookie
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)
}
}
这两个方法可以像下面这样使用
// 仅删除名为 name 的子 cookie
SubCoolieUtil.unset('data', 'name')
// 删除整个 cookie
SubCookieUtil.unsetAll('data')
如果你担心开发中可能会达到单域名的 cookie 上限,那么子 cookie 可是一个非常有吸引力的备选方案。不过,你需要更加密切关注 cookie 的长度,以防超过单个 cookie 的长度限制。
3.1.5、关于 cookie 的思考
由于 所有的 cookie 都会有浏览器作为 请求头发送,所以在 cookie 中存储大量信息会影响到特定域的 请求性能。cookie 的性质和它的局限使得其并不能作为存储大量信息的理想手段。
3.2、IE 用户数据
在 IE5.0 中,微软通过一个自定义行为引入了持久化用户数据的概念。用户数据允许每个文档最多 128KB 数据,每个域名最多 1MB数据。
要使用持久化用户数据,首先必须如下所示,使用 CSS 在某个元素上指定 userData 行为:
一旦元素使用了 userData 行为,那么就可以使用 setAttribute() 方法在上面保存数据了。为了将数据提交到浏览器缓存中,还必须调用 save() 方法并告诉它要保存到的数据空间的名字。数据空间名字可以完全任意,仅用于区分不同的数据集。
var dataStore = document.getElementById('dataStore')
dataStore.setAttribute('name', 'Nicholas')
dataStore.setAttribute('book', 'Professional JavaScript')
dataStore.save('BookInfo')
在下一次页面载入之后,可以使用 load() 方法指定 同样的数据空间名称来获取数据
dataStore.load('BookInfo')
console.log(dataStore.getAttribute('name')) // Nicholas
console.log(dataStore.getAttribute('book')) // Prefessional JavaScript
你可以通过 removeAttribute() 方法明确指定要删除某些元素数据,只要指定属性名称。
删除之后,必须像下面这样再次调用 save() 来提交更改
dataStore.removeAttribute('name')
dataStore.save('BookInfo')
对 IE 用户数据的访问限制和对 cookie 的限制类似。要访问某个数据空间,脚本运行的页面必将来自用一个域名,在同一个路径下,并使用与进行存储的脚本同样的协议。和 cookie 不同的是:
- 你无法将 用户数据访问限制扩展到更多的客户。
- 用户数据默认是可以跨越会话持久存在的,同时也不会过期; 数据需要通过 removeAttribute() 方法专门进行 删除以释放空间。
3.3、Web 存储机制
Web Storage 最早是在 Web 超文本应用技术工作组(WHAT-WG)的Web 应用 1.0 规范中描述的。这个规范的最初的工作最终成为了 HTML5 的一部分。Web Storage 的目的是克服由 cookie 带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器。
Web Storage 的两个主要目标是:
- 提供一种在 cookie 之外存储会话数据的途径;
- 提供一直存储大量可以跨会话存在的数据的机制
最初 的 Web Storage 规范包含了两种对象的定义:sessionStorage 和 globalStorage。这两个对象再支持的浏览器中都是以 window 对象属性的形式存在的。
3.3.1、Storage 类型
Storage 类型提供最大的存储空间(因浏览器而异)来存储名值对儿。Storage 的实例与其他对象类似,有如下方法。
- clear():删除所有值;
- getItem(name):根据指定的名字 那么获取对应的值。
- key(index):获得 index 位置处的值的名字
- removeItem(name):删除有 name 指定的明值对儿。
- setItem(name, value):为指定的 name 设置一个对应的值。
其中,getItem()、removeItem()、setItem() 可以直接调用,也可以通过 Storage对象访问。不过,建议使用方法而不是属性来访问数据,以免某个键会以外重写该对象上已经存在的成员。
还可以使用 length 属性来判断有多少名值对儿存放在 Storage 对象中。
3.3.2、sessionStorage 对象
sessionStorage 对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。存储在 sessionStorage 中的数据可以跨越页面刷新而存在,同时如果浏览器支持,浏览器奔溃并重启之后依然可用
sessionStorage 对象其实是 Storage 的一个实例,所以可以使用 setItem() 或者直接设置新的属性来存储数据。下面是这两种方法的例子:
// 使用方法存储数据
sessionStorage.setItem('name', 'Nicholas')
// 使用属性存储数据
sessionStorage.book = 'Professional JavaScript'
不同浏览器写入数据方式略有不同。Firefox 和 WebKit 实现了同步写入,而IE 的实现则是异步写入数据,对于少量数据而言,这个差异是可以忽略的
sessionStorage 中有数据时,可以使用 getItem() 或者通过直接访问属性来获取
数据
// 使用方法读取数据
console.log(sessionStorage.getItem('name'))
// 使用属性读取数据
console.log(sessionStorage.book)
还可以通过结合 length 属性 和 key() 方法来迭代 sessionStorage 中的值
for (var i = 0, len = sessionStorage.length; i < len; i++) {
var key = sessionStorage.key(i)
var value = sessionStorage.getItem(key)
console.log(key + ': ' + value)
}
还可以使用 for-in 循环来迭代 sessionStorage 中的值
for (var key in sessionStorage) {
var value = sessionStorage.getItem(key)
console.log(key + ': ' + value)
}
要从 sessionStorage 中删除数据,可以使用 delete 操作符删除对象属性,也可以调用 removeItem() 方法。
// 使用 delete 删除一个值
delete sessionStorage.name
// 使用方法删除一个值
sessionStorage.removeItem('book')
sessionStorage 对象应该主要用于仅针对会话的小段数据的存储。如果需要跨越会话存储数据,那么 globalStorage 或者 localStorage 更为合适
3.3.3、globalStorage 对象
要使用 globalStorage,首先要指定 哪些域可以访问该数据。可以通过方括号标记使用属性来实现,
// 保存数据
globalStorage['wrox.com'].name = 'Nicholas'
// 获取数据
var name = globalStorage['wrox.com'].name
globalStorage 对象不是 Storage 的实例,而具体的 globalStorage['wrox.com'] 才是。这个存储空间对于 wrox.com 及其所有子域都是可以访问的。可以像下面这样指定子域名
// 保存数据
globalStorage['www.wrox.com'].name = 'Nicholas'
// 获取数据
var name = globalStorage['www.wrox.com'].name
这里所指定的存储空间只能来自 www.wrox.com 的页面访问,其他子域名都不行。
某些浏览器允许更加宽泛的访问限制,比如只根据顶级域名进行限制或者允许全局访问。
// 存储数据,任何人都可以访问——不建议这样做
globalStorage[''].name = 'Nicholas'
// 存储数据,可以让任何以 .net 结尾的域名访问 —— 不要这样做!
globalStorage['net'].name = 'Nicholas'
虽然这些也支持,但是还是要避免使用这种可宽泛访问的数据存储,以防止出现潜在的安全问题。
对 globalStorage 空间的访问,是依据发起请求的页面的域名、协议、端口来限制的。这类似于 Ajax 的同源策略。
globalStorage 的每个属性都是 Storage 的实例。因此,可以像如下代码中这样使用。
globalStorage['www.wrox.com'].name = 'Nicholas'
globalStorage['www.wrox.com'].book= 'Professional Javascript'
globalStorage['www.wrox.com'].removeItem('name')
console.log(globalStorage['www.wrox.com'].getItem('book'))
如果你事先不能确定域名,那么使用 location.host 作为属性名比较安全。
globalStorage[location.host].name = 'Nicholas'
console.log(globalStorage[location.host].getItem('book'))
如果不使用 removeItem() 或者 delete 删除,或者用户未清除浏览器缓存,存储在 globalStorage 属性中的数据会 一直保留在磁盘上。这让 globalStorage 非常适合在客户端存储 文档或长期保存用户偏好设置。
3.3.4、localStorage 对象
localStorage 对象在 修订过的 HTML5 规范中作为持久保存客户端数据的方案取代了 globalStorage。
要访问一个 localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。这相当于 globalStorage[location.host]
由于 localStorage 是 Storage 的实例,所以可以像使用 sessionStorage 一样来使用它。
// 使用 方法存储数据
localStorage.setItem('name', 'Nicholas')
// 使用 属性存储数据
localStorage.book = 'Professional JavaScript'
// 使用方法读取数据
console.log(localStorage.getItem('name'))
// 使用属性读取数据
console.log(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')
}
}
然后,像下面这样调用这个函数,就可以正常地读写数据了
var storage = getLocalStorage()
3.3.5、storage 事件
对 Storage 对象进行任何修改,都会在文档上触发 storage 事件。
这个事件的 event 对象有以下属性:
- domain:发生变化的存储空间的域名
- key:设置或删除的键名
- newValue:如果是设置值,则是新值;如果是删除键,则是 null
- oldValue:键被更改之前的值
以下代码展示了如何侦听 storage 事件
document.onstorage = function(event) {
console.log('Storage changed for ' + event.domain)
}
无论是 sessionStorage、globalStorage、localStorage 进行操作,都会触发 storage 事件,但不做区分。
3.3.6、限制
Web Storage 也有限制。这些限制因浏览器而异。对存储空间大小的限制都是以每个来源(协议、域、端口)为单位的。考虑到这个限制,就要注意分析和控制每个来源中有多少页面需要保持数据。
对于 localStorage 而言,大多数桌面浏览器会设置每个来源 5MB的限制。Chrome 和 Safari 对每个来源的限制时 2.5MB。而 iOS 版 Safari 和 Android 版 Webkit 的限制也是 2.5MB
对 sessionStorage 的限制也是因浏览器而异。有的浏览器对 sessionStorage 的大小没有限制 但Chrome、Safari、iOS 版的Safari 和 Android版 WebKit 都有限制,也都是 2.5MB。IE8+ 和 Opera 对象 sessionStorage 的限制时 5MB。
3.4、IndexedDB
Indexed Database API, 是在浏览器中保存结构化数据的一种数据库。IndexedDB 的思想是创建一套 API,方便保存 和 读取 JavaScript 对象,同时还支持查询及搜索。
indexDB设计的操作完全是异步进行的。差不多每一次 indexedDB 操作,都需要你注册 onerror 或 onsuccess事件处理程序,以确保适当处理结果
在得到完整支持的情况下,IndexDB 将是一个作为API 宿主的全局对象。由于 API 仍然可能有变化,浏览器也都使用提供商前缀,因为这个对象在 IE10 中叫 msIndexedDB,在 Firefox 4中叫 mozIndexedDB,在 Chrome 中叫 webkitIndexDB。
var indexedDB = window.indexDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB
3.4.1、数据库
IndexedDB 就是一个数据库,最大的特色是使用对象保存数据,而不是使用表保持数据。一个IndexedDB 数据库,就是一组位于相同命名空间下的对象的集合
使用 indexedDB 的第一步是打开它,即把要打开的数据库名传给 indexedDB.open()。如果传入的数据库已经存在,就会发送一个打开它的请求;如果传入的数据库还不存在,就会发送一个创建并打开它的请求。
调用 indexedDB.open() 会返回一个 IDBRequest 对象,在这个对象上可以添加 onerror 和 onsuccess 事件处理程序。
var indexedDB = window.indexDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB
var request = indexedDB.open('admin')
request.onerror = function(event) {
console.log('Error: ' + event.target.errorCode) // 发生错了,保存一个错误码
}
request.onsuccess = function(event) {
var database = event.target.result // 响应成功 数据库实例对象
// console.log(database) // IDBDatabase
}
在这两个事件处理程序中, event.target 都指向 request 对象。
以下就是 error 中 errrorCode 可能出现的 错误码(这个错误码适合所有操作):
- IDBDatabaseException.UNKNOWN_ERR(1):意外错误,无法归类
- IDBDatabaseException.NON_TRANSIENT_ERR(2):操作不合法
- IDBDatabaseException.NOT_FOUND_ERR(3):未发现要操作的数据库
- IDBDatabaseException.CONSTRAINT_ERR(4):违反了数据库约束
- IDBDatabaseException.DATA_ERR(5):提供给事务的数据不能满足要求
- IDBDatabaseException.NOT_ALLOWED_ERR(6):操作不合法
- IDBDatabaseException.TRANSACTION_INACTIVE_ERR(7):试图重用已完成的事务。
- IDBDatabaseException.ABORT_ERR(8):请求中断,未成功
- IDBDatabaseException.READ_ONLY_ERR(9):试图在只读模式下写入或修改数据
- IDBDatabaseException.TIMEOUT_ERR(10):在有效时间内未完成操作
- IDBDatabaseException.QUOTA_ERR(11):磁盘空间不足
默认情况下,indexedDB 数据库是没有版本号的,最好一开始就位数据库指定一个版本号。为此,可以调用 setVersion() 方法,传入以字符串形式表示的版本号。同样,调用这个方法也会返回一个请求对象,需要你再指定时间处理程序。
if (database.version != '1.0') request = database.setVersion('1.0')
3.4.2、对象存储空间
在建立了与数据库的连接之后下一步就是使用对象存储空间。如果数据库的版本与你传入的版本不匹配,那可能就需要创建一个新的对象存储空间。在创建对象存储空间之前,必须要想清楚你想要保存什么类型数据。
保存一条记录的对象应该类似如下所示:
var user = {
id: '00,
username: 'momo11123',
password: 'foo'
}
有了这个对象,很容易想到 username 属性可以作为这个对象存储空间的键。这个 username 必须全局唯一,而且大多数时候都要通过这个键来访问数据,这一点非常重要,因为在创建对象存储空间时,必须指定这个一个键。
以下是为了保存上述用户记录而创建对象存储空间的示例:
request.onupgradeneeded = function(event) { // 必须在 upgradeneeded 事件中进行创建
var db = event.target.result;
var objectStore = db.createObjectStore("users", { keyPath: "id" });
console.log('数据库版本更改为: ' + db.version);
}
第一次打开成功后或者版本有变化时会触发这个事件:一般用于初始化数据库。其中,第二个参数中的keyPath 属性,就是空间中将有保存的对象的一个属性,而这个属性将作为存储空间的 键来使用
接下来可以使用 add() 或 put() 方法来向其中添加数据。这两个方法都接受一个参数,即要保存的对象。区别在于,在空间中已经包含键值相同的对象时,add() 会返回错误,而 put() 会重写原有对象。
// users 中保存着一批用户对象
var users = [
{
id: '001',
username: 'lwx',
password: 'mom1123'
},
{
id: '002',
username: 'smlz',
password: 'momo123'
},
]
var i = 0, len = users.length
while(i < len) {
store.add(users[i++])
}
每次调用 add() 或 put() 都会创建一个新的针对这个对象存储空间的更新请求。如果想验证请求是否完成成功,可以把返回的请求对象保存在一个变量中,然后再指定 onerror 或 onsuccess 事件处理程序
// users(数组) 中保存着一批 用户对象
var users = [
{
id: '001',
username: 'lwx',
password: 'mom1123'
},
{
id: '002',
username: 'smlz',
password: 'momo123'
},
]
var i = 0,
request,
requests = [],
len = users.length
while(i < len) {
request = store.add(users[i++])
request.onerror = function() {
// 处理错误
}
request.onsuccess = function() {
// 处理成功
}
request.push(request)
}
3.4.3、事务
跨越创建对象存储空间这一步后,接下来的所有操作都是通过事务来完成的。在数据库对象上调用 transaction() 方法可以创建事务。任何时候,只要想读取或修改数据,都要通过事务来组织所有操作。在最简单的情况下,可以像下面这个创建事务。
var transaction = db.transaction('users')
如果要访问多个对象存储空间,也可以在第一个参数的位置上传入字符串数组
var transaction = db.transaction(['users', 'anothersStore'])
这些事务都是以只读方式访问数据。要修改访问方式,必须在创建事务时传入第二个参数,这个参数表示访问模式,用 IDBTransaction 接口定义的如下常量表示:
- READ_ONLY(0):表示只读
- READ_WRITE(1):表示读写
- VERSION_CHANGE(2):表示改变
IE 10+ 和 Firefox 4+ 实现的是 IDBTransaction,但在 Chrome 中则叫 webkitIDBTransaction,所以使用下面的代码可以统一接口:
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction
如此,就可以更方便的为 transaction() 指定第二个参数了
var transaction = db.transaction('users', IDBTransaction.READ_WRITE)
取得了事务的索引后,使用 objectStore() 方法并传入存储空间的名称,就可以访问特定的存储空间。然后,可以像以前一样使用 add() 和 put() 方法,此外:
- get() 用来获取值 —— 接受一个对象键作为参数
- delete() 用来删除对象 —— 接受一个对象键作为参数
- clear() 则可以删除所有对象
var transaction = db.transaction('users', IDBTransaction.READ_WRITE)
var request = transaction.objectStore('users').get('001')
request.onerror = function(event) {
console.error('Error ' + event.error.message)
}
request.onsuccess = function(event) {
var result = event.target.result
console.log(result.username) // lwx
}
因为 一个事务 可以完成任何多个请求,所以事务对象本身也有事件处理程序:onerror 和 oncomplete。这两个事件可以提供事务级的状态信息。
transaction.onerror = function(event) {
// 整个事务都被取消了
}
transaction.oncomplete = function(event) {
// 整个事务都成功完成了
}
注意:通过 oncomplete 事件的事件对象(event)访问不到 get() 请求返回的任何数据。必须在 相应请求的 onsuccess 事件处理程序中才能访问到数据。
3.4.4、使用游标查询
使用 事务可以直接通过已知的键检索单个对象。而在需要检索多个对象的情况下,则需要在事务内部创建游标。游标就是一指向结果集的指针。游标并不提前收集结果。游标指针会先指向结果中的第一项,在接到查找下一项指令是,才会指向下一项。
在对象存储空间上调用 openCursor() 方法可以创建游标。与 indexedDB 中的其他操作一样,openCursor() 方法返回的是一个请求对象,因此必须为该对象指定 onsuccess 和 onerror 事件处理程序。
var store = db.transaction('users').objectStore('users')
var 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))
}
}
使用 游标可以更新个别的记录,调用 update() 方法可以用指定的对象更新当前游标的 value。与其他操作一样,调用 update() 方法也会创建一个 新请求,因此如果你想知道结果,就要为它指定 onsuccess 和 onerror 事件处理程序。
var store = db.transaction('users', 'readwrite').objectStore('users') // 更新需要在读写模式下进行
var request = store.openCursor() // 创建游标
request.onerror = function(event) {
console.error('Error ' + event.error.message)
}
request.onsuccess = function(event) {
var cursor = event.target.result
if (cursor) { // 必须要检查
if (cursor.key == '001') {
var value = cursor.value
value.password = 'MOMO_)GG'
var updateRequest = cursor.update(value)
updateRequest.onsuccess = function(event) {
console.log('处理成功')
}
updateRequest.onerror = function(event) {
console.error('Error ' + event.taregt.error.message)
}
}
}
}
如果调用 delete() 方法,就会删除相应的记录。与 update() 一样,调用 delete() 也返回一个请求
request.onsuccess = function(event) {
var cursor = event.target.result
console.log(cursor)
if (cursor) { // 必须要检查
if (cursor.key == '001') {
vardeleteRequest = cursor.delete()
deleteRequest.onsuccess = function(event) {
console.log('处理成功')
}
deleteRequest.onerror = function(event) {
console.error('Error ' + event.taregt.error.message)
}
}
}
}
如果当前事务没有修改 对象存储空间的权限,update() 和 delete() 会抛出错误
默认情况下,每个游标只发送起 一次请求。要想发起另一次请求,必须调用下面的一个方法
- continue([key]):移动到结果集中的下一项。参数 key 是可选的,不指定这个参数,游标移动到下一项;指定这个参数,游标会移动到指定位置。
- advance(count):向前移动 count 指定的项数
着两个方法都会导致游标使用相同的请求,因此同样的 onsuccess 和 onerror 使劲按处理程序也会得到重用。例如,下面的例子遍历了对象存储空间中的所有项。
request.onsuccess = function(event) {
var cursor = event.target.result
console.log(cursor)
if (cursor) { // 必须要检查
cursor.continue() // 移动到下一项
} else {
console.log('Done!')
}
}
调用 continue() 会触发另一次请求,进而再次调用 onsuccess 事件处理程序。在没有更多项可以迭代时,将最后一次调用 onsuccess 事件处理程序,此时 event.target.result 的值为 null。
3.4.5、键范围
键范围(key range) 为适用游标增添了一些灵活性。键范围有 IDBKeyRange 的实例表示。
支持标准 IDBKeyRange 类型的浏览器有 IE10+、Firefox4+,Chrome 中的名字叫 webkitIDBKeyRange。与使用 IndexedDB 中的其他类型一样,你最好先声明一个本地的类型,同时要考虑到不同浏览器中的差异。
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange
有四种定义键范围的方式:
- 使用 only() 方法,传入你想要取得的对象的键
var onlyRange = IDBKeyRange.only('001')
这个范围可以保证只取得键为 ‘007’ 的对象。使用这个范围创建的游标与直接访问存储空间并调用 get('001')差不多
- 指定结果集的下界,lowerBound()。下界表示游标开始的位置。例如,以下键范围可以保证游标从键为‘001’的对象开始,然后继续向西移动,直至最后一个对象
// 从键为 001 的对象开始,然后可以移动到最后
var lowerRange = IDBKeyRange.lowerBound('001')
如果你想忽略键为 001 的对象,从它的下一个对象开始,那么可以传入第二个参数 true:
var lowerRange = IDBKeyRange.lowerBound('001', true)
- 指定结果集上界,也就是指定游标不能超越哪个键。指定上界使用 upperRange() 方法
// 从头开始,到键为 002 的对象为止
var upperRange = IDBKeyRange.upperBound('002')
如果不想包含键为指定值对象,同样,传入第二个参数 true
var upperRange = IDBKeyRange.upperBound('002', true)
- 同时指定上、下界,使用 bound() 方法。这个方法可以接收 4个参数:表示下界的键、表示上界的键、可选的表示是否跳过下界的布尔值、可选的表示是否跳过下界的布尔值和可选的表示是否跳过上界的布尔值。
// 从键为 001 的对象开始,到键为007的对象
var boundRange = IDBKeyRange.bound('001', '007')
// 从键为 001 的对象的下一个对象开始,到键为 007 的对象为止
var boundRange = IDBKeyRange.bound('001', '007', true)
// 从键为 001 的对象的下一个对象开始,到键为 007 的对象的上一个为止
var boundRange = IDBKeyRange.bound('001', '007', true, true)
// 从键为 001 的对象开始,到键为 007 的对象的上一个对象为止
var boundRange = IDBKeyRange.bound('001', '007', false, true)
无论如何,在定义键范围之后,把它传给 openCursor() 方法,就能得到一个符合响应约束条件的游标。
var store = db.transaction('users', 'readwrite').objectStore('users') // 更新需要在读写模式下进行
var range = IDBKeyRange.bound('001', '007')
var request = store.openCursor(range) // 创建游标
request.onerror = function(event) {
console.error('Error ' + event.error.message)
}
request.onsuccess = function(event) {
var cursor = event.target.result
if (cursor) { // 必须要检查
console.log('key: ' + cursor.key + ': ' + JSON.stringify(cursor.value))
cursor.continue() // 移动到下一项
} else {
console.log('done!')
}
}
3.4.6、设定游标方向
实际上,openCursor() 可以接收两个参数
- 第一个参数是刚刚上面演示的 IDBKeyRange 的实例。
- 第二个是表示方向的数值常量。
首先还是本地消除差异:
var IDBCursor = window.IDBCursor || window.webkitIDBCursor
游标的默认方向值是 IDBCursor.NEXT。如果对象存储空间中有重复的项,而你想让游标跳过那些重复的项,可以为 openCursor 传入 IDBCursor.NEXT_NO_DUPLICATE 作为第二个参数
var store = db.transcation('users').objectStore('users')
var request = store.openCursor(null, IDBCursor.NEXT_NO_DUPLICATE)
当然,也可以创建一个游标,让他在对象存储空间中向后移动,即从最后一个对象开始,逐个迭代,直至第一个对象。此时,要传入的常量是 IDBCursor.PREV 和 IDBCursor.PREV_NO_DUPLICATE。
var store = db.transcation('users').objectStore('users')
var request = store.openCursor(null, IDBCursor.PREV)
使用 上述两种参数 打开游标时,每次调用 continue() 或 advance(),都会在存储空间中向后而不是向前移动游标
3.4.7、索引
对于某些数据,可能需要为一个对象存储空间指定多个键,除主键外,还可以创建索引。
首先,引用对象存储空间,然后调用 createIndex() 方法
var store = db.transaction('users').objectStore('users'),
index = store.createIndex('username', 'username', {unique: false})
createIndex() 三个参数分别表示:
- 第一个参数是索引的名字
- 第二个参数是索引的属性的名字
- 第三个参数是一个包含 unique 属性的选项(options)对象。这个选项通常都必须指定,因为它表示键所在记录中是否唯一。
因为 username 有可能重复,所以这里的值为 false
createIndex() 的返回值是 IDBIndex 的实例。在对象存储空间上 调用 index() 方法也能返回同一个实例。
var store = db.transaction('users').objectStore('users'),
index = store.index('username') // 获取索引
在索引上调用 openCursor() 方法也可以创建新的游标,除了将来会把索引键而非主键保存在 event.result.key 属性中之外,这个游标与在对象存储空间上调用 openCursor() 返回的游标完全一样。
var store = db.transaction('users').objectStore('users'),
index = store.index('username'),
request = index.openCursor()
request.onsuccess = function(event) {
// 处理成功
}
在索引上也能创建一个特殊的只返回每条记录主键的游标,那就要调用 openKeyCursor() 方法。其中 event.result.key 中保存着索引键,而 event.result.value 中保存的则是主键
var store = db.transaction('users').objectStore('users'),
index = store.index('username'),
request = index.openCursor()
request.onsuccess = function(event) {
// 处理成功
console.log(event.result)
}
同样,使用 get() 方法能够从索引中取得一个对象,只要传入相应的索引键即可;这个方法也将返回一个请求
var store = db.transaction('users', 'readwrite').objectStore('users'),
index = store.index('id'),
request = index.openKeyCursor('007')
request.onerror = function(event) {
// 处理失败
}
request.onsuccess = function(event) {
// 处理失败
}
要根基给定的索引键取得主键,可以使用 getKey() 方法。这个方法也会创建一个新的请求,但 event,result.value 等于主键的值,而不是包含整个对象
var store = db.transaction('users', 'readwrite').objectStore('users'),
index = store.index('id'),
request = index.getKey('007')
request.onsuccess = function(event) {
// 处理成功
}
任何时候,通过 IDBIndex 对象的下列属性都可以取得有关索引的相关信息
- name:索引的名字。
- keyPath:传入 createIndex() 中的属性路径
- objectStore:索引的对象存储空间。
- unique:表示索引键是否唯一的布尔值
另外,通过对象存储对象的 indexName 属性可以访问到位该空间建立的所有索引。
根据存储的对象建立了哪些索引
var store = db.trasaction('users').objectStore('users'),
indexNames = store.indexNames
index,
i = 0,
len = indexNames.length
while(i < len) {
index = store.index(indexNames[i++])
console.log('index Name: ' + index.name + ', keyPath: ' + index.keyPath + ', unique: ' + index.unique)
}
在 对象存储空间上调用 deleteIndex() 方法 并传入索引的名字可以删除索引。
var store = db.transaction('users').objectStore('users')
store.deleteIndex('username')
3.4.8、并发问题
虽然网页中的 IndexedDB 提供的是异步 API,但任然存在并发操作的问题。
如果浏览器的两个不同的标签打开了同一个页面,那么一个页面视图更新另一个页面尚未准备就绪的数据库的问题就有可能发生。
把数据库设置为新版本有可能导致这个问题。因此,只有当浏览器中仅有一个标签页使用数据库的情况下,调用 setVersion() 才能完成操作。
刚打开数据库时,要记着指定 onversionchange 事件处理程序,当同一个来源的另一个标签页调用 setVersion() 时,就会执行这个回调函数。处理这个事件的最佳方式是关闭数据库,从而保证版本更新顺利完成。
var request, database
request = indexedDB.open('admin')
request.onsuccess = function(event) {
database = event.target.result
database.onversionchange = function() {
database.close() // 关闭数据库
}
}
调用 setVersion() 时,指定请求的 onblocked 事件处理程序也很重要。在你想要更新数据库的版本但另一个标签页已经打开数据库的情况下,就会触发这个事件处理程序。此时,最好先通知用户关闭其他标签页,然后再重新调用 setVersion()。例如:
var request = database.setVersion('2.0')
request.onblocked = function() {
alert('Please c;pse all other tabs any try again.')
}
request.onsuccess = function(event) {
// 处理成功...
}
请记住,其他标签页中的 onversionchange 事件处理程序也会执行
3.4.9、限制
对 IndexedDB 的限制 很多斗鱼 对 Web Storage 的类似。
- 首先,IndexedDB 数据库只能由同源(相同协议,域名、端口)页面操作,因此不能跨域共享信息。换句话说,www.wrox.com 与 p2p.wrox.com 的数据库是完全独立的。
- 其次,每个来源的数据库占用的磁盘空间也有限制。Firefox 4+ 目前的上限是 每个源 50MB,而 Chrome 的限制时 5MB。移动设备上的 Firefox 最多运行保存 5MB,如果超过了这个配额,将会请求用户的许可
Firefox 还有另一个限制,即不允许本地文件访问IndexedDB。Chrome 没有这个限制。
四、小结
离线 Web 应用程序和客户端存储数据的能力对未来的Web 应用越来越重要。浏览器已经能够检测到用户是否离线,并处罚 JavaScript 事件以便应用做出处理。可以指定在应用缓存中保存哪些文件以便离线时使用,对于应用缓存的状态及变化,也有相应的 JavaScript API 可以调用 检测。
客户端存储
- cookie 是一小块可以客户端设置 也可以在 服务器端设置的信息,每次请求时都会传送它。
- 在JavaScript中 通过 document.cookie 进行访问 cookie
- cookie 的限制使其可以存储少量数据,对于大量数据效率很低。
Web Storage 定义了两种用于存储数据的对象:sessionStorage、localStorage。前者严格用于在一个浏览器会话中存储数据,因为数据在浏览器关闭后会立即删除;后者用于跨会话持久数据并遵循跨域安全策略。
IndexDB 是一种类似 SQL 数据库的结构化数据存储机制。但它的数据不是保存在表中,而是保存在对象存储空间中。创建对象存储空间时,需要定义一个键,然后就可以添加数据。可以使用游标在对象存储空间中查询特定的对象。而索引则是为了提高查询速度而基于特定的属性创建的。