客户端存储 | IndexedDB
什么是indexedDB
indexedDB
是浏览器结构化存储的一种方式,用于代替目前已经废弃的Web SQL Database API
indexedDB
是类似于MySQL
或者Web SQL Database
的数据库,但是**IndexDB
使用对象而不是表格保存数据,因此,可以说indexedDB
是在一个公共空间下的一组对象存储,类似于NoSQL风格的实现
如何使用IndexDB
数据库
使用indexedDB
数据库可通过如下步骤(基础):
- Step1:调用
indexedDB.open()
方法 - Step2:对象存储
- Step3:调用事务进行增删改查
Step1:调用indexedDB.open()
方法
indexedDB.open()
方法用于打开数据库(如果数据库不存在,则为创建)
参数说明
- 要打开的数据库名称(必选)
- 指定版本号(可选)
返回值说明
返回一个IDBRequest
的实例,可以在这个实例上添加onerror
和onsuccess
事件处理程序,这两个事件处理程序的event.target
都指向这个实例
- 如果打开的时候发生错误,就会在
event.target.errorCode
中存储错误码 - 如果打开成功,就可以通过
event.target.result
访问数据库
1 | let db, |
Step2:对象存储
对象存储你需要准备:
- 指定一个键,必须是全局唯一的
1 | let user = { |
假设要存储这样一个对象,很显然username
可以用来做主键
- upgradeneeded事件用于监听更新数据库
1 | request.onupgradeneeded = (event) => { |
Step3:通过事务进行后序操作
在对象存储之后,剩下的所有操作都是通过事务完成的
- 插入对象:add put
- 删除对象:delete (键作为参数)
- 获取对象:get(键作为参数)
- 清空对象:clear
1. 创建事务 db.transaction()
- 没有指定名称——数据库中所有的对象都只有只读权限
- 访问一个或者多个数据库——传入数据库名称,单个直接传入,多个以数组的形式传入
- 传入第二个参数以修改访问模式——
readonly readwrite versionchange
1 | // 没有指定名称,数据库中所有的对象都只有只读权限 |
2. 事务事件处理程序绑定
1 | const transaction = db.transaction('users'), |
因为一个事务可以完成任意多个请求,所以事务对象本身也有事件处理程序:onerror
和oncomplete
,这两个事件可以用来获取事务级的状态信息
1 | transaction.onerror = (event) => { |
注意,不能通过oncomplete
事件处理程序的event
对象访问get()
请求返回的任何数据,因此,仍然需要通过这些请求的onsuccess
事件处理程序来获取数据
插入数据
插入数据的方式有:
- add(增加新值)
- put(修改,更新)
这两个方法都接收一个参数:要存储的对象
两者的区别在于:传入对象存储已经包含同名键时,add会报错,而put是重写对象
1 | for (let user of users) { |
每次调用add()
或put()
都会创建对象存储的新更新请求
如果想验证请求成功与否,可以把请求对象保存到一个变量,然后为它添加onerror
和onsuccess
事件处理程序
1 | let request, |
通过游标查询
使用事务可以通过一个已知键取得一条记录,如果想取得多条数据,则需要在事务中创建一个游标
游标是一个指向结果集的指针
与传统数据库查询不同,游标不会事先收集所有结果,相反,游标指向第一个结果,并在接到指令前不会主动查找下一条数据
创建游标 openCursor()
可以接收两个参数:(后面讲!!!)
- 键范围
- 游标方向
openCursor()也返回一个请求,所以必须为它添加onsuccess和onerror事件处理程序
1 | const transaction = db.transaction('users'), |
在调用
onsuccess
事件处理程序时,可以通过event.target.result
访问对象存储中的下一条记录,这个属性中保存着IDBCursor
的实例(有下一条记录时)或null(没有记录时)实例有如下属性:
- direction:字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值。
- 可能的值包括:NEXT(“next”)、NEXTUNIQUE(“nextunique”)、PREV(“prev”)、PREVUNIQUE(“prevunique”)
- key:对象的键
- value:实际的对象
- primaryKey:游标使用的键,可能是对象键或索引键
1
2
3
4
5
6request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) { // 永远要检查!
console.log(`Key:${cursor.key},Value:${JSON.stringify(cursor.value)}`)
}
}- direction:字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值。
游标可用于更新个别记录——update()
1 | request.onsuccess = (event) => { |
- 可以删除游标位置的记录_delete()
1 | request.onsuccess = (event) => { |
注意:如果事务没有修改对象存储的权限,那么update和delete都会报错
游标迭代
默认情况下,每个游标只会创建一个请求
要创建另一个请求,必须调用下列中的一个方法
- continue(key):移动到结果集中的下一条记录,参数key 是可选的,如果没有指定key,游标就移动到下一条记录;如果指定了,则游标移动到指定的键
- advance(count):游标向前移动指定的count 条记录,这两个方法都会让游标重用相同的请求,因此也会重用onsuccess 和onerror 处理程序,直至不再需要
1 | request.onsuccess = (event) => { |
键范围
可以使用键范围更容易地管理游标
键范围对应IDBKeyRange
的实例
有四种方式指定键范围
- 方式1:only
1 | const onlyRange = IDBKeyRange.only('007'); |
这个范围保证只获取键为”007”的值
使用这个范围创建的游标类似于直接访问对象存储并调用get(“007”)
- 方式2:lowerBound
定义结果集的下限,下限表示游标开始的位置
1 | const lowerRange = IDBKeyRange.lowerRange('007'); // 保证游标从"007"这个键开始 |
- 方式3:upperBound
定义结果集的上限,通过调用upperBound()方法可以指定游标不会越过的记录,如果不想包含指定的键,可以在第二个参数传入true
1 | // 从头开始,到"ace"记录为止 |
- 方式4:bound
同时指定下限和上限,这个方法接收四个参数:下限的键、上限的键、可选的布尔值表示是否跳过下限和可选的布尔值表示是否跳过上限
1 | // 从"007"记录开始,到"ace"记录停止 |
游标范围设置——传入openCursor
1 | const store = db.transaction("users").objectStore("users"), |
设置游标方向
游标方向:
next
(默认):从第一条到最后一条,不跳过重复项nextunique
:从第一条到最后一条,跳过重复项prev
:从最后一条到第一条,不跳过重复项prevunique
:从最后一条到第一条,跳过重复项
索引
创建新索引 createIndex()
参数说明
- 索引名称
- 索引属性名称
- 包含键unique的options对象
- unique必须指定,表示这个键是否在所在记录里面唯一
1 | const transaction = db.transaction("users"), |
返回值说明
返回IDBIndex实例
1 | const transaction = db.transaction("users"), |
可以在索引上使用openCursor()方法创建新游标,这个游标与在对象存储上调用openCursor()创建的游标完全一样,只是其result.key 属性中保存的是索引键,而不是主键
1
2
3
4
5
6
7
8const 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是主键而不是整个记录
1
2
3
4
5
6
7
8
9const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.index("username"),
request = index.openKeyCursor();
request.onsuccess = (event) => {
// 处理成功
// event.result.key 是索引键,event.result.value 是主键
};如果想只取得给定索引键的主键,可以使用getKey()方法,这样也会创建一个新请求,但result.value 等于主键而不是整个记录
1
2
3
4
5
6
7
8
9const 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 属性,保存着与之相关索引的名称
使用如下代码可以方便地了解对象存储上已存在哪些索引
1
2
3
4
5
6
7
8
9
10const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
indexNames = store.indexNames
for (let indexName in indexNames) {
const index = store.index(indexName);
console.log(`Index name: ${index.name}
KeyPath: ${index.keyPath}
Unique: ${index.unique}`);
}在对象存储上调用deleteIndex()方法并传入索引的名称可以删除索引
1
2
3const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
store.deleteIndex("username");因为删除索引不会影响对象存储中的数据,所以这个操作没有回调
并发问题
IndexedDB
虽然是网页中的异步API
,但仍存在并发问题
如果两个不同的浏览器标签页同时打开了同一个网页,则有可能出现一个网页尝试升级数据库而另一个尚未就绪的情形
有问题的操作是设置数据库为新版本,而版本变化只能在浏览器只有一个标签页使用数据库时才能完成
第一次打开数据库时,添加onversionchange 事件处理程序非常重要,另一个同源标签页将数据库打开到新版本时,将执行此回调,对这个事件最好的回应是立即关闭数据库,以便完成版本升级
1 | let request, database; |
应该在每次成功打开数据库后都指定onversionchange
事件处理程序
记住,onversionchange
有可能会被其他标签页触发
通过始终都指定这些事件处理程序,可以保证Web
应用程序能够更好地处理与IndexedDB
相关的并发问题
限制
- IndexedDB 数据库是与页面源(协议、域和端口)绑定的,因此信息不能跨域共享。这意味着www.wrox.com 和p2p.wrox.com 会对应不同的数据存储
- 每个源都有可以存储的空间限制。当前Firefox 的限制是每个源50MB,而Chrome 是5MB,移动版Firefox 有5MB 限制,如果用度超出配额则会请求用户许可
- Firefox 还有一个限制——本地文本不能访问IndexedDB 数据库,Chrome 没有这个限制。