IDB 操作的基本步骤是
window.indexedDB
是一个 IDBFactory
对象,调用对象 open
方法返回的是一个 IDBOpenDBRequest
请求,监听 success
事件,e.target.result
指向一个名为 IDBDatabase
的对象,该对象就是连接到数据库的唯一 API。需要注意的是 IDBDatabase
对象有 close、createObjectStore、deleteObjectStore、transaction 方法和 name、version 等常用属性:
下面是个例子:
function getDB(dbName) { // 1️⃣
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(dbName) // 2️⃣
request.onerror = reject
request.onsuccess = function (e) { // 3️⃣
let db = e.target.result
console.log(`db ${db.name}::${db.version} open success`)
resolve(db)
}
})
}
步骤:
result
对象里面使用方法:
; (async function () {
+ let db = await getDB('DEMO')
+ db.close()
})()
打开 Application 面板就能够看到这个数据库和存储空间了:
但目前没什么卵用 ? 因为没有定义数据库数据结构调用还不能存储东西,这个时候我们就要更新版本号并创建数据存储的结构了
function getUpgradedDB(db) { // 1️⃣
return new Promise((resolve, reject) => {
console.log(`current ${db.name}::${db.version}`)
let request = window.indexedDB.open(db.name, ++db.version) // 2️⃣
request.onerror = reject
request.onupgradeneeded = function (e) { // 3️⃣
let db = e.target.result
console.log('db upgraded')
resolve(db)
}
})
}
步骤:
result
对象返回,这样我们就获得了新版本的数据库使用方法:
; (async function () {
let db = await getDB('DEMO')
+ db.close()
+ db = await getUpgradedDB(db)
})()
⚠️️ 再次提醒如果传入 db 升级数据库,一定要先 close 方法关闭旧版本数据库
现在我们数据库的版本就升级一了,这个时候就应该在升级的同时创建数据库的结构:
假定我们需要存储这样的数据:
{ name: 'oli', age: 12, email: 'example@example.com' }
需要注意的是:
创建存储空间需要调用 db 数据库对象的 createObjectStore 方法;该方法返回的是一个 IDBObjectStore
对象。
存储空间有以下方法和属性:
看名字基本都能明白代表的含义,详细见文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IDBObjectStore
function initDBStructor(db, storeName, opts, indexArr) { // 1️⃣
return new Promise((resolve, reject) => {
let store = db.createObjectStore(storeName, opts) // 2️⃣
indexArr.forEach(indexObj => { // 3️⃣
store.createIndex(indexObj.name, indexObj.name, { unique: indexObj.unique })
})
store.transaction.onerror = reject
store.transaction.oncomplete = function (e) { // 4️⃣
console.log('db initiated')
resolve(e.target.db)
}
})
}
步骤
使用方法:
; (async function () {
let db = await getDB('DEMO')
db.close()
db = await getUpgradedDB(db)
+ db = await initDBStructor(db, 'users', { keyPath: 'email' }, [{ name: 'name', unique: false }, { name: 'age', unique: false }, { name: 'email', unique: true }])
})()
根据数据,我们设置了主键为 email 字段,然后 name 和 age 字段 unique 为 false,email 字段 unique 属性为 true,设置完毕再去调试面板:
然后我们就可以对数据仓库进行操作了:
增加数据
function setItem(db, storeName, data) { // 1️⃣
return new Promise((resolve, reject) => {
let request = db.transaction(storeName, 'readwrite').objectStore(storeName).add(data) // 2️⃣
request.onerror = reject
request.onsuccess = function (e) { // 3️⃣
console.log('add data success')
resolve(e.target.result)
}
})
}
步骤:
使用方法:
; (async function () {
let db = await getDB('DEMO')
- db.close()
- db = await getUpgradedDB(db)
+ let res = await setItem(db, 'users', { name: 'oli', age: 11, email: 'hello@example.com' })
})()
我们增加了一条数据,调试工具打开看下现在的数据库:
成功插入数据!值得庆祝 ?
查询数据
function getItem(db, storeName, key) { // 1️⃣
return new Promise((resolve, reject) => {
let request = db.transaction(storeName, 'readonly').objectStore(storeName).get(key) // 2️⃣
request.onerror = reject
request.onsuccess = function (e) { // 3️⃣
console.log('get data success')
resolve(e.target.result)
}
})
}
步骤:
使用方法:
; (async function () {
let db = await getDB('DEMO')
- let res = await setItem(db, 'users', { name: 'oli', age: 11, email: 'hello@example.com' })
+ let res = await getItem(db, 'users', 'hello@example.com')
})()
返回的数据如下:
修改数据
function modifyItem(db, storeName, key, data) { // 1️⃣
return new Promise(async (resolve, reject) => {
let request = db.transaction(storeName, 'readonly').objectStore(storeName).get(key) // 2️⃣
request.onerror = reject
request.onsuccess = function (e) { // 3️⃣
if (e.target.result) {
let requestUpdate = db.transaction(storeName, 'readwrite').objectStore(storeName).put({ ...e.target.result, ...data }) // 4️⃣
requestUpdate.onerror = reject
requestUpdate.onsuccess = function (e) { // 6️⃣
console.log('modify item success')
resolve(e.target.result)
}
} else {
reject('not match key')
}
}
})
}
步骤:
数据修改成功:
; (async function () {
let db = await getDB('DEMO')
- let res = await getItem(db, 'users', 'hello@example.com')
+ let res = await modifyItem(db, 'users', 'hello@example.com', { name: 'woooh' })
})()
效果如下:
删除数据
最后再来个删除数据:
function deleteItem(db, storeName, key) { // 1️⃣
return new Promise((resolve, reject) => {
let request = db.transaction(storeName, 'readwrite').objectStore(storeName).delete(key) // 2️⃣
request.onerror = reject
request.onsuccess = function (e) { // 3️⃣
console.log('remove item success')
resolve(e.target.result)
}
})
}
步骤:
删除成功:
; (async function () {
let db = await getDB('DEMO')
- let res = await getItem(db, 'users', 'hello@example.com')
+ let res = await deleteItem(db, 'users', 'hello@example.com')
})()
看看效果:
empty! ?
使用游标 openCursor 返回的是一个 IDBCursor
对象,监听 success 返回的 cursor 则是含有 value 的 IDBCursorWithValue
对象,两者区别:
在使用游标之前,我们先插入几条假数据:
然后实现一个通过游标获取所有数据的函数:
function getAllValue(db, storeName) { // 1️⃣
return new Promise((resolve, reject) => {
let request = db.transaction(storeName).objectStore(storeName).openCursor() // 2️⃣
let res = []
request.onerror = reject
request.onsuccess = function (e) { // 3️⃣
let cursor = e.target.result
if (cursor) { // 4️⃣
res.push(cursor.value)
cursor.continue()
} else {
resolve(res)
console.log('get all value finish')
}
}
})
}
步骤:
使用方法:
; (async function () {
+ let db = await getDB('DEMO')
+ let res = await getAllValue(db, 'users')
})()
效果如下:
另外获得所有数据的数组还可以使用 getAll 方法:
function getAllData(db, storeName) {
return new Promise((resolve, reject) => {
let request = db.transaction(storeName).objectStore(storeName).getAll() // 1️⃣
request.onerror = reject
request.onsuccess = function (e) { // 2️⃣
let data = e.target.result
console.log('get all value finish')
resolve(data)
}
})
}
步骤:
另外还可以给游标 openCursor 方法传入 keyRange 来指定游标的范围,这种 keyRange 有多种类型,分别是 IDBKeyRange 的几个内置方法:
详细见下表:
function getDataByCursor(db, storeName, indexName, type, ...args) { // 1️⃣
return new Promise((resolve, reject) => {
let keyRange
switch (type) { // 2️⃣
case 'only':
keyRange = IDBKeyRange.only(args[0])
break
case 'lowerBound':
keyRange = IDBKeyRange.lowerBound(args[0], args[1])
break
case 'upperBound':
keyRange = IDBKeyRange.upperBound(args[0], args[1])
break
case 'bound':
keyRange = IDBKeyRange.bound(...args)
break
default:
break
}
let request = db.transaction(storeName).objectStore(storeName).index(indexName).openCursor(keyRange) // 3️⃣
let res = []
request.onerror = reject
request.onsuccess = function (e) { // 4️⃣
let cursor = e.target.result
if (cursor) {
res.push(cursor.value)
cursor.continue()
} else {
resolve(res)
console.log('get all value finished')
}
}
})
}
步骤:
使用方法:
; (async function () {
let db = await getDB('DEMO')
let res
+ res = await getDataByCursor(db, 'users', 'name', 'only', 'oli')
+ res = await getDataByCursor(db, 'users', 'name', 'lowerBound', 'oli')
+ res = await getDataByCursor(db, 'users', 'name', 'lowerBound', 'oli', true)
+ res = await getDataByCursor(db, 'users', 'name', 'bound', 'oli', 'troy', false, false)
})()
另外还可以指定游标的方向,给 openCursor 方法传入第二个参数
prev
来指定使用倒序;如果想要过滤重复的记录,那么传入IDBCursor.nextunique
或IDBCursor.prevunique
即可;其他参数见文档:https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor
index 方法返回一个名为 IDBIndex
的对象
IDBIndex 这个对象的属性方法见文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IDBIndex
可通过索引搜索已经建立索引的条目:
function getDataByIndex(db, storeName, indexName, key) { // 1️⃣
return new Promise((resolve, reject) => {
let request = db.transaction(storeName).objectStore(storeName).index(indexName).get(key) // 2️⃣
request.onerror = reject
request.onsuccess = function(e) { // 3️⃣
let data = e.target.result
console.log('get value')
resolve(data)
}
})
}
; (async function () {
let db = await getDB('DEMO')
- let res = await getAllData(db, 'users')
+ let res = await getDataByIndex(db, 'users', 'name', 'troy')
})()
另外还有在索引上调用的两种游标使用方法,见 MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API/Using_IndexedDB#%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%95
最后来聊一聊 IDB 数据库的事务和请求之间的关系:
首先,请求就是所有针对 IDB 的异步操作都会返回请求,我们监听 success 才能在请求成功之后通过 result 对象获取到的结果
然后是事务,一个请求可能有事务也有可能没有事务,比方说只连接上数据库但没有操作数据,那么就存在请求,但不存在事务
The transaction for the request. This property can be null for certain requests, such as for request returned from IDBFactory.open (You're just connecting to a database, so there is no transaction to return).
举个例子:
; (async function () {
let getIDBRequest = window.indexedDB.open('DEMO')
let db
getIDBRequest.onsuccess = e => {
db = getIDBRequest.result
debugger
}
})()
打开数据库操作就是一个请求:
这里面就没有事务
一旦操作数据,那么你就必须首先创建一个事务并设置模式,然后通过存储空间来发起请求
; (async function () {
let getIDBRequest = window.indexedDB.open('DEMO')
let db
getIDBRequest.onsuccess = e => {
db = getIDBRequest.result
let transaction = db.transaction('users', 'readonly')
let store = transaction.objectStore('users')
let request = store.count()
debugger
}
})()
那么这个请求也就会包含事务了:
PWA 系列第三章 IDB 的简要介绍就到这里,这个系列的下篇文章将介绍 Service Worker ?