indexDb 标准的本地数据库
特点
优点
- 存储容量:分配本地磁盘 1/3 的空间
- 数据格式:直接存储 js 数据,包括二进制数据流 Blob
- 索引:提高搜索性能
- 事务:保证数据准确性和一致性,这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 异步:避免操作大量数据时,导致浏览器锁死
- 同源策略:每个数据库对应创建时的域名,网页不能跨域访问只能访问自域名下的数据库。
缺点
- 书写繁琐,对没有数据库概念的小白,入门较高
主要 api
数据库:IDBDatabase 对象
数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。
IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。
对象仓库:IDBObjectStore 对象
每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格。
索引: IDBIndex 对象
为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
事务: IDBTransaction 对象
数据记录的读写和删改,都要通过事务完成。事务对象提供 error、abort 和 complete 三个事件,用来监听操作结果。
操作请求:IDBRequest 对象
指代打开 indexdb 数据库后的对象,表示数据库示例对象
指针: IDBCursor 对象
表示当前文档集合对象,所谓的指针就是指被操作的数据表
主键集合:IDBKeyRange 对象
一般指定为 id,这个是一个唯一的标记,用于作为检索数据的唯一身份,相当于 vue 里的 v-for 循环的 key
使用 indexDB 步骤
数据库操作 3 步骤
- 打开数据库 DB
- 在 versionChange 事件中 创建表(ObjectStore),包括定义表的键,索引规则等。
- 操作数据(增删改查)
操作数据库 4 部分
- 开启事务
- 获取事务中的 objectStore
- 通过 objectStore 发起操作请求
- 定义请求的回调函数
代码示例
连接数据库
indexedDB.open()
// version 版本号,用于未来指定连接的数据库版本,如果不传默认为数字1
var version = 1;
var request = window.indexedDB.open("MyDbName", version);
// 数据库如果不存在就是创建,存在就是连接
// request是IDBRequest对象,它有三个事件用于处理打开数据库的操作
var db = null;
request.onsuccess = function (event) {
// 数据库打开/创建成功
// 成功后把数据库实例对象赋给db
db = event.target.result;
// event.target.result === request.result
};
request.onerror = function (event) {
// 数据库打开/创建报错
};
创建数据表
// upgradeneeded如果指定的版本号大于数据库实际版本,就会触发数据库升级事件,另外创建数据库的时候也会触发。
request.upgradeneeded = function (event) {
db = event.target.result; // 这里赋值一次db实例对象
// 在这里我们创建数据表,这里的数据表也可以叫仓库,数据库,它只是一个概念
// 如果把indexdb叫数据库,那么MyDbName就是数据库里一个仓库,我个人更愿意把存放数据的叫数据表
// 创建前先判断是不是已经存在该数据表里
var objectStore;
if (!db.objectStoreNames.contains("person")) {
// 不存在则创建一个名叫person的数据表,并且指定主键名叫id,并且自动增量,id的值是数字,从1开始
objectStore = db.createObjectStore("person", {
keyPath: "id",
autoIncrement: true,
});
// 然后我们指定索引字段,这样就可以在搜索的时候用索引去查,不然只能通过id去查
// 一维对象类型的数据,指定索引name
objectStore.createIndex("name", "name", { unique: false });
// 有联合索引,唯一索引,对数组字段建索引
objectStore.createIndex("index_name", ["field1", "field2", "field3"], {
unique: true,
});
}
};
关于索引
业务事项
处理业务,增删查改使用事务 apitransaction
来进行,那么这里说一下事务中的权限参数类型:readonly
只读;readwrite
读写。两种权限。
增
/*
创建事务
1. 第一个参数指明事务所涉及的objectStores,如果只有一个objectStore,[]可以省略,本例可以直接写 'person'
2. 第二参数指明事务操作数据的方式,如不写 默认是 readonly,表示只能读数据 不能写。如果不仅仅是读,还有增删改数据,必须用 readwrite
3. 很多人可能有疑惑为什么还要再objectStore('person')一下,是因为只有在事务中objectStore才能获取,可以粗暴的理解为回指,也只有这样才能完成后续的业务操作,db是不可以直接操作的
*/
let result = transaction(['person'],'readwrite').objectStore('person').add({
name: "xxx“
})
result.onsuccess = function (event) {
console.log(event.target.result)
}
result.onerror = function (event) {
console.log(event.target.result)
}
删
// 只能使用id进行操作
const id = 1;
const result = db
.transaction("person", "readwrite")
.objectStore("person")
.delete(id);
result.onsuccess = function (event) {
// 删除操作的返回追是undefined
console.log(event.target.result);
};
result.onerror = function (event) {
console.log(event.target.result);
};
查
查询是最最重要的,所以有三点需要掌握:
- 索引和 key 的操作形式(传递参数的形式,查询条件的形式)是一模一样的
- IDBIndex 和 ObjectStore 的各种 api:
get, getKey, getAll, getAllKeys, openCursor, openKeyCursor
里面都可以传入条件,也可以不传,条件可以是 key 的或索引的特定值或范围。 - 需要一次操作多个数据的情况很常见,但是并不提倡直接
getAll( condition )
或getAllKeys( condition )
这样的操作,思考一下它的性能,以及占用的内存资源你就明白了——我们更多采用的是游标(cursor)
。
鉴于所有操作都基本相同,所以接下来举一个常见的使用游标且稍微有点难的查询场景!开始之前:
// 回顾 前面定义的索引:(索引必须先创建再使用)
// 一维对象类型的数据,指定索引name
objectStore.createIndex("name", "name", { unique: false });
// 有联合索引,唯一索引,对数组字段建索引
objectStore.createIndex("index_name", ["field1", "field2", "field3"], {
unique: true,
});
查询单个数据
// 查询没有嵌套的普通对象类型数据
const result = db
.transaction("person", "readonly")
.objectStore("person")
.index("name")
.get("宋宇");
result.onsuccess = function (event) {
console.log(event.target.result);
};
result.onerror = function (event) {
console.log(event.target.result);
};
// 查询数组嵌套的
const dbIndex = db
.transaction("person", "readonly")
.objectStore("person")
.index("index_name");
// 注意: 下面传入索引值的语法规则,v1 对应字段 field1,v2 对应字段 field2, v3 对应字段 field3
// 注意:如果索引不是unique的(unique索引get最多当然只会得到一条数据),有可能有多条对应的数据,这时get只会得到最小key的数据。获取所有数据要使用 getAll
dbIndex.get([v1, v2, v3]).onsuccess = (e) => {
let data = e.target.result; // 得到符合条件的数据
};
查找范围内多条数据
使用 IDBKeyRange 查询范围内的多个数据:
// 游标查询范围内的多个:
const range = IDBKeyRange.bound([min1, min2, min3], [max1, max2, max3]); // 除了bound 还有 only,lowerBound, upperBound 方法,还可以指明是否排除边界值
dbIndex.openCursor(range, "prev").onsuccess = (e) => {
// 传入的 prev 表示是降序遍历游标,默认是next表示升序;
//如果索引不是unique的,而你又不想访问重复的索引,可以使用nextunique或prevunique,这时每次会得到key最小的那个数据
let cursor = e.target.result;
if (cursor) {
let data = cursor.value; // 数据的处理就在这里。。。 [ 理解 cursor.key,cursor.primaryKey,cursor.value ]
cursor.continue();
} else {
// 游标遍历结束!
}
};
需要说明的是IDBKeyRange.bound([min1, min2, min3], [max1, max2, max3])
到底是什么样的范围?如下:
(field1 > min1 ||
(field1 === min1 && field2 > min2) ||
(field1 === min1 && field2 === min2 && field3 >= min3)) &&
(field1 < max1 ||
(field1 === max1 && field2 < max2) ||
(field1 === max1 && field2 === max2 && field3 <= max3));
// 好好理解一下这个 bound 的含义吧 !
改
// put修改,如果找到对应的id为1的就替换,没有就插入
const result = db
.transaction("student", "readwrite")
.objectStore("student")
.put(
// 修改的时候千万记得把修旧值合并,不然就是新值替换旧指,会丢失;
{ id: 1, name: "宋宇", ...oldData }
);
result.onsuccess = function ({ target }) {
console.log(target.result);
};
result.onerror = function ({ target }) {
console.log(target.result);
};
关于事务中为何要 objectStore 回指
业务需求:我需要对 person 表做插入,同时还要对 student 表做修改,如果我们同时执行两个事务就会报错。
先来一个错误的示范
// 注意这里是错误示范,person表做插入,同时还要对student表做修改,不能保证都同时成功
const person = db.transaction("person", "readwrite").objectStore("person");
const student = db.transaction("student", "readwrite").objectStore("student");
person.add(data).onsuccess = () => {
console.log("ok");
};
student.delete(processId).onsuccess = () => {
console.log("ok");
};
上面会报错,是因为程序执行异常,我们知道 indexdb 是异步执行的,那么这时候就可以知道官方为何要给出一个 objectStore 的好处了。
const trans = db.transaction(["person", "student"], "readwrite");
const person = trans.objectStore("person");
const student = trans.objectStore("student");
// 下方两个操作请求共用了一个事务trans,必须同时成功,否则就失败(即使成功了的请求,数据也将会回滚)
person.add(data).onsuccess = () => {
console.log("ok");
};
student.delete(processId).onsuccess = () => {
console.log("ok");
};