《JavaScript高级程序设计》第二十三章,离线应用与客户端存储。
支持离线Web应用是HTML5的一个重点。开发离线Web应用需要几个步骤:首先应用要能够知道设备是否在线;其次,应用还必须能访问一定的资源(图像、JavaScript、CSS等),这样才能正常工作;最后,必须有一块用于保存数据的本地空间,无论是否上网都不妨碍读写。
离线检测
HTML5定义了navigator.onLine
属性,true
表示设备能上网,false
表示设备离线;HTML5还定义了online
和offline
事件,分别在网络从离线变为在线和从在线变为离线时触发(在window
对象上触发)。
1 | if(navigator.onLine) { |
应用缓存
HTML5的应用缓存(application cache, appcache)是从浏览器缓存中分出来的一块儿缓存区,专门为开发离线Web应用而设计的。
使用方法
用描述文件(manifest file)列出要下载和缓存的资源。
1 | CACHE MANIFEST |
将描述文件与页面关联起来,这个文件的MIME类型必须是text/cache-manifest
。
1 | <html manifest="./offline.manifest"> |
相关对象
应用缓存有相关的JavaScript API,它的核心是applicationCache
对象,有下面的属性:
status
:表示缓存的状态,有下面几个取值0
:无缓存,没有与页面相关的appcache1
:闲置,appcache未得到更新2
:检查中,正在下载manifest文件并检查更新3
:下载中,appcache正在下载manifest文件中指定资源4
:更新完成,appcache已更新资源,所有资源已下载完毕,可通过swapCache()
来使用5
:废弃,appcache的manifest文件已不存在,页面无法再访问appcache
相关的事件:
checking
:浏览器为appcache查找更新时触发error
:检查更新或下载资源期间发生错误时触发noupdate
:检查manifest文件发现文件无变化时触发downloading
:开始下载资源时触发progress
:文件下载appcache时持续触发updateready
:页面新的appcache下载完毕且可通过swapCache()
使用时触发cached
:appcache完整可用时触发
一般来讲,上述事件会随着页面加载依次触发。当调用update()
方法时,顺序有所不同:会先检查manifest文件是否更新(checking
事件);再像页面刚刚加载那样继续执行后续操作;如果触发了cached
事件,则说明appcache已就绪,不用发生其它操作了;如果触发了updateready
事件,则说明新版本的appcache已可用,需要调用swapCache()
来启用新应用缓存。
1 | applicationCache.update(); |
数据存储
随着Web应用程序的出现,产生了对于能够直接在客户端上存储用户信息能力的要求。也就是让某个特定用户的信息存在该用户的机器上,包括登录信息、偏好设定或其它数据,下面就是现有的一些方法。
Cookie
HTTP Cookie是Netscape公司提出的一种客户端存储数据的方式,用来存储会话信息。
使用方法
服务器对任意HTTP请求发送Set-Cookie
响应头部,其中包含会话信息,类似下面:
1 | 200 OK |
浏览器会存储这样的会话信息,并在这之后为每个请求添加Cookie
请求头部,类似下面:
1 | GET /index.html |
构成
cookie由浏览器保存的以下几块信息构成。
- 名称
唯一确定cookie的名称,不区分大小写(所以myCookie
和MyCookie
被认为是同一个cookie),必须被URL编码。 - 值
存储在cookie中的字符串值,必须被URL编码。 - 域
cookie有效的那个域,如果没有明确设定,那么就是设置cookie的那个域。这个值可以包含子域(如www.wrox.com
),也可不包含子域(如.wrox.com
,这个对wrox.com
的所有子域都有效)。 - 路径
cookie有效的那个路径。比如可以指定cookie只有从http://www.wrox.com/books/
中才能访问,那么向http://www.wrox.com
页面发送请求就不会发送cookie信息。 - 失效时间
表示cookie何时应该被从浏览器删除的时间戳,默认情况下,浏览器会在会话结束时将所有cookie删除。这个值是GMT格式的日期(Wdy, DD-Mon-YYYY HH:MM:SS GMT)。 - 安全标志
指定后,cookie只有在使用SSL连接的时候才发送到服务器。例如cookie信息只能发送给https://www.wrox.com
,而发往http://www.wrox.com
的请求不能带cookie。
1 | 200 OK |
上面的例子中,指定了一个叫做name
的cookie,它会在格林威治时间2007年1月27如7:10:24失效,同时对于www.wrox.com
和wrox.com
的任何子域,及这些域名下的所有页面(由path
参数指定的)都有效。因为设置了secure
,这个cookie只能通过SSL连接才能传输。
要注意的是,域、路径、失效时间、secure
标志都是服务器给浏览器的指示,以指定何时应该发送cookie。这些参数并不会作为发送到服务器的cookie信息的一部分,只有键值对才会被发送。
限制
cookie在性质上是绑定在特定域名下的,这个限制确保了cookie中的信息只能让允许的接受者(服务器端)访问,而无法被其它域访问。还有一些额外的限制,用于防止cookie被恶意使用,同时不会占用太多磁盘空间,下面就是限制。
- 每个域名下的cookie数限制
- IE6前:20个
- IE7后:50个
- Firefox:50个
- Opera:30个
- Safari和Chrome:没有硬性规定
- cookie尺寸限制:大多为4096B,最好将整个cookie长度限制在4095B内
当超过域名个数限制还设置cookie,浏览器就会清除以前设置的cookie;当尝试创建超过最大尺寸限制的cookie,该cookie会被悄无声息地丢掉。
API
在JavaScript中处理cookie比较复杂,因为接口比较蹩脚,即BOM的document.cookie
。当用来获取属性值时,会返回所有cookie;当用于设置属性值时,会将新cookie添加到现有的cookie集合中,而不是覆盖cookie,除非设置的cookie名称已经存在。没有直接删除cookie的方法,一般都通过重置为""
来实现。
1 | // 获取cookie |
子cookie
是为了绕开浏览器的单域名下cookie数限制而产生的方法。方法是使用一个cookie值来存储多个cookie键值对,一般会用query string格式来进行格式化。下面就是子cookie常见的格式:
1 | name=name1=value1&name2=value2&name3=value3&name4=value4 |
* 关于cookie的思考
还有一类cookie被称为HTTP专有cookie,它可以从浏览器或服务器设置,但只能从服务器端读取,因为JavaScript无法获取HTTP专有cookie的值。由于所有cookie都放在请求头,所以cookie信息越大,请求的时间就越长,因此cookie不适合存储大量信息。
IE userData
IE5中,微软通过一个自定义行为引入了持久化用户数据的概念。userData
默认可跨会话持久存在,不会过期。
使用方法
先使用CSS在某个元素上指定userData
。
1 | <div stype="behavior:url(#default#userData)" id="dataStore"></div> |
设置好后就可以使用setAttribute()
来保存数据了,还要用save()
来将数据提交到浏览器缓存中。
1 | const dataStore = document.getElementById("dataStore"); |
下一次页面载入之后,可以通过load()
和getAttribute()
从中获取数据。
1 | dataStore.load("BookInfo"); |
当然,也可以用removeAttribute()
来删除数据。删除之后,要调用save()
来提交修改。
1 | dataStore.removeAttribute("name"); |
限制
要访问某个数据空间,脚本运行的页面必须来自同一个域、在同一个路径下、并使用与进行存储的脚本同样的协议。允许每个文档最多128KB、每个域名最多1MB数据。
WebStorage
主要是为了克服cookie带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器。Web Storage的两个目标是:
- 提供一种在cookie之外存储会话数据的途径
- 提供一种存储大量可跨会话存在的数据的机制
最初的Web Storage规范包含了两种对象的定义:sessionStorage
和globalStorage
,它们都是window
对象的属性。对它们存储空间的限制,都是以每个来源(协议、域、端口)为单位的,每个来源都有固定的空间,一般限制在2.5~5MB,因浏览器而异。
Storage类型
也是存键值对,只能存字符串,有这么些属性方法。
length
:键值对数量clear()
:删除所有值getItem(name)
:获取键为name
的值setItem(name)
:设置键为name
的值removeItem(name)
:删除键为name
的键值对key(index)
:获取index
位置处的键
Storage事件
Storage
对象有一个storage
事件,当使用setItem()
、removeItem()
、clear()
等操作时都会触发它。它的event
有这么些属性:
domain
:发生变化的存储空间的域名key
:设置或删除的键名newValue
:如果是设置值,则是新值;如果是删除键,则是null
oldValue
:键被更改之前的值
sessionStorage对象
是Storage
的一个实例。特点是:不能跨会话存储,存储某个会话的数据直到浏览器关闭,不过刷新后依然存在。由于对象绑定于某个服务器会话,所以当文件在本地运行时不可用。其中数据只能由最初给对象存储数据的页面访问到,所以对多页面应用有限制。
1 | // 设置(2种方法) |
globalStorage对象
globalStorage
自己不是Storage
实例,不过它的每一个属性都是Storage
的实例。特点是:可以跨越会话存储数据,不过有特定的访问限制(同源限制)。只要不手动删除,其中的数据会一直保留在磁盘上,适合在客户端存储文档或长期保存用户偏好。
1 | globalStorage["wrox.com"].name = "Nicholas"; |
localStorage对象
在HTML5规范中取代了globalStorage
,相当于globalStorage[location.host]
。特点是:不能指定访问规则因为已事先设定好了,也有同源限制,也能够将数据保存直到通过JavaScript删除或用户清除浏览器缓存。
1 | localStorage.setItem("name", "Nicholas"); |
IndexedDB
是浏览器中保存结构化数据的一种数据库。它的操作完全是异步进行的,所以一般需要注册onerror
和onsuccess
事件处理程序来处理结果。
数据库
IndexedDB就是数据库,特色是使用对象而不是表来保存数据,常规操作如下。默认情况下无版本号,可以用setVersion()
指定版本号。
1 | let request, db; |
错误码有下面的取值:
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):磁盘空间不足
对象存储空间
连好数据库后,就可以使用对象存储空间(object storage)了。
1 | /* |
事务
创建好对象存储空间后,接下来所有操作都通过事务来完成。
1 | /* 创建事务 */ |
游标
使用事务可以直接通过已知的键检索单个对象,检索多个对象则需要在事务内创建游标。
基本操作
IDBCursor
实例有下面几个属性:
direction
:游标移动的方向IDBCursor.NEXT
(0):表示下一项,为默认值IDBCursor.NEXT_TO_DUPLICATE
(1):表示下一个不重复的项IDBCursor.PREV
(2):表示前一项IDBCursor.PREV_NO_DUPLICATE
(3):表示前一个不重复的项
key
:对象的键value
:实际的对象primaryKey
:游标使用的键,可能是对象键,可能是索引键
1 | // 创建游标 |
使用游标可以通过update()
更新值,通过delete()
删除值,使用它们都会创建新请求并返回。
1 | request.onsuccess = function(event) { |
默认情况下,每个游标只发起一次请求。如果想发起另一次请求,必须调用下面的方法。
continue(key)
:参数可选。当不指定key
时,游标移动到下一项;当指定key
时,游标移动到指定键的位置advance(count)
:向前移动count
项
1 | request.onsuccess = function(event) { |
键范围
游标查找非常有限,所以提出以键来选定范围,由IDBKeyRange
的实例来表示,有4种定义方式。
1 | const IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange; |
定义完键范围后,把它传给openCursor()
,就可以得到符合约束条件的游标。
1 | let store = db.transaction("users").objectStore("users"), |
上面这个例子就可以输出从键"007"
到键"ace"
的对象。
设定方向
openCursor()
可以接收2个参数:第一个是键范围(如上),第二个是方向(之前提到过几个取值)。
1 | const IDBCursor = window.IDBCursor || window.webkitIDBCursor; |
索引
需要用多个键存取的情况下,可以考虑将一个作为主键,另一个作为索引。用IDBIndex
实例来表示,它有下面的属性。
name
:索引的名字keyPath
:传入createIndex()
中的属性路径objectStore
:索引的对象存储空间unique
:索引键是否唯一
1 | // 创建索引,参数为:索引名、索引的属性名、选项对象 |
索引和对象存储空间相似,相似的操作如下。
1 | const store = db.transaction("users").objectStore("users"); |
并发问题
虽然IndexedDB提供的是异步API,但仍然存在并发操作的问题。
比如,浏览器两个不同的标签页打开了同一个页面,页面a有可能试图用setVersion()
更新页面b尚未准备就绪的数据库,解决办法是b监听事件,当onversionchange
的时候用db.close()
关闭数据库以保证版本顺利更新完成。
再比如,页面a有可能试图用setVersion()
更新页面b上已经打开的数据库,解决办法是a监听事件,当onblocked
的时候弹框提醒用户关闭其它标签页。
限制
IndexedDB数据库有如下限制。
- 只能通过同源页面操作,不能跨域共享信息
- 每个来源的数据库有空间限制,Chrome是5MB,Firefox是50MB
- Firefox限制不允许本地文件访问IndexedDB