《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:如果是设置值,则是新值;如果是删除键,则是nulloldValue:键被更改之前的值
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