这是我参与更文挑战的第1天,活动详情查看: 更文挑战
前言
IndexedDB 是网页浏览器的一个数据库,它的存储空间比 localStorage 大得多,有时候我们实现某些功能(如聊天)时需要存储大量的数据,这个时候就可以用上 IndexedDB。
除了存储大量数据特点外,IndexedDB 还有这几个特点,它的数据结构是键值类型,除了主键是必须存在外,每个记录的其他的键值可以随意定义;它的所有操作都是异步。
下面将逐一介绍如何使用 IndexedDB 和解决某些场景问题,以聊天场景为例。
初始化数据库
假设有一个 db.js,在里面创建一个 DB 类,封装 init 方法,init 方法里面先用 window.indexedDB.open 打开对应的数据库(若没有,则自动创建),这个时候会返回一个对象,这个对象主要有以下三个回调 api:
1、onsuccess,数据库连接成功时回调
2、onupgradeneeded,数据库版本升级时回调
3、onerror,数据库连接失败时回调
使用如下:
class DB {
constructor() {
this.db = null // 内部的数据库对象
this.tb_chat = 'chat' // 这个是表名
this.version = 1 // 版本号
}
// 初始化
init() {
return new Promise((resolve, reject) => {
var that = this
// 打开数据库 db_chat,指定数据库版本号为 1(如果没有指定,那么默认为1)
var request = window.indexedDB.open('db_chat', this.version)
// 成功打开数据库时回调
request.onsuccess = () => {
this.db = request.result
resolve()
}
// 连接数据库时,版本号大于现有版本号或第一次创建数据库时也会回调,进行回调,可以利用版本号来更改数据结构
request.onupgradeneeded = function (event) {
this.db = event.target.result
var objectStore
// 如果没有聊天 chat 表(对象仓库),则创建 chat
if (!this.db.objectStoreNames.contains(that.tb_chat)) {
objectStore = this.db.createObjectStore(that.tb_chat, { keyPath: 'id' }) // 创建表 chat,并指定 id 为主键
objectStore.createIndex('id', 'id', { unique: true }) // 创建 id 索引,具有唯一性
objectStore.createIndex('uid', 'uid', { unique: false }) // 创建 发送者id 索引
objectStore.createIndex('uid2', 'uid2', { unique: false }) // 创建 接收者id 索引
objectStore.createIndex('sendTime', 'sendTime', { unique: true }) // 发送时间 索引
objectStore.createIndex('uid_uid2', ['uid', 'uid2'], { unique: false }) // 发送者—接收者 索引
}
}
// 打开数据库失败时回调
request.onerror = (error) => {
reject(error)
}
})
}
}
注意一点,IndexedDB 的所有相关操作都是异步的,因此上面的 init 方法用了 promise,可以让一些需要在数据库初始化后立刻操作的代码写到 promise 的 then 里面去。
为了方便使用,这里直接导出 DB 类的对象。
class DB {
...
}
let db = new DB()
export default db
需要用到的地方,可以这样调用
// 导入上面的 db.js,路径写为自己的
import db from db.js
db.init().then(() => {
// 操作
db.add({id: 1, content: 'hello'}) // 结合下面章节的新增代码
})
如果需要设置主键为自增,则可以用以下两种方式设置,但是记得,设置自增后,新建数据时,主键的值只能由 IndexdDB 自身控制,不能传入自定义值,否则会报错。
- 主键有 keyPath,新增数据时,会自动给数据添加 id 属性
objectStore = this.db.createObjectStore(that.tb_chat, { keyPath: 'id', autoIncrement: true }) // 在创建表时,添加 autoIncrement
- 主键没有 keyPath,主键将只有值的概念,不会给数据添加主键信息
objectStore = this.db.createObjectStore(that.tb_chat, { autoIncrement: true }) // 在创建表时,添加 autoIncrement
设置自增 id 后,应该这样调用
import db from db.js
db.init().then(() => {
// 操作
// db.add({id: 1, content: 'hello'}) // 若设置 id 为自增主键后,不能这样调用,会报错
db.add({content: 'hello'})
// 如果主键的 keyPath 是 id,那么入库数据将会是 { id: 1, content: 'hello'}
// 如果主键没有 keyPath,那么入库数据将会是 { content: 'hello' }
})
新增操作
在 IndexedDB 里,增删改查都要通过事务来进行操作,所谓的事务就是,如果中途有一个操作出现了异常,那么之前的操作都会进行回滚,数据回到操作前的样子。
新增数据前,先创建一个事务(transaction),事务要指定需要操作的表名(数组)和处理方式(只读或读写),创建好事务对象后,用事务对象调用 objectStore(表名).add(json对象)即可以新增数据。
class DB {
...
// 新增数据(返回主键)
add(tb, data) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb], 'readwrite').objectStore(tb).add(data)
request.onsuccess = (event) => {
resolve(event.target.result)
}
request.onerror = (event) => {
reject(event)
}
})
}
}
调用
db.add(db.tb_chat, {id:1, uid:1, uid2:2, sendTime: 1622539418861, content:'Hello World'})
删除操作
根据主键删除数据,和新增代码差不多。
class DB {
...
// 根据主键删除数据
delete(tb, key) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb], 'readwrite')
.objectStore(tb)
.delete(key)
request.onsuccess = (event) => {
resolve(event)
}
request.onerror = (error) => {
reject(error)
}
})
}
}
调用
// 删除主键为 1 的数据
db.delete(db.tb_chat, 1)
修改操作
根据主键修改数据(貌似修改数据是只能通过主键修改,不能通过其他条件修改)
class DB {
...
// 根据主键修改数据
update(tb, data) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb], 'readwrite')
.objectStore(tb)
.put(data)
request.onsuccess = (event) => {
resolve(event)
}
request.onerror = (error) => {
reject(error)
}
})
}
}
调用
// 修改主键为 1 的数据,修改内容为 hi
db.delete(db.tb_chat, {id:1, uid:1, uid2:2, sendTime: 1622539418861, content:'hi'})
查询操作
查询是数据库操作的重点难点,通常涉及到查询的场景千姿百态。
在初始化操作里,你可能注意到在 onupgradeneeded 回调方法里,在创建表时,同时也创建了几个索引,这是因为在 IndexedDB 里,任何的查询条件必须是索引或主键,比如我要查询“‘发送时间’等于2021年6月1日”的聊天记录,如果没有创建“发送时间”的索引,则无法查询。
主键查询和索引查询
下面贴出主键查询和索引查询的代码。
class DB {
...
// 根据主键查询数据
selectById(tb, key) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb])
.objectStore(tb)
.get(key)
request.onsuccess = (event) => {
if (request.result) {
resolve(request.result)
} else {
resolve()
}
}
request.onerror = (error) => {
reject(error)
}
})
}
// 根据索引查询
select(tb, index, content) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb])
.objectStore(tb)
.index(index)
.get(content)
request.onsuccess = (event) => {
if (request.result) {
resolve(request.result)
} else {
resolve()
}
}
request.onerror = (error) => {
reject(error)
}
})
}
}
可是,有时候我们会遇到需要多个索引作为条件进行查询,这个时候我们可以建立一个由多个索引组合起来的组合索引。再回头看看 onupgradeneeded 方法,是不是有一个由发送者 id 和接收者 id 组合得来的“发送者-接收者”组合索引,这个是为查询“发送者-接受者”的聊天记录做准备的。
objectStore.createIndex('uid_uid2', ['uid', 'uid2'], { unique: false }) // 发送者—接收者 索引
不过,我们很快就发现,没有这么简单,如果我们这样调用。
// 查询 id 为 1 的发送者 与 id 为 2 的接收者的聊天记录
db.select(db.tb_chat, 'uid_uid2', [1, 2]).then(res => {
console.log(res)
})
即使聊天记录有多条,我们会发现只有一条记录查询出来,很明显无法符合我们的业务需求,因此我们需要用到游标,记得需要查询多条记录出来的场景就要用到游标。这个时候我们再封装一个游标查询。
游标查询
什么叫游标,游标就是指向数据集合的特定行的一个指针,我们可以通过游标来对查询的结果集进行遍历并将数据放到数组里,把数组 return 出去。
// 根据索引游标查询
selectList(tb, index, content) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb]).objectStore(tb).index(index)
// 创建游标
var c = request.openCursor(IDBKeyRange.only(content))
var arr = []
c.onsuccess = (event) => {
var cursor = event.target.result
if (cursor) {
arr.push(cursor.value)
cursor.continue()
} else {
resolve(arr)
}
}
c.onerror = (error) => {
reject(error)
}
})
}
调用
// 查询 id 为 1 的发送者 与 id 为 2 的接收者的聊天记录
db.selectList(db.tb_chat, 'uid_uid2', [1, 2]).then(res => {
console.log(res)
})
游标查询还可以控制查询条件范围,比如,查询“2019年~2021年”的聊天记录。只要使用好关键代码 IDBKeyRange 的方法,还可以做其他炫酷的查询,在这里就不一一详述查询方法。
修改数据表结构
有时候,我们需要修改数据表结构,添加、删除、更改数据表或索引,这个时候 window.indexedDB.open 和 onupgradeneeded 方法尤其重要。
假设我们新增一个索引“发送者昵称”:nickname。
首先,我们先要修改数据库的版本号,比以前的版本号大即可。
class DB {
constructor() {
this.db = null // 内部的数据库对象
this.tb_chat = 'chat' // 这个是表名
//this.version = 1 // 旧版本号
this.version = 2 // 新版本号
}
...
}
然后再修改 onupgradeneeded 方法
request.onupgradeneeded = function (event) {
this.db = event.target.result
var objectStore
// 如果没有聊天 chat 表(对象仓库),则创建 chat
if (!this.db.objectStoreNames.contains(that.tb_chat)) {
objectStore = this.db.createObjectStore(that.tb_chat, { keyPath: 'id' }) // 创建表 chat,并指定 id 为主键
objectStore.createIndex('id', 'id', { unique: true }) // 创建 id 索引,具有唯一性
objectStore.createIndex('uid', 'uid', { unique: false }) // 创建 发送者id 索引
objectStore.createIndex('uid2', 'uid2', { unique: false }) // 创建 接收者id 索引
objectStore.createIndex('sendTime', 'sendTime', { unique: true }) // 发送时间 索引
objectStore.createIndex('uid_uid2', ['uid', 'uid2'], { unique: false }) // 发送者—接收者 索引
objectStore.createIndex('nickname', 'nickname', { unique: false }) // 发送者昵称 索引
}
}
重新 init 数据库后,我们按F12,打开控制台的 application,可以发现 IndexedDB 的聊天表并没有 nickname 索引。原来 IndexedDB 的表是一旦创建好就无法修改其结构了。 我们只好弃用旧表,创建新表。在这里,我们只需要更改构造函数的表名即可(这就是为什么要特别用一个变量来存储表名的原因)。
class DB {
constructor() {
this.db = null // 内部的数据库对象
//this.tb_chat = 'chat' // 这个是旧表名
this.tb_chat = 'new_chat' // 新表名
//this.version = 1 // 旧版本号
this.version = 3 // 新版本号,版本号改为3,因为上面已经 init 过 1 次了
}
...
}
这里可能还会涉及到一个问题,就是旧表数据如有用处,则需要转移到新表里。因此,在设计 IndexedDB 的表时,一定要考虑周到。
其他
如果嫌原生代码封装麻烦,可以直接使用 dexie 库。
完整代码
db.js
class DB {
constructor() {
this.db = null // 内部的数据库对象
this.tb_chat = 'chat' // 这个是表名
this.version = 1 // 版本号
}
// 初始化
init() {
return new Promise((resolve, reject) => {
var that = this
// 打开数据库 db_chat,指定数据库版本号为 1(如果没有指定,那么默认为1)
var request = window.indexedDB.open('db_chat', this.version)
// 成功打开数据库时回调
request.onsuccess = () => {
this.db = request.result
resolve()
}
// 连接数据库时,版本号大于现有版本号或第一次创建数据库时也会回调,进行回调,可以利用版本号来更改数据结构
request.onupgradeneeded = function (event) {
this.db = event.target.result
var objectStore
// 如果没有聊天 chat 表(对象仓库),则创建 chat
if (!this.db.objectStoreNames.contains(that.tb_chat)) {
objectStore = this.db.createObjectStore(that.tb_chat, {
keyPath: 'id'
}) // 创建表 chat,并指定 id 为主键
objectStore.createIndex('id', 'id', {
unique: true
}) // 创建 id 索引,具有唯一性
objectStore.createIndex('uid', 'uid', {
unique: false
}) // 创建 发送者id 索引
objectStore.createIndex('uid2', 'uid2', {
unique: false
}) // 创建 接收者id 索引
objectStore.createIndex('sendTime', 'sendTime', {
unique: true
}) // 发送时间 索引
objectStore.createIndex('uid_uid2', ['uid', 'uid2'], {
unique: false
}) // 发送者—接收者 索引
}
}
// 打开数据库失败时回调
request.onerror = (error) => {
reject(error)
}
})
}
// 新增数据(返回主键)
add(tb, data) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb], 'readwrite').objectStore(tb).add(data)
request.onsuccess = (event) => {
resolve(event.target.result)
}
request.onerror = (event) => {
reject(event)
}
})
}
// 根据主键删除数据
delete(tb, key) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb], 'readwrite')
.objectStore(tb)
.delete(key)
request.onsuccess = (event) => {
resolve(event)
}
request.onerror = (error) => {
reject(error)
}
})
}
// 根据主键修改数据
update(tb, data) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb], 'readwrite')
.objectStore(tb)
.put(data)
request.onsuccess = (event) => {
resolve(event)
}
request.onerror = (error) => {
reject(error)
}
})
}
// 根据主键查询数据
selectById(tb, key) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb])
.objectStore(tb)
.get(key)
request.onsuccess = (event) => {
if (request.result) {
resolve(request.result)
} else {
resolve()
}
}
request.onerror = (error) => {
reject(error)
}
})
}
// 根据索引查询
select(tb, index, content) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb])
.objectStore(tb)
.index(index)
.get(content)
request.onsuccess = (event) => {
if (request.result) {
resolve(request.result)
} else {
resolve()
}
}
request.onerror = (error) => {
reject(error)
}
})
}
// 根据索引查询列表
selectList(tb, index, content) {
return new Promise((resolve, reject) => {
var request = this.db.transaction([tb]).objectStore(tb).index(index)
// 创建游标
var c = request.openCursor(IDBKeyRange.only(content))
var arr = []
c.onsuccess = (event) => {
var cursor = event.target.result
if (cursor) {
arr.push(cursor.value)
cursor.continue()
} else {
resolve(arr)
}
}
c.onerror = (error) => {
reject(error)
}
})
}
}
let db = new DB()
export default db
调用
import db from db.js
db.init().then(() => {
db.add(db.tb_chat, {id:1, uid:1, uid2:2, sendTime: 1622539418861, content:'hi'})
db.selectList(db.tb_chat, 'uid_uid2', [1, 2]).then(res => {
console.log(res)
})
db.delete(db.tb_chat, 1)
})