indexdb 浏览器数据库

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 步骤

  1. 打开数据库 DB
  2. 在 versionChange 事件中 创建表(ObjectStore),包括定义表的键,索引规则等。
  3. 操作数据(增删改查)

操作数据库 4 部分

  1. 开启事务
  2. 获取事务中的 objectStore
  3. 通过 objectStore 发起操作请求
  4. 定义请求的回调函数

代码示例

连接数据库

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

查询是最最重要的,所以有三点需要掌握:

  1. 索引和 key 的操作形式(传递参数的形式,查询条件的形式)是一模一样的
  2. IDBIndex 和 ObjectStore 的各种 api:get, getKey, getAll, getAllKeys, openCursor, openKeyCursor里面都可以传入条件,也可以不传,条件可以是 key 的或索引的特定值或范围。
  3. 需要一次操作多个数据的情况很常见,但是并不提倡直接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");
};

最后

官方 api
本文部分资料参考
本文部分资料参考




如果你遇到了前端难题,或者需要一对一帮扶服务,请到淘宝搜索店铺:前端在线或扫下面二维码

  转载规则


《indexdb 浏览器数据库》宋宇采用知识共享署名 4.0 国际许可协议进行许可。
  目录