【重走JavaScript之高级程序设计】客户端存储
1 cookie
🚀 站内跳转,WebStorage 本地存储之间的区别及使用
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。客户端会存储这些会话信息,并在之后每个请求中都会通过 HTTP 头部 cookie 将它们发回服务器
GET /index.jsl
HTTP/1.1
Cookie: name=value
Other-header: other-header-value
cookie 是与特定域绑定的。设置 cookie 后,它会与请求一起发送到创建它的域。这个限制能保证 cookie 中存储的信息只对被认可的接收者开放,不被其他域访问。
因为 cookie 存储在客户端机器上,所以为保证它不会被恶意利用,浏览器会施加限制。同时,cookie 也不会占用太多磁盘空间。
1.1 cookie 的限制
cookie 需遵守以下限制
- 不超过 300 个 cookie;
- 每个 cookie 不超过 4096 字节;
- 每个域不超过 20 个 cookie;
- 每个域不超过 81920 字节。
- 每个域能设置的 cookie 总数也是受限的,但不同浏览器的限制不同。例如:
- 最新版 IE 和 Edge 限制每个域不超过 50 个 cookie;
- 最新版 Firefox 限制每个域不超过 150 个 cookie;
- 最新版 Opera 限制每个域不超过 180 个 cookie;
- Safari 和 Chrome 对每个域的 cookie 数没有硬性限制。
还有其他需要注意的点
- 如果 cookie 总数超过上限超过单个域的上限,浏览器会删除之前设置的的 cookie。IE 和 Opera 会按照最近最少使用的原则删除,firefox 随机删除,所以最好不要超出限制。
- 为了保证不同浏览器的兼容性,留有一个字节的误差,顾每个 cookie 不能超过 4095 字节
- 如果创建的 cookie 超过最大限制,则该 cookie 会被静默删除。其中一个字符会占一个字节。如果使用多字节字符(如 UTF-8 Unicode 字符),则每个字符最多可能占 4 个字节。
1.2 cookie 的构成
cookie 在浏览器中是由以下参数构成的。
- 名称: 唯一标识 cookie 的名称。cOOkie 名不区分大小写,因此 mycookie 和 Mycookie 是同个名称。不过,实践中最好将 cookie 名当成区分大小写来对待,因为一些服务器软件可能这对待它们。cookie 名必须经过 URL 编码。
- 值: 存储在 cookie 里的字符串值。这个值必须经过 URL 编码。
- 域: cookie 有效的域。发送到这个域的所有请求都会包含对应的 cookie。这个值可能包含子域(如www.baidu.com )、也可以不包含(如,baidu.com 表示对 baidu.com 的所有子域都有效 )。如果不明确设置,则默认为设置 cookie 的域。
- 路径: 请求 URL 中包含这个路径才会把 cookie 发送到服务器。例如,可以指定 cookie 只能由http://www.baidu.com/books/访问,因此访问 http://www.baidu.com/下的页面就不会发送 cookie,即使请求的是同一个域。
- 过期时间: 表示何时删除 cookie 的时间戳(即什么时间之后就不发送到服务器了)。默认情况下浏览器会话结束后会删除所有 cookie。不过,也可以设置删除 cookie 的时间。这个值是 GMT 格式(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定删除 cookie 的具体时间。这样即使关闭浏览器 cookie 也会保留在用户机器上。把过期时间设置为过去的时间会立即删除 cookie。
- 安全标志: 设置之后,只在使用 SSL 安全连接的情况下才会把 cookie 发送到服务器。例如,请求 https://www.baidu.com 会发送 cookie,而请求 http://www.baidu.com 则不会。
这些参数在 Set-cookie
头部使用分号加空格隔开,比如
HTTP/1.1 200 OK
Content-type: text/htm1
Set-Cookie: namesvalue! expires=mon, 22-Jan-07 07:10:24 GMT; domain=.baidu.com
Other-header: other-header-value
这个头部设置一个名为"name"的 cookie,这个 cookie 在 2007 年 1 月 22 日 7:10:24 过期,对 www.baidu.com 及其他 baidu.com 的子域 (如 p2p.baidu.com) 有效。安全标志 secure 是 cookie 中唯一的非名/值对,只需一个 secure 就可以了。比如:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.baidu.com; path=/;secure
Other-header: other-header-value
这里创建的 cookie 对所有 baidu.com 的子域及该域中的所有页面有效(通过 path=/指定 )。不过这个 cookie 只能在 SSL 连接上发送,因为设置了 secure 标志。
要知道,域、路径、过期时间和 secure 标志用于告诉浏览器什么情况下应该在请求中包含 cookie.这些参数并不会随请求发送给服务器,实际发送的只有 cookie 的名/植对。
1.3 JavaScript 中的 cookie
要使用该属性获取值时, document.cookie
返回包含页面中所有 cookie 的字符串(根据域、路径、过期时间和安全设置),以分号分隔,如下面的例子所示:
name1 = value1;name2=value2;name3=value3;
所有名和值都是 URL 编码的,因此必须使用 decodeURIcomponent()
解码。
在设置值时,可以通过 document.cookie
属性设置新的 cookie 字符串。这个字符串在被解析后会添加到原有 cookie 中。设置 document.cookie
不会覆盖之前存在的任何 cookie,除非设置了已有的 cookie。设置 cookie 的格式如下,与 Set-cookie
头部的格式一样:
name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure
在所有这些参数中,只有 cookie 的名称和值是必需的。下面是个简单的例子: document.cookie = "name=Nicholas";
这行代码会创建一个名为"name"的会话 cookie,其值为"Nicholas"。这个 cookie 在每次客户端向服务器发送请求时都会被带上,在浏览器关闭时就会被删除。虽然这样直接设置也可以,因为不需要在名称或值中编码任何字符,但最好还是使用 encodeURIComponent()
对名称和值进行编码,比如:
document.cookie = encodeURIComponent("name") + "=" +encodeURIComponent("Nicholas");
要为创建的 cookie 指定额外的信息,只要像 Set-cookie
头部一样直接在后面追加相同格式的字符串即可:
document.cookie = encodeURIComponent ("name") + "="encodeURIComponent ("Nicholas") + "; domain=.wrox.com; path=/";
因为在 JavaScript 中读写 cookie 不是很直观,所以可以通过辅助函数来简化相应的操作。与 cookie 相关的基本操作有读、写和删除。这些在 Cookieutil 对象中表示如下:
class CookieUtil {
// get方法用于取得给定名称的cookie值
static get(name) {
let cookieName = `${encodeURIComponent(name)}=`,
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = nell;
if (cookieStart > -1) {
let cookieEnd = document.cookie.indexOf(";", cookieStart);
if (cookieEnd == -1) {
cookieEnd = document.cookie.length;
}
cookieValue = decodeURIComponent(document.cookie.substring(cookieStart, cookieName.length, cookieEnd));
return cookieValue;
}
}
// 用于设置页面上的cookie,接受多个参数。
/*
接收多个参数:cookie 名称、cookie 值、可选的 Date 对象(表示何时删除 cookie )、可选的 URL 路径、可选的域以及可选的布尔值(表示是否加secure标志)。这些参数以它们的使用频率为序,只有前两个是必需的。
*/
static set(name, value, expire, path, domain, secure) {
let 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;
}
// 删除cookie,过期时间设置为 1970 年1月1日(以O毫秒初始化的 Date 对象的值 )。这样可以保证删除 cookie
static unset(name, path, domain, secure) {
CookieUtil.set(name, "", new Date(0), path, domain, secure);
}
}
这些方法通过处理解析和 cookie 字符串构建,简化了使用 cookie 存储数据的操作。
// 设置cookie
CookieUtil.set("name", "Nicholas");
CookieUtil.set("book", "professional Javascript");
// 读取 cookie
alert(CookieUtil.get("name")); // "Nicholas"
alert(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);
1.4 子 cookie
为绕过浏览器对每个域 cookie 数的限制,有些开发者提出了子 cookie的概念。子 cookie是在单个 cookie 存储的小块数据,本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对。最常用的子 cookie模式如下:
name=name1=valuel&name2=value2&name3=value3&name4=value4&name5=value5
子 cookie 的格式类似于查询字符串。这些值可以存储为单个 cookie,而不用单独存储为自己的名/值对。结果就是网站或 Web 应用程序能够在单域 cookie 数限制下存储更多的结构化数据。要操作子 cookie,就需要再添加一些辅助方法。解析和序列化子 cookie的方式不-样,且因为对子 cookie的使用而变得更复杂。比如,要取得某个子 cookie,就需要先取得cookie,然后在解码值之前需要先像下面这样找到子 cookie:
class SubCookieUtil {
// get方法用于取得一个子cookie的值
static get(name, subName) {
let subCookies = SubCookieUtil.getAll(name);
return subCookies ? subCookies[subName] : null;
}
// getAll方法用于取得所有子cookie
static getAll(name) {
let cookieName = `${encodeURIComponent(name)}=`,
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null,
cookieEbd,
subCookies,
parts,
result = {};
if (cookieStart > -1) {
let cookieEnd = document.cookie.indexOf(";", cookieStart);
if (cookieEnd == -1) {
cookieEnd = document.cookie.length;
}
cookieValue = document.cookie.substring(cookieStart, cookieName.length, cookieEnd);
if (cookieValue.length > 0) {
subCookies = cookieValue.split("&");
for (let i = 0; i < subcookies.length; i++) {
parts = subCookies[i].split("=");
result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
}
return result;
}
}
return null;
}
// 用于设置页面上的子cookie,接受多个参数。
/*
接收多个参数:cookie 名称、子cookie的名称cookie 值、可选的 Date 对象用于设置cookie的过期时间、可选的cookie路径、可选的cookie域和可选的布尔值secure标志。
所有可选参数都作用于cookie本身而不是子cookie。为了在同一个 cookie 中存储多个子 cookie,路径、和 secure 标志也必须相同。过期时间作用于整个 cookie,可以在写入个别子 cookie 时另行设置。在这个方法内部,第一步是取得给定 cookie 名称下包含的所有子 cookie。逻辑或操作符(1)在这里用于在 geta11 ()返回 null 的情况下将 subcookies 设置为新对象。然后,在 subcookies 上设cookie 的值,再将参数传给 setA11 ()。
*/
static set(name, subName, value, expire, path, domain, secure) {
let subcookies = SubCookieUtil.getAll(name) || {};
subcookies[subName] = value;
SubCookieUtil.setAll(name, subcookies, expire, path, domain, secure);
}
/*
setal1()方法接收6个参数:cookie 的名称、包含所有子 cookie 的对象,然后是 set()方法中用的4个可选参数。这个方法会在 for-in 循环中迭代第二个参数的属性。为保证只存储合适的数据这里使用了 hasownProperty()方法确保只有实例属性才会序列化为子 cookie。因为存在属性名等空字符串的可能,所以在添加到 subcookieParts 数组之前也要检查属性名的长度。subcookiepart数组包含了子 cookie 的名/值对,这样我们可以方便地使用 join()方法用和号将它们拼接成字符串,下的逻辑与 cookieUtil.set()一样
*/
static setAll(name, subcookies, expire, path, domain, secure) {
let cookieText = `${encodeURIComponent(name)}=`,
subcookieParts = new Array();
for (let subName in subcookies) {
if (subName.length > 0 && subcookies.hasOwnProperty(subName)) {
subcookieParts.push(`${encodeURIComponent(subName)}=${encodeURIComponent(subcookies[subName])}`);
}
subcookieText += `${encodeURIComponent(subName)}=${encodeURIComponent(subcookies[subName])}&`;
}
if (subcookieText.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;
}
// 从cookie中删除一个子cookie
static unset(name, subName, path, domain, secure) {
let subcookies = SubCookieUtil.getAll(name);
if (subcookies) {
delete subcookies[subName]; // 删除
SubCookieUtil.setAll(name, subcookies, null, path, domain, secure);
}
}
// 删除整个cookie
static unsetAll(name, path, domain, secure) {
SubCookieUtil.setAll(name, null, new Date(0), path, domain, secure);
}
}
这些方法通过处理解析和 子 cookie 字符串构建,简化了使用 子 cookie 存储数据的操作。
// 假设 document.cookie=data=name=Nicholas&book=Professional20JavaScript
//设置两个子 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,2010"));
//修改"name"的值并修改整个cookie 的过期时间
SubCookieUtil.set("data", "name", "Michael", new Date("February 1,2010"));
// 只删除"name"子cookie
SubCookieUtil.unget("data", "name");
// 删除整个cookie
SubCookieUti1.unsetA11("data");
1.5 使用 cookie 的注意事项
- HTTP-only 的 cookie。HTTP-only 可以在浏览器设置,也可以在服务器设置,但是只能在服务器上读取,这是因为 JavaScript 无法取得这种 cookie 的值
- 为所有 cookie 都会作为请求头部由浏览器发送给服务器,所以在 cookie 中保存大量信息可能会影响特定域浏览器请求的性能。保存的 cookie 越大,请求完成的时间就越长。即使浏览器对 cookie 大小有限制,最好还是尽可能只通过 cookie 保存必要信息,以避免性能问题。对 cookie 的限制及其特性决定了 cookie 并不是存储大量数据的理想方式。因此,其他客户端存储技术出现了。
- 注意 不要在 cookie 中存储重要或敏感的信息。cookie 数据不是保存在安全的环境中,因此任何人都可能获得。应该避免把信用卡号或个人地址等信息保存在 cookie 中。
2 Web Storage
Web storage 的目的解决客户段存储不需要频繁发送回服务器的数据时使用 cookie 的问题。
Web storage 的目的
- 提供 cookie 之外的存储会话数据的途径。
- 提供跨会话持久化存储大量数据的机器。
Web storage 包含两个对象,两个对象均不受页面刷新而影响。
- localStorage 永久存储机制
- sessionStorage 跨会话存储机制
2.1 storage 的类型
storage 类型用于保存名/值对数据,直至存储空间上限(由浏览器决定)。storage 实例与其他对象一样,但增加了以下方法
clear()
清除所有值。不在 Firefox 中实现getItem(name)
取得给定 name 的值key(index)
取得给定数值位置的名称removeItem(name)
删除给定 name 的名/值对setItem(name,value)
设定给定 name 的值
注意:通过 length 属性可以确定 Storage 对象中保存了多少名/值对。但我们无法确定对象中所有数据占用对空间大小。storage 类型只能存储字符串,非字符串在存储之前会自动转换为字符串。
2.2 sessionStorage 对象
sessionStorage 对象只存储会话数据,这意味着数据只会存储到浏览器关闭。这跟浏览器关闭时会消失的会话 cookie 类似。
所有现代浏览器在实现存储写入时都使用了同步阻塞方式,因此数据会被立即提交到存储。具体 API 的实现可能不会立即把数据写入磁盘(而是使用某种不同的物理存储 ),但这个区别在 JavaScript 层面是不可见的。通过 Web Storage 写入的任何数据都可以立即被读取。
// 使用方法存储数据,不推荐通过对象直接操作sessionStorage
sessionStorage.setItem("name", "Nicholas");
// 使用方法读取数据
let name = sessionStorage.getItem("name");
// 使用方法删除数据
sessionStorage.removeItem("name");
可以结合 sessionStorage 的 length 属性和 key()
方法遍历所有的值:
for (let i = 0, len = sessionstorage.length; i < len; i++) {
let key = sessionStorage.key(i);
let value = sessionStorage.getItem(key);
alert(`${key}=${value}`);
}
这里通过 key()
先取得给定位置中的数据名称,然后使用该名称通过 getItem()
取得值,可以依次访问 sessionStorage 中的名/值对。
for (let key in sessionstorage) {
let value = sessionStorage.getItem(key);
alert(`${key}=${value}`);
}
2.3 localStorage 对象
localStorage 对象在客户端持久存储数据。要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议。
// 使用方法存储数据
localStorage.setItem("name", "Nicholas");
// 使用方法读取数据
let name = localStorage.getItem("name");
// 使用方法删除数据
localStorage.removeItem("name");
2.4 存储事件
每当 storage 对象发生变化时,都会在文档上触发 storage 事件。使用属性或 setItem()
设置值、使用 removeItem()
删除值,以及每次调用 clear()
时都会触发这个事件。这个事了事件对象有如下 4 个属性。
- domain: 存储变化对应的域。
- key: 被设置或删除的键。
- newvalue: 键被设置的新值,若键被删除则为 null。
- olaValue: 键变化之前的值。
可以使用如下代码监听 storage 事件,🚀 站内跳转,前端实现跨标签页通信:
// 对于sessionstorage和 localstorage 上的任何更改都会触发storage 事件,但storage事件不会区分这两者。
window.addEventListener("storage", event => alert("storage changed for $(event.domain) "));
2.5 限制
与其他客户端数据存储方案一样,Web Storage 也有限制。具体的限制取决于特定的浏览器。-般来说,客户端数据的大小限制是按照每个源(协议、域和端口)来设置的,因此每个源有固定大小的数据存储空间。分析存储数据的页面的源可以加强这一限制。不同浏览器给 localstorage 和 sessionStorage 设置了不同的空间限制,但大多数会限制为每个源 5MB。
3 IndexedDB
Indexed Database API 简称 IndexedDB,是浏览器中存储结构化数据的一个方案。IndexedDB 用于替目前已废弃的 Web SQLDatabase API。IndexedDB 背后的思想是创造一套 API,方便 JavaScript 对象的存储和获取,同时也支持查询和搜索。 IndexedDB 的设计几乎完全是异步的。为此,大多数操作以请求的形式执行,这些请求会异步执行产生成功的结果或错误。绝大多数 IndexedDB 操作要求添加 onerror
和 onsuccess
事件处理程序来定输出。 2017 年,新发布的主流浏览器(Chrome、Firefox、Opera、Safari)完全支持 IndexedDB,IEI0! 和 Edge 浏览器部分支持 IndexedDB。
3.1 数据库
IndexedDB 是类似于 MySQL 或 Web SQL Database 的数据库。与传统数据库最大的区别在于 indexedDB 使用对象存储而不是表格保存数据。IndexedDB 数据库就是在一个公共命名空间下的一组对象存储,类似于 NoSQL 风格的实现。使用 IndexedDB 数据库的第一步是调用 indexedD.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;
};
以前,IndexedDB 使用 setVersion()
方法指定版本号。这个方法目前已废弃。如前面代码所示,要在打开数据库的时候指定版本。这个版本号会被转换为一个 unsigned long long 数值,因此不要使用小数,而要使用整数。在两个事件处理程序中,event.target
都指向 request,因此使用哪个都可以。如果 onsuccess
事件处理程序被调用,说明可以通过 event.target.result
访问数据库(IDBDatabase)实例了,这个实例会保存到 db 变量中。之后,所有与数据库相关的操作都要通过 db 对象本身来进行。如果打开数据库期间发生错误, event.target.errorcode
中就会存储表示问题的错误码。
3.2 对象存储
建立了数据库连接之后,下一步就是使用对象存储。如果数据库版本与期待的不一致,那可能需要创建对象存储。不过,在创建对象存储前,有必要想一想要存储什么类型的数据。
假设要存储包含用户名、密码等内容的用户记录。可以用如下对象来表示一条记录:
let user = { username: "007", firstName: "James", lastName: "Bond", password: "foo" };
观察这个对象,可以很容易看出最适合作为对象存储键的 username 属性。用户名必须全局唯一它也是大多数情况下访问数据的凭据。这个键很重要,因为创建对象存储时必须指定一个键。
数据库的版本决定了数据库模式,包括数据库中的对象存储和这些对象存储的结构。如果数据库还不存在, open()
操作会创建一个新数据库,然后触发 upgradeneeded
事件。可以为这个事件设置处理程序、并在处理程序中创建数据库模式。如果数据库存在,而你指定了一个升级版的版本号,则会即触发 upgradeneeded
事件,因而可以在事件处理程序中更新数据库模式。下面的代码演示了为存储上述用户信息如何创建对象存储:
request.onupgradeneeded = event => {
const db = event.target.result;
// 如果存在则删除当前 objectStore。测试的时候可以这样做
// 但这样会在每次执行事件处理程序时删除已有数据
if (db.objectStoreNames.contains("users")) {
db.createObjectStore("users");
}
// 这里第二个参数的 keyPath 属性表示应该用作键的存储对象的属性名。
db.createObjectStore("users", { keyPath: "username" });
};
3.3 事务
创建了对象存储之后,剩下的所有操作都是通过事务完成的。事务要通过调用数据库对象的 transaction()
方法创建。任何时候,只要想要读取或修改数据,都要通过事务把所有修改操作组织起来。最简单的情况下,可以像下面这样创建事务: let transaction = db.transaction()
如果不指定参数,则对数据库中所有的对象存储有只读权限。更具体的方式是指定一个或多个要访问的对象存储的名称: let transaction = db.transaction("users")
这样可以确保在事务期间只加载 users 对象存储的信息。如果想要访问多个对象存储,可以给第一个参数传人一个字符串数组: let transaction = db.transaction(["users","anotherStore"])
如前所述,每个事务都以只读方式访问数据。要修改访问模式,可以传人第二个参数。这个参数应该是下列三个字符串之一:"readonly"、"readwrite"或"versionchange"。比如: let transaction = db.transaction(["users"], "readwrite")
这样事务就可以对 users 对象存储读写了。有了事务的引用,就可以使用 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);
因为一个事务可以完成任意多个请求,所以事务对象本身也有事件处理程序:onerror
和 oncomplete
。这两个事件可以用来获取事务级的状态信息:
transaction.onerror = event => {
//整个事务被取消
};
transaction.oncomplete = event => {
//整个事务成功完成
};
注意,不能通过 oncomplete
事件处理程序的 event 对象访问 get()
请求返回的任何数据。因此仍然需要通过这些请求的 onsuccess
事件处理程序来获取数据。
3.4 插入对象
拿到了对象存储的引用后,就可以使用 add()
或 put()
写人数据了。这两个方法都接收一个参数,即要存储的对象,并把对象保存到对象存储。这两个方法只在对象存储中已存在同名的键时有区别。这种情况下, add()
会导致错误,而 put()
会简单地重写该对象。更简单地说,可以把 add()
想象成插入新值,而把 put()
想象为更新值。因此第一次初始化对象存储时,可以这样做:
// users 是一个用户数据的数组
for (let user of users) {
store.add(user);
}
每次用 add()
或 put()
都会创建对象存储的新更新请求。如果想验证请求成功与否,可以把请求对象保存到一个变量,然后为它添加 onerror
和 onsuccess
事件处理程序:
// users 是一个用户数据的数组
// 创建并填充了数据后,就可以查询对象存储了,
let request,
requests = [];
for (let user of users) {
request = store.add(user);
request.onerror = () => {
//处理错误
};
request.onsuccess = () => {
// 处理成功
};
requests.push(request);
}
3.5 通过游标查询
使用事务可以通过一个已知键取得一条记录。如果想取得多条数据,则需要在事务中创建一个游标。游标是一个指向结果集的指针。与传统数据库查询不同,游标不会事先收集所有结果。相反,游标指向第一个结果,并在接到指令前不会主动查找下一条数据。需要在对象存储上调用 openCursor()
方法创建游标。与其他 IndexedDB 操作-样, opencursor()
方法也返回一个请求,因此必须为它添加 onsuccess
和 onerror
事件处理程序。例如:
const transaction = db.transaction("users"),
store = (transaction.objectStore("users"), (request = store.openCursor()));
request.onsuccess = event => {
//处理成功
};
request.onerror = event => {
//处理错误:
};
在调用 onsuccess
事件处理程序时,可以通过 event.target.result
访问对象存储中的下-条记录,这个属性中保存着 IDBCursor 的实例( 有下一条记录时 )或 nul1( 没有记录时 )。这个 IDEcurso 实例有几个属性。
- airection: 字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值。可能的值包括 NEX("next")、NEXTUNIQUE("nextunique" )、PREV("prev" )、PREVUNIQUE("prevunique")
- key: 对象的键。
- value: 实际的对象。
- primaryKey: 游标使用的键。可能是对象键或索引键(稍后讨论 )。可以像下面这样取得一个结果:
request.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
//永远要检查
console.log(`Key: $(cursor,key), Value: $(JSON,stringify(cursor.value)}`);
}
};
注意,这个例子中的 cursor.value
保存着实际的对象。正因为如此, JSON 来编码。在显示它之前才需要体游标可用于更新个别记录。update()
方法使用指定的对象更新当前游标对应的值。与其他类似-样,用 update()
会创建一个新请求,因此如果想知道结果,需要为 onsuccess
和 onerror
request.onsuccess = event => {
const cursor = event.target.result;
let value, updateRequest;
if (cursor) {
//永远要检查
if (cursor.key == "foo") {
value = cursor.value; // 取得当前对象
value.password = "magic!"; // 更新密码
updateRequest = cursor.update(value); // 请求保存更新后的对象
updateRequest.onsuccess = () => {
//处理成功
};
updateRequest.onerror = () => {
// 处理错误
};
}
}
};
也可以调用 delelte()
来删除游标位置的记录,与 update()
一样,这也会创建一个请求:
request.onsuccess = event => {
const cursor = event.target.result;
let value, deleteRequest;
if (cursor) {
//永远要检查
if (cursor.key == "foo") {
deleteRequest = cursor.delete(); // 请求删除对象
deleteRequest.onsuccess = () => {
//处理成功
};
deleteRequest.onerror = () => {
// 处理错误
};
}
}
};
如果事务没有修改对象存储的权限,update()
和 delete()
都会抛出错误。默认情况下,每个游标只会创建一个请求。要创建另一个请求,必须调用下列中的一个方法。
- continue(key): 移动到结果集中的下一条记录。参数 key 是可选的。如果没有指定 key,游标就移动到下一条记录;如果指定了,则游标移动到指定的键。
- advance(count): 游标向前移动指定的 count 条记录。
这两个方法都会让游标重用相同的请求,因此也会重用 onsuccess
和 onerror
处理程序,直至不再需要。例如,下面的代码迭代了一个对象存储中的所有记录:
request.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
//永远要检查
console.log(`Key: $(cursor,key), Value: $(JSON,stringify(cursor.value)}`);
cursor.continue();
} else {
console.log("Done");
}
};
调用 cursor.continue()
会触发另一个请求并再次调用 onsuccess
事件处理程序。在没有更多记录时,onsuccess
事件处理程序最后一次被调用,此时 event.target.result
等于 null。
3.6 键范围
使用游标会给人一种不太理想的感觉,因为获取数据的方式受到了限制。使用键范围(key range)可以让游标更容易管理。键范围对应 IDBeyRange 的实例。有四种方式指定键范围,第一种是使用 only()
方法并传人想要获取的键: const onlyRange = IDBKeyRange.only("007");
这个范围保证只获取键为"007"的值。使用这个范围创建的游标类似于直接访问对象存储并调用 get("007")。
第二种键范围可以定义结果集的下限。下限表示游标开始的位置。例如,下面的键范围保证游标从"007"这个键开始,直到最后: // 从"007"记录开始,直到最后 const lowerRange = IDBKeyRange.lowerBound("007");
如果想从"007"后面的记录开始,可以再传人第二个参数 true: // 从"007"后面的记录开始,直到最后 const lowerRange = IDBKeyRange.lowerBound("007", true);
第三种键范围可以定义结果集的上限,通过调用 upperBound()方法可以指定游标不会越过的记录。下面的键范围保证游标从头开始并在到达键为"ace"的记录停止: // 从头开始,到"ace"记录为止 const upperRange = IDBKeyRange.upperBound("ace")
如果不想包含指定的键,可以在第二个参数传人 true: // 从头开始,到"ace"的前一条记录为止 const upperRange = IDBKeyRange.upperBound("ace", true)
要同时指定下限和上限,可以使用 bouna()方法。这个方法接收四个参数:下限的键、上限的键、可选的布尔值表示是否跳过下限和可选的布尔值表示是否跳过上限。下面是几个例子:
//从“007"记录开始,到‘ace’记录停止
const boundRange = IDBKeyRange.bound("007", "ace");
//从“007”的下一条记录开始,到‘ace’记录停止,
const boundRange = IDBKeyRange.bound("007", "ace", true);
// 从“007”的下一条记录开始,到‘ace’的前一条记录停止,
const boundRange = IDBKeyRange.bound("007", "ace", true, true);
// 从“007”记录开始,到‘ace’的前一条记录停止
const boundRange = IDBKeyRange.bound("007", "ace", false, true);
定义了范围之后,把它传给 opencursor()
方法,就可以得到位于该范围内的游标:
const store = db.transaction("users").objectStore("users"),
range = IDBKeyRange.bound("007", "ace"),
request = store.openCursor(range);
request.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
//永远要检查
console.log(`Key: $(cursor,key), Value: $(JSON,stringify(cursor.value)}`);
cursor.continue(); // 移动到下一条记录
} else {
console.log("Done");
}
};
这个例子只会输出从键为"007"的记录开始到键为"ace"的记录结束的对象,比上一节的例子要少。
3.7 设置游标方向
openCursor()
方法实际上可以接收两个参数,第一个是 IDBKeyRange 的实例,第二个是表示方向的字符串。通常,游标都是从对象存储的第一条记录开始,每次调用 continue()
或 advance()
都会向最后一条记录前进。这样的游标其默认方向为"next"。如果对象存储中有重复的记录,可能需要游标跳过那些重复的项。为此,可以给 opencursor()
的第二个参数传人"nextunique":
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
request = store.openCursor(null, "nextunique");
注意,opencursor()
的第一个参数是 null,表示默认的键范围是所有值。此游标会遍历对象存储中的记录,从第一条记录开始迭代,到最后一条记录,但会跳过重复的记录。
另外,也可以创建在对象存储中反向移动的游标,从最后一项开始向第一项移动。此时需要给 opencursor()
传人"prev"或"prevunique"作为第二个参数(后者的意思当然是避免重复)。例如:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
request = store.openCursor(null, "prevunique");
在使用"prev"或"prevunique"打开游标时,每次调用 continue()
或 advance()
都会在对象存储中反向移动游标。
3.8 索引
对某些数据集,可能需要为对象存储指定多个键。例如,如果同时记录了用户 ID 和用户名,那可能需要通过任何一种方式来获取用户数据。为此,可以考虑将用户 ID 作为主键,然后在用户名上创建索引。
要创建新索引,首先要取得对象存储的引用,然后像下面的例子-样调用 createIndex()
:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.createIndex("username", "username", { unique: true });
createIndex()
的第一个参数是索引的名称,第二个参数是索引属性的名称,第三个参数是包含键 unique 的 options 对象。这个选项中的 unique 应该必须指定,表示这个键是否在所有记录中唯一。因为 username 可能不会重复,所以这个键是唯一的。
createIndex()
返回的是 IDBIndex 实例。在对象存储上调用 index()
方法也可以得到同一个实例。例如,要使用一个已存在的名为"username"的索引,可以像下面这样:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.index("username");
索引非常像对象存储。可以在索引上使用 openCursor()
方法创建新游标,这个游标与在对象存储上调用 openCursor()
创建的游标完全一样。只是其 result.key
属性中保存的是索引键,而不是主键。下面看一个例子:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.index("username"),
request = index.openCursor();
request.onsuccess = event => {
// 处理成功
};
使用 openKeyCursor()
方法也可以在索引上创建特殊游标,只返回每条记录的主键。这个方法接收的参数与 openCursor()
一样。最大的不同在于,event.result.key
是索引键,且 event.result.value
是主键而不是整个记录。
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.index("username"),
request = index.openKeyCursor();
request.onsuccess = event => {
// 处理成功
// event.result.key 是索引键, event.result.value 是主键
};
可以使用 get()
方法并传人索引键通过索引取得单条记录,这会创建一个新请求:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.index("username"),
request = index.get("007");
request.onsuccess = event => {
// 处理成功
};
request.onerror = event => {
// 处理失败
};
如果想只取得给定索引键的主键,可以使用 getKey()
方法。这样也会创建一个新请求,但 result.value
等于主键而不是整个记录:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.index("username"),
request = index.getKey("007");
request.onsuccess = event => {
// 处理成功
// event.target.result.key 是索引键, event.target.result.value 是主键
};
在这个 onsuccess
事件处理程序中,event.target.result.value
中应该是用户 ID 任何时候,都可以使用 IDBIndex 对象的下列属性取得索引的相关信息。
- name: 索引的名称。
- keyPath: 调用 createIndex()时传人的属性路径
- objectStore: 索引对应的对象存储。
- unique: 表示索引键是否唯一的布尔值。
对象存储自身也有一个 indexNames 属性,保存着与之相关索引的名称。使用如下代码可以方便地了解对象存储上已存在哪些索引:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
indexNames = store.indexNames;
for (let indexName of indexNames) {
const index = store.index(indexName);
console.log(
`Index name: ${indexName}
KeyPath: ${index.keyPath}
ObjectStore: ${index.objectStore.name}
Unique: ${index.unique}`
);
}
以上代码迭代了每个索引并在控制台中输出了它们的信息。在对象存储上调用 deleteIndex()
方法并传人索引的名称可以删除索引:
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
store.deleteIndex("username");
因为删除索引不会影响对象存储中的数据,所以这个操作没有回调
3.9 并发问题
IndexedDB 虽然是网页中的异步 API,但仍存在并发问题。如果两个不同的浏览器标签页同时打开了同一个网页,则有可能出现一个网页尝试升级数据库而另一个尚未就绪的情形。有问题的操作是设置数据库为新版本,而版本变化只能在浏览器只有一个标签页使用数据库时才能完成。第一次打开数据库时,添加 onversionchange
事件处理程序非常重要。另一个同源标签页将数据库打开到新版本时,将执行此回调。对这个事件最好的回应是立即关闭数据库,以便完成版本升级。例如:
let request, database;
request = indexedDB.open("admin", 1);
request.onsuccess = event => {
database = event.target.result;
database.onversionchange = () => database.close();
};
应该在每次成功打开数据库后都指定 onversionchange
事件处理程序。记住,onversionchange
有可能会被其他标签页触发。通过始终都指定这些事件处理程序,可以保证 Web 应用程序能够更好地处理与 IndexedDB 相关的并发问题。
3.10 限制
IndexedDB 的很多限制实际上与 Web Storage 一样。首先,IndexedDB 数据库是与页面源(协议、域和端口)绑定的,因此信息不能跨域共享。这意味着 www.wrox.com 和 p2p.wrox.com 会对应不同的数据存储。
其次,每个源都有可以存储的空间限制。当前 Firefox 的限制是每个源 50MB,而 Chrome 是 5MB。移动版 Firefox 有 5MB 限制,如果用度超出配额则会请求用户许可。
Firefox 还有一个限制---本地文本不能访问 IndexedDB 数据库。地运行本书示例时,要使用 Chrome。
4 小结
WebStorage 定义了两个对象用于存储数据:sessionstorage 和 localstorage。前者用于严格保存浏览器一次会话期间的数据,因为数据会在浏览器关闭时被删除。后者用于会话之外持久保存数据。
IndexedDB 是类似于 SOL 数据库的结构化数据存储机制。不同的是,IndexedDB 存储的是对象,而不是数据表。对象存储是通过定义键然后添加数据来创建的。游标用于查询对象存储中的特定数据,而索引可以针对特定属性实现更快的查询。
有了这些存储手段,就可以在客户端通过使用 JavaScript 存储可观的数据。因为这些数据没有加密所以要注意不能使用它们存储敏感信息。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现