5

20190326213827.png

IDB 操作的基本步骤是

  • open 方法打开数据库 ?
  • 然后是创建数据库 store 对象仓库 ?

    • 需要注意更新数据库版本应先调用 close 方法关闭旧版数据库
    • 需要注意创建 store 一定要在新版本数据库的 upgradeneeded 事件处理函数中创建,因为本质上他是修改数据库结构
  • 如果对数据库进行数据操作那么需要通过事务来执行 ?。

打开数据库

window.indexedDB 是一个 IDBFactory 对象,调用对象 open 方法返回的是一个 IDBOpenDBRequest 请求,监听 success 事件,e.target.result 指向一个名为 IDBDatabase 的对象,该对象就是连接到数据库的唯一 API。需要注意的是 IDBDatabase 对象有 close、createObjectStore、deleteObjectStore、transaction 方法和 name、version 等常用属性:

20190326095725.png

下面是个例子:

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)
        }
    })
}

步骤:

  • 第一步,需要先定义一个数据库这里通过自定义的 dbName 来创建这个新的数据库
  • 第二部,通过 open 方法打开数据库
  • 第三部,监听 onsuccess 方法,打开成功数据库对象就在这个 request 的 result 对象里面

使用方法:

; (async function () {
+   let db = await getDB('DEMO')
+   db.close()
})()

打开 Application 面板就能够看到这个数据库和存储空间了:

20190325153436.png

但目前没什么卵用 ? 因为没有定义数据库数据结构调用还不能存储东西,这个时候我们就要更新版本号并创建数据存储的结构了

升级版本号

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)
        }
    })
}

步骤:

  • 第一步,首先获取 db 对象,(可选,方便累加现有的数据库版本号)
  • 第二步,然后通过 open 方法传入新版本号来创建新的版本数据库( ⚠️ 注意版本号始终应当为整型数值)
  • 第三步,监听 upgradeneeded 事件,将 request 的 result 对象返回,这样我们就获得了新版本的数据库

使用方法:

; (async function () {
    let db = await getDB('DEMO')
+   db.close()
+   db = await getUpgradedDB(db)
})()

⚠️️ 再次提醒如果传入 db 升级数据库,一定要先 close 方法关闭旧版本数据库

20190325154942.png

现在我们数据库的版本就升级一了,这个时候就应该在升级的同时创建数据库的结构:

创建存储空间

假定我们需要存储这样的数据:

{ name: 'oli', age: 12, email: 'example@example.com' }

需要注意的是:

  • name 为 name 键的索引 不唯一
  • age 为 age 键的索引 不唯一
  • email 为主键 唯一不重复

创建存储空间需要调用 db 数据库对象的 createObjectStore 方法;该方法返回的是一个 IDBObjectStore 对象。

存储空间有以下方法和属性:

20190326100633.png

看名字基本都能明白代表的含义,详细见文档:https://developer.mozilla.org...
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)
        }
    })
}

步骤

  • 首先,我们接收数据库作为参数,storeName 代表存储空间的名称,opts 参数是创建的存储空间的选项(比如设置主键等),indexArr 则代表数据库需要创建的索引(这里假定所有数据都需要创建索引)
  • 然后通过调用数据库的 createObjectStore 方法创建存储空间
  • 根据传入的数据结构使用 store 的 createIndex 创建索引
  • 监听 store 事务的 complete 方法,数据库初始化完成

使用方法:

; (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,设置完毕再去调试面板:

20190325160422.png

然后我们就可以对数据仓库进行操作了:

数据的增查改删

增加数据

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)
        }
    })
}

步骤:

  • 这个函数需要获取三个参数,一个是数据库对象 db,一个是 store 的名称,另外一个则是需要插入的数据
  • 然后调用 db 的 transaction,传入 store 名称并设置操作为读写权限,然后打开存储空间并调用 add 方法插入数据
  • 最后监听 request 的 success 事件

使用方法:

; (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' })
})()

我们增加了一条数据,调试工具打开看下现在的数据库:

20190325161148.png

成功插入数据!值得庆祝 ?

查询数据

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)
        }
    })
}

步骤:

  • 第一步,获取到 db 数据库对象,接收 store 名称以及数据 key 我们要根据 key 来做查询操作获取 value
  • 第二步,调用数据库对象的 transaction 传入 store 名称和只读权限,然后获取存储空间并调用 get 方法=
  • 第三步,监听 success 事件

使用方法:

; (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')
})()

返回的数据如下:

20190325164156.png

修改数据

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')
            }
        }
    })
}

步骤:

  • 首先接收 db 数据库对象、store 存储空间名称以及 key 和修改后的 data
  • 然后根据 key 进行查询操作
  • 监听 request 的 success 事件
  • 然后调用 put 方法将查询的数据和修改后的数据 merge 并插入到存储空间(为了方便演示,直接合并对象)
  • 最后监听 requestUpdate 的 success 事件

数据修改成功:

; (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' })
})()

效果如下:

20190325171536.png

删除数据

最后再来个删除数据:

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)
        }
    })
}

步骤:

  • 接收参数 db 数据库对象,store 仓库名称,key
  • 然后创建 readwrite 事务,调用存储空间的 delete 方法
  • 监听 request 的 success 事件

删除成功:

; (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')
})()

看看效果:

20190325171856.png

empty! ?

游标

使用游标 openCursor 返回的是一个 IDBCursor 对象,监听 success 返回的 cursor 则是含有 value 的 IDBCursorWithValue 对象,两者区别:

20190326101724.png

在使用游标之前,我们先插入几条假数据:

20190325172738.png

然后实现一个通过游标获取所有数据的函数:

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')
            }
        }
    })

}

步骤:

  • 首先函数接收两个参数,一个是 db 对象,一个是存储空间名称
  • 然后创建一个 request 调用存储空间的 openCursor 方法
  • 然后监听 request 的 success 事件
  • 检测是否存在 cursor,如果是则 push 数据并调用 continue 方法继续监听 openCursor 的 success 事件遍历,否则返回所有结果

使用方法:

; (async function () {
+   let db = await getDB('DEMO')
+   let res = await getAllValue(db, 'users')
})()

效果如下:

20190325173808.png

另外获得所有数据的数组还可以使用 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)
        }
    })
}

步骤:

  • 使用存储空间上的 getAll 方法
  • 监听 success 事件

游标范围

另外还可以给游标 openCursor 方法传入 keyRange 来指定游标的范围,这种 keyRange 有多种类型,分别是 IDBKeyRange 的几个内置方法:

  • only 指定仅匹配
  • lowerBound 在...之下 另外接收一个 Boolean 指定是否包括当前 key
  • upperBound 在...之上 另外接收一个 Boolean 指定是否包括当前 key
  • bound 在...之间 另外接收两个 Boolean 指定是否包括当前两个 key

20190326102117.png

详细见下表:

20190326091859.png

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')
            }
        }
    })
}

步骤:

  • 首先接收几个参数 db 数据库对象,store 存储空间名称,index 索引名称以及 范围类型名称和选项
  • 然后根据不同名称对应不同游标范围
  • 调用 openCursor 方法并传入游标范围参数
  • 监听 success 事件获取数据

使用方法:

; (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.nextuniqueIDBCursor.prevunique 即可;其他参数见文档:https://developer.mozilla.org...

使用索引搜索

index 方法返回一个名为 IDBIndex 的对象

IDBIndex 这个对象的属性方法见文档:https://developer.mozilla.org...

可通过索引搜索已经建立索引的条目:

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)
        }
    })
}
  • 首先接收参数 db 对象、store 存储空间名称、index 索引名称和搜索关键词
  • 然后调用存储空间的 index 方法的 get 方法搜索关键词
  • 然后监听 success request 的 result 值即为搜索到的数据
; (async function () {
    let db = await getDB('DEMO')
-   let res = await getAllData(db, 'users')
+   let res = await getDataByIndex(db, 'users', 'name', 'troy')
})()

20190326083344.png

另外还有在索引上调用的两种游标使用方法,见 MDN 文档:https://developer.mozilla.org...

事务和请求之间的关系

最后来聊一聊 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
    }
})()

打开数据库操作就是一个请求:

20190326103421.png

这里面就没有事务

一旦操作数据,那么你就必须首先创建一个事务并设置模式,然后通过存储空间来发起请求

; (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
    }
})()

那么这个请求也就会包含事务了:

20190326104651.png

PWA 系列第三章 IDB 的简要介绍就到这里,这个系列的下篇文章将介绍 Service Worker ?


JS菌
6.4k 声望2k 粉丝