本章内容
cookie
浏览器存储 API
IndexedDB
文章目录
1 cookie
HTTP cookie 通常也叫作 cookie,最初用于在客户端存储会话信息。这个规范要求服务器在响应HTTP 请求时,通过发送 Set-Cookie HTTP 头部包含会话信息。例如,下面是包含这个头部的一个 HTTP响应:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value
这个 HTTP 响应会设置一个名为"name",值为"value"的 cookie。名和值在发送时都会经过 URL编码。浏览器会存储这些会话信息,并在之后的每个请求中都会通过 HTTP 头部 cookie 再将它们发回服务器,比如:
GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value
这些发送回服务器的额外信息可用于唯一标识发送请求的客户端。
1.1 限制
因为 cookie 存储在客户端机器上,所以为保证它不会被恶意利用,浏览器会施加限制。同时,cookie也不会占用太多磁盘空间。通常,只要遵守以下大致的限制,就不会在任何浏览器中碰到问题:
不超过 300 个 cookie;
每个 cookie 不超过 4096 字节;
每个域不超过 20 个 cookie;
每个域不超过 81 920 字节。
每个域能设置的 cookie 总数也是受限的,但不同浏览器的限制不同。
1.2 cookie 的构成
cookie 在浏览器中是由以下参数构成的。
名称:唯一标识 cookie 的名称。
值:存储在 cookie 里的字符串值。这个值必须经过 URL 编码。
域:cookie 有效的域。发送到这个域的所有请求都会包含对应的 cookie。
路径:请求 URL 中包含这个路径才会把 cookie 发送到服务器。例如,可以指定 cookie 只能由http://www.wrox.com/books/访问,因此访问 http://www.wrox.com/下的页面就不会发送 cookie,即使请求的是同一个域。
过期时间:表示何时删除 cookie 的时间戳(即什么时间之后就不发送到服务器了)
安全标志:设置之后,只在使用 SSL 安全连接的情况下才会把 cookie 发送到服务器。
这些参数在 Set-Cookie 头部中使用分号加空格隔开,比如:
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com
1.3 JavaScript 中的 cookie
JavaScript 中处理 cookie 比较麻烦,因为接口过于简单,只有 BOM 的 document.cookie 属性。设置 document.cookie 不会覆盖之前存在的任何 cookie,除非设置了已有的cookie。
document.cookie = encodeURIComponent("name") + "=" +
encodeURIComponent("Nicholas");
1.4 子 cookie
为绕过浏览器对每个域 cookie 数的限制,有些开发者提出了子 cookie 的概念。子 cookie 是在单个cookie 存储的小块数据,本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对。最常用的子 cookie模式如下:
name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5
子 cookie 的格式类似于查询字符串。这些值可以存储为单个 cookie,而不用单独存储为自己的名/值对。结果就是网站或 Web 应用程序能够在单域 cookie 数限制下存储更多的结构化数据。
要操作子 cookie,就需要再添加一些辅助方法。解析和序列化子 cookie 的方式不一样,且因为对子cookie 的使用而变得更复杂。比如,要取得某个子 cookie,就需要先取得 cookie,然后在解码值之前需要先找到子 cookie。
如果实际开发中担心碰到每个域的 cookie 限制,则可以考虑使用子 cookie 这个方案。此时要特别注意 cookie 的大小,不要超过对单个 cookie 大小的限制。
1.5 使用 cookie 的注意事项
还有一种叫作 HTTP-only 的 cookie。HTTP-only 可以在浏览器设置,也可以在服务器设置,但只能在服务器上读取,这是因为 JavaScript 无法取得这种 cookie 的值。
因为所有 cookie 都会作为请求头部由浏览器发送给服务器,所以在 cookie 中保存大量信息可能会影响特定域浏览器请求的性能。保存的 cookie 越大,请求完成的时间就越长。即使浏览器对 cookie 大小有限制,最好还是尽可能只通过 cookie 保存必要信息,以避免性能问题。
注意 不要在 cookie 中存储重要或敏感的信息。cookie 数据不是保存在安全的环境中,因此任何人都可能获得。应该避免把信用卡号或个人地址等信息保存在 cookie 中。
2 Web Storage
Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。
提供在 cookie 之外的存储会话数据的途径;
提供跨会话持久化存储大量数据的机制。
Web Storage 的第 2 版定义了两个对象:localStorage 和 sessionStorage。localStorage 是
永久存储机制,sessionStorage 是跨会话的存储机制。这两种浏览器存储 API 提供了在浏览器中不
受页面刷新影响而存储数据的两种方式。
2.1 Storage 类型
Storage 类型用于保存名/值对数据,直至存储空间上限(由浏览器决定)。Storage 的实例与其他对象一样,但增加了以下方法
clear():删除所有值;不在 Firefox 中实现。
getItem(name):取得给定 name 的值。
key(index):取得给定数值位置的名称。
removeItem(name):删除给定 name 的名/值对。
setItem(name, value):设置给定 name 的值。
2.2 sessionStorage 对象
sessionStorage 对象只存储会话数据,这意味着数据只会存储到浏览器关闭。这跟浏览器关闭时会消失的会话 cookie 类似。存储在 sessionStorage 中的数据不受页面刷新影响。
/ 使用方法存储数据
sessionStorage.setItem("name", "Nicholas");
// 使用属性存储数据
sessionStorage.book = "Professional JavaScript";
2.3 localStorage 对象
要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议。
// 使用方法存储数据
localStorage.setItem("name", "Nicholas");
// 使用属性存储数据
localStorage.book = "Professional JavaScript";
// 使用方法取得数据
let name = localStorage.getItem("name");
// 使用属性取得数据
let book = localStorage.book;
存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览器而丢失
2.4 存储事件
每当 Storage 对象发生变化时,都会在文档上触发 storage 事件。使用属性或 setItem()设置值、使用 delete 或 removeItem()删除值,以及每次调用 clear()时都会触发这个事件。这个事件的事件对象有如下 4 个属性。
domain:存储变化对应的域。
key:被设置或删除的键。
newValue:键被设置的新值,若键被删除则为 null。
oldValue:键变化之前的值。
// storage 事件不会区分sessionStorage、localStorage
window.addEventListener("storage",
(event) => alert('Storage changed for ${event.domain}'));
2.5 限制
具体的限制取决于特定的浏览器。一般来说,客户端数据的大小限制是按照每个源(协议、域和端口)来设置的,因此每个源有固定大小的数据存储空间。分析存储数据的页面的源可以加强这一限制。不同浏览器给 localStorage 和 sessionStorage 设置了不同的空间限制,但大多数会限制为每个源 5MB。
3 IndexedDB
IndexedDB是浏览器中存储结构化数据的一个方案,IndexedDB 背后的思想是创造一套 API,方便 JavaScript 对象的存储和获取,同时也支持查询和搜索。
IndexedDB 的设计几乎完全是异步的。为此,大多数操作以请求的形式执行,这些请求会异步执行,产生成功的结果或错误。绝大多数 IndexedDB 操作要求添加 onerror 和 onsuccess 事件处理程序来确定输出。
3.1 数据库
与传统数据库最大的区别在于,IndexedDB 使用对象存储而不是表格保存数据。
使用 IndexedDB 数据库的第一步是调用 indexedDB.open()方法,并给它传入一个要打开的数据库名称。如果给定名称的数据库已存在,则会发送一个打开它的请求;如果不存在,则会发送创建并打开这个数据库的请求。这个方法会返回 IDBRequest 的实例,可以在这个实例上添加 onerror 和onsuccess 事件处理程序。举例如下:
let db,
request,
version = 1;
request = indexedDB.open("admin", version);
request.onerror = (event) =>
alert(`Failed to open: ${event.target.errorCode}`);
request.onsuccess = (event) => {
db = event.target.result;
};
3.2 对象存储
建立了数据库连接之后,下一步就是使用对象存储。假设要存储包含用户名、密码等内容的用户记录。可以用如下对象来表示一条记录:
let user = {
username: "007",
firstName: "James",
lastName: "Bond",
password: "foo"
};
如果数据库还不存在,open()操作会创建一个新数据库,然后触发 upgradeneeded 事件。
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 如果存在则删除当前 objectStore。测试的时候可以这样做
// 但这样会在每次执行事件处理程序时删除已有数据
if (db.objectStoreNames.contains("users")) {
db.deleteObjectStore("users");
}
db.createObjectStore("users", { keyPath: "username" });
};
3.3 事务
创建了对象存储之后,剩下的所有操作都是通过事务完成的。事务要通过调用数据库对象的transaction()方法创建。任何时候,只要想要读取或修改数据,都要通过事务把所有修改操作组织起来。最简单的情况下,可以像下面这样创建事务:
// 访问多个对象存储
let transaction = db.transaction(["users", "anotherStore"]);
有了事务的引用,就可以使用 objectStore()方法并传入对象存储的名称以访问特定的对象存储。然后,可以使用 add()和 put()方法添加和更新对象,使用 get()取得对象,使用 delete()删除对象,使用 clear()删除所有对象。其中,get()和 delete()方法都接收对象键作为参数,这 5 个方法都创建新的请求对象。来看下面的例子:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
request = store.get("007");
request.onerror = (event) => alert("Did not get the object!");
request.onsuccess = (event) => alert(event.target.result.firstName);
.3.4 插入对象
// users 是一个用户数据的数组
let request,
requests = [];
for (let user of users) {
request = store.add(user);
request.onerror = () => {
// 处理错误
};
request.onsuccess = () => {
// 处理成功
};
requests.push(request);
}
3.5 通过游标查询
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
request = store.openCursor();
request.onsuccess = (event) => {
// 处理成功
};
request.onerror = (event) => {
// 处理错误
};
3.6 键范围
使用游标会给人一种不太理想的感觉,因为获取数据的方式受到了限制。使用键范围(key range)可以让游标更容易管理。键范围对应 IDBKeyRange 的实例。