客户端存储
cookie
HTTP cookie 通常也叫做 cookie,最初用于在客户端储存绘画信息;这个规范要求服务器在响应 HTTP 请求时,通过发送 Set-Cookie HTTP 头部包含会话信息
名和值在发送时都会经过 URL 编码;浏览器会储存这些会话信息,并在之后的每个请求中都会通过 HTTP 头部 cookie 再将它们发回服务器
这些发送回服务器的额外信息可用于唯一标识发送请求到客户端
限制
cookie 是与特定域绑定的;设置 cookie 后,它会与请求一起发送到创建它的域;这样可以保证 cookie 中储存的信息只对被认可的接收者开放,不会被其它域访问
浏览器会对储存在客户端机器上的 cookie 加以限制,cookie 也不会占用太多磁盘空间
一般要遵守以下大致的限制,就不会在任何浏览器中碰到问题:
不超过 300 个 cookie
每个 cookie 不超过 4096 字节
每个域不超过 20 个 cookie
每个域不超过 81920 字节
每个浏览器对每个域能设置的 cookie 数量有限制;如果超出了单个域最大容量,浏览器就会删除之前的 cookie,但是一般不要超出限制
浏览器也会限制 cookie 的大小,大多数浏览器的限制是不超过 4096 字节,上下可以有一个字节的误差;为保证兼容,最好不要超过 4095 字节,这个限制适用于一个域所有 cookie 而不是单个 cookie
如果创建的 cookie 超过最大限制,则该 cookie 会静默删除
cookie 的构成
名称:唯一标识 cookie 的名称;不区分大小写,但是服务器可能区分,所以可以在实践中区分大小写
值:储存在 cookie 里的字符串值;这个值必须经过 URL 编码
域:cookie 有效的域;发送到这个域所有请求都会包含对应的 cookie;如果不包含子域(如.coding.net),则 cookie 会对所有子域都生效
路径:请求 URL 中包含这个路径才会把 cookie 发送到服务器
过期时间:表示何时删除 cookie 的时间戳;这个值是 GMT 格式;默认情况下浏览器会话结束后会立即删除;cookie 可以保存在机器上,即使关闭浏览器;把过期时间设置为过去的时间会立即删除 cookie
安全标志:设置之后,只在使用 SSL 安全连接的情况下才会把 cookie 发送到服务器
这些参数在 Set-Cookie 头部中使用分号加空格隔开,例如:
Set-Cookie: name=value; expires=Mon, 22-Jan-07 11:11:11 GMT; domain=.wrox.com
安全标志 secure 是 cookie 中唯一的非名/值对,只需要一个 secure 就可以了,例如:
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
JavaScript 中的 cookie
在 js 中处理 cookie 比较麻烦,只有 BOM 的 document.cookie 属性可以访问
使用该接口获取值时,document.cookie 返回包含页面所有有效 cookie 的字符串,以分号分隔,例如:
name1=value1;name2=value2
所有名和值都是 URL 编码的,因此必须使用 decodeURIComponent()解码
在设置值时,通过 document.cookie 设置新的 cookie 字符串;设置 cookie 不会覆盖之前存在的任何 cookie,除非设置了已有的 cookie;格式和 Set-Cookie 头格式一样
所有的参数中,只有名称和值是必要的,例如:document.cookie = "name=lsn";
;这个例子中的 cookie 会在客户端向服务器发送请求时都被带上,浏览器关闭动时候立即删除,虽然这个例子不需要编码任何字符,但是最好使用 encodeURIComponent()对名称和值进行编码,例如:document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("lsn");
js 中的 cookie 操作很麻烦,一般通过一个辅助函数进行操作,有增、查、删操作,详情看红宝书 p754
子 cookie
为绕过浏览器对每个域 cookie 数的限制,有些开发者提出了子 cookie 的概念;是在单个 cookie 中储存的小块数据,本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对,例如:
name=name1=value1&name2=value2&name3=value3
这样便可以储存更多的 cookie,但是操作子 cookie,就得先取得 cookie,操作函数需要进一步简化,详情查看红宝书 p754
使用 cookie 的注意事项
还可以设置 HTTP-only 的 cookie,可以在浏览器设置,也能在服务器设置,但是只能在服务器上获取;js 无法获取这种 cookie 的值
cookie 不是储存大量数据的理想方式
不要在 cookie 中储存重要或铭感信息,因为 cookie 数据不是保存在安全的环境中,因此任何人都有可能获得
Web Storage
Web Storage 规范最新版本是第二版,这一规范主要有两个目标:
提供在 cookie 之外的存储会话数据的途径
提供跨会话持久化存储大量数据的机制
这一版本定义了两个对象:localStorage、sessionStorage;前者是永久储存机制,后者是跨会话储存机制;这两种机制提供了不受页面刷新影响的储存数据的方式;这两个对象都在 window 上实现了
Storage 类型
该类型用于保存名/值对数据,直至储存空间上限(浏览器决定);该类型实例还增加了以下方法:
clear():删除所有值(Firefox 未实现)
getItem(name):取得给定 name 的值
key(index):取得给定数值位置的名称
removeItem(name):删除给定 name 的名/值对
setItem(name, value):设置给定 name 值
因为每个数据项都作为属性存储在该对象上,所以可以使用点或方括号操作符访问这些属性;通过 length 属性可以确定 Storage 对象中保存了多少名/值对
Storage 类型只能存储字符串,非字符串数据会在存储之前会自动转换为字符串,这个转换在取值时不能撤销
sessionStorage 对象
该对象只存储会话数据,这意味着数据只会存储到浏览器关闭,不受页面刷新影响,可以在浏览器崩溃并重启后恢复(取决于浏览器)
本地文件不能使用,只能由最初存储数据的页面使用,在多页应用程序中用处有限
该对象是 Storage 的实例,所以能用 Storage 上的方法
所有现代浏览器在实现存储写入时都使用了同步阻塞方式,因此数据会被立即提交到储存;通过 Web Storage 写入到任何数据都可以立即被读取;老版 IE 在这个写入方式上又有区别(详情看红宝书 p760)
可以通过 length 或 for-in 遍历 sessionStorage
可以使用 delete 直接删除对象属性,或者使用 removeItem 方法删除
localStorage 对象
在修订的 HTML5 规范里,localStorage 对象取代了 globalStorage,作为在客户端持久存储数据的机制;要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议
该对象也是 Storage 实例,可以使用 Storage 上有的方法
存储事件
每当 Storage 对象发生变化时,会在文档上触发 storage 事件,使用属性或者 Storage 实例方法时都会触发
该事件 event 对象有 4 个属性:
domain:存储变化对应的域
key:被设置或删除的键
newValue:键被设置的新值,若键被删除则为 null
oldValue:键变化之前的值
可以使用 DOM Level 2 方法添加事件处理程序
对于 sessionStorage 和 localStorage 上任何改变都会触发,但是 storage 事件不会区分两者
限制
具体的限制取决于浏览器;一般来说,客户端数据的大小限制是按照每个源来设置的,每个源有固定大小的数据存储空间
大多数会限制为每个源 5MB
IndexedDB
Indexed DataBase API,是浏览器中存储结构化数据的一个方案
IndexedDB 背后的思想是创造一套 API,方便 js 对象的存储和获取,同时也支持查询和搜索
IndexedDB 的设计几乎完全是异步的;绝大多数 IndexedDB 要求添加 onerror 和 onsuccess 事件处理程序来确定输出
数据库
IndexedDB 是类似于 MySQL 或 Web SQL Database 的数据库;但是 IndexedDB 使用的是对象存储而不是表格保存数据
IndexedDB 数据库就是在一个公共命名空间下的一组对象储存,类似于 NoSQL 风格的实现
使用 indexedDB.open()方法,传入一个要打开的数据库名和版本;如果数据库已存在,则会发送一个打开它的请求,如果不存在,则会发送创建并打开这个数据库的请求;该方法返回一个 IDBRequest 实例,可以在这个实例上添加 onerror 和 onsuccess 事件处理程序
let db,
request,
version = 1;
request = indexedDB.open("admin", version);
request.onerror = (event) => {
alert(event.target.errorCode);
};
request.onsuccess = (event) => {
db = event.target.result;
};
打开数据库时要指定版本,这个版本号会转换为 unsigned long long 数值,所以要使用整数,不要使用浮点数
成功打开后所有与该数据库相关的操作都要通过 db 对象本身来进行;打开错误可以通过 errorCode 访问错误码
对象储存
数据库版本决定了数据库模式,包括数据中的对象存储和这些对象存储的结构;如果数据库不存在,open()操作会创建一个新数据库,然后出发 upgradeneeded 事件;可以为这个事件设置处理程序,并在程序中创建数据库模式;如果数据库存在,而你指定了一个升级版的版本号,则会立即触发 upgradeneeded 事件,然后更新数据库模式
下面例子演示了如何创建对象存储:
request.onupgradeneeded = (event) => {
const db = event.target.result;
//删除当前objectStore,测试的时候可以这样做
//但是这样会删除已有数据
if (db.objectStoreNames.contains("users")) {
db.deleteObjectStore("users");
}
db.createObjectStore("users", { keyPath: "username" });
};
这里 keyPath 参数表示作为键的存储对象的属性名
事务
创建了对象存储后,剩下的所有操作都是通过事务完成的;事务要通过调用数据库对象的 transaction()方法创建;任何时候想要读取或修改数据,都要通过事务把所有修改操作组织起来
最简单的情况:
let transaction = db.transaction();
//不指定参数,则会对数据库中所有对象存储只有读取权限
let t = db.transaction("users");
//这样可以确保事务期间只加载users对象存储信息
let t = db.transaction(["users", "anotherStore"]);
//这样可以访问多个对象存储
//上述三种方式都只读
//访问模式通过第二个参数修改,有三个模式:“readonly”、“readwrite”、“versionchange”
let t = db.transaction("users", "readwrite");
有了事务的引用,就可以用 objectStore()方法并传入对象存储的名称以访问特定的对象存储
然后使用 add()和 put()方法添加和更新对象,get()方法取得对象,delete()方法删除对象,clear()方法删除所有对象;get()和 delete()方法都接收对象键作为参数,这五个请求都创建新的请求对象
const t = db.transaction("users"),
store = transaction.objectStore("users"),
request = store.get("007");
request.onerror = (event) => alert("error");
request.onsuccess = (event) => alert(event.target.result.firstName);
一个事务可以完成多个请求,所以事务本身也有事件处理程序:onerror、oncomplete
t.onerror = (event) => {
//事务被取消
};
t.oncomplete = (event) => {
//整个事务成功完成
};
不能通过 oncomplete 事件处理程序的 event 访问 get()请求返回的任何数据
插入对象
拿到对象存储的引用后就可以使用 add()和 put()写入数据了;这两个方法都接受一个参数,及要存储的对象,并把对象保存到对象存储
当对象存储中存在同名键时,add()会报错,put()会重写该对象;在其它时候这两个方法没有区别
每次调用这两个方法都会创建对象存储的更新请求;可以把请求对象保存起来,然后用 onerror 和 onsuccess 事件处理程序验证是否成功
let request,
requests = [];
for (let user of users) {
request = store.add(user);
request.onerror = (event) => {
//...
};
request.onsuccess = (event) => {
//...
};
requests.push(request);
}
创建并填充数据后,可以查询对象存储了
通过游标查询
使用事务可以通过一个已知键取得一条记录;如果想取得多条数据,则需要在事务中创建一个游标;游标是一个指向结果集的指针
游标不会先收集所有数据,而是指向第一个结果,并且在接到指令前不会主动查找下一条数据
需要在对象储存上调用 openCursor()方法创建游标,方法返回一个一个请求,必须为它添加 onsuccess 和 onerror 事件处理程序
const t = db.transaction("users"),
store = t.objectStore("users"),
request = store.openCursor();
request.onsuccess = (event) => {
//...
};
request.onerror = (event) => {
//...
};
调用 onsuccess 事件处理程序,可以通过 event.target.result 访问对象储存中下一条记录,这个属性中保存着 IDBCursor 实例或 null;这个 IDBCursor 实例有几个属性:
direction:字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值;有如下值:NEXT("next")、NEXTUNIQUE("nextunique")、PREV("prev")、PREVUNIQUE("prevunique")
key:对象的键
value:实际的对象
primaryKey:游标使用的键;可能是对象键或者索引键
游标可用于更新个别记录,update()方法使用指定的对象更新当前游标对应的值,该方法回创建一个新的请求,可以通过 onsuccess 和 onerror 事件处理程序观察结果
request.onsuccess = (event) => {
const cursor = event.target.result;
let value, updateRequest;
if (cursor) {
//永远要检查
value = cursor.value;
value.password = "magic";
updateRequest = cursor.update(value);
updateRequest.onsuccess = () => {
//...
};
updateRequest.onerror = () => {
//...
};
}
};
也可以调用 delete()来删除游标位置的记录,与 update 一样,也会创建一个请求
如果事务没有修改对象存储的权限,update()和 delete()都会抛出错误
如果要访问新记录可以使用两个方法:
continue(key):移动到结果集中下一条记录;key 是可选的,如果没有指定,游标就移动到下一条记录;如果指定了,则移动到指定的键
advance(count):游标向前移动指定的 count 条记录
这两个方法都会让游标重用相同的请求,因此也会重用 onsuccess 和 onerror 处理程序
键范围
使用键范围可以让游标更容易管理;键范围对应 IDBKeyRange 实例;有四种方法指定键范围:
only(key):传入想要获取的键,这个方法和 get()差不多
lowerBound(key, flag):定义结果集的下限,表示游标从 key 位置开始,直到最后;可选参数 flag 是一个 Boolean 值,表示是否跳过游标位置 key,true 表示从 key 后面一条记录开始,不传或传 false 则相反
upperBound(key, flaf):定义结果集的上限,表示游标从开头位置开始,直到 key 位置;可选参数 flag 是一个 Boolean 值,表示是否包含游标位置 key,true 表示到 key 前面一条记录,不传或传 false 则相反
bound(lowerKey, upperKey, lowerFlag, upperFlag):结合上述两个方法的方法,同时定义上限和下限
将定义好后的范围传给 openCursor()方法,就可以获得该范围的游标
const store = db.transaction("users").objectStore("users"),
range = IDBKeyRange.bound("007", "ace"),
request = store.openCursor(range);
设置游标方向
openCursor()方法实际上接收两个参数:IDBKeyRange 实例或 null(传入 null 则默认键范围是所有值)、方向字符串(默认为”next“)
如果需要跳过重复的项,可以在第二个参数传入“nextunique”
如果需要反向移动的游标,从最后一项开始向第一项移动,需要传“prev”或“prevunique”作为第二个参数;这样子会使得 continue()和 advance()在对象储存中反向移动游标
索引
对于某些数据集,可能需要为对象储存创建多个键
要创建新的索引,首先要取得对象储存的引用,调用 createIndex()方法:
const index = store.createIndex("username", "username", { unique: true });
createIndex()方法的第一个参数是索引的名称,第二个参数是索引属性的名称,第三个参数是包含键 unique 的 options 对象;该方法返回 IDBIndex 实例,在储存对象上调用一个 index()方法也可以得到同一个实例
let index = store.index("username");
索引非常像对象存储,可以在索引上调用 openCursor()方法创建新游标,创建的游标和在对象储存上调用 openCursor()创建的游标完全一样;不过 result.key 属性中保存的是索引键,而不是主键
let index = store.index("username");
let request = index.openCursor();
使用 openKeyCursor()方法也可以在索引上创建特殊游标,只返回每条记录的主键;这个方法接收参数与 openCursor()一样;不同之处在于 event.result.key 是索引键,event.result.value 是主键而不是整个记录
可以使用 get()方法并传入索引键通过索引取得单条记录,这会创建一个请求
如果想只取得给定索引键的主键,可以使用 getKey()方法,这会创建一个新请求,但 result.value 等于主键而不是整个记录
IDBIndex 对象有下列属性:
name:索引的名称
keyPath:调用 createIndex()时传入的属性路径
objectStore:索引对应的对象储存
unique:表示索引键是否唯一的布尔值
对象存储自身也有一个 indexNames 属性,保存着与之相关索引的名称
在存储对象上调用 deleteIndex()方法并传入索引的名称可以删除索引
并发问题
IndexDB 虽然是异步 API,但仍存在并发问题
两个不同的浏览器标签页同时打开同一个网页,有可能出现一个尝试升级数据库而另一个尚未就绪的情形;有问题的操作是设置数据库为新版本,而版本变化只能在浏览器只有一个标签页使用数据库时才能完成
第一次打开数据库时,添加 onversionchange 事件处理程序非常重要,另一个同源标签页将数据库打开到新版本时,将执行此回调;对这个事件最好的回应是立即关闭数据库,以便完成版本升级
let request, db;
request = indexDB.open("admin", 1);
request.onsuccess = (event) => {
db = event.target.result;
db.onversionchange = () => db.close();
};
应该在每次成功打开数据库之后都指定 onversionchange 事件处理程序,这个事件可能会被其它标签页触发
限制
IndexedDB 很多限制实际上与 Web Storage 一样;IndexedDB 数据库是与页面源绑定的,因此不能跨域共享
其次,每个源都有可以储存的空间限制
不同浏览器有不同区别,详情请看红宝书 p771