IndexedDB教程

IndexedDB教程

一、概述

IndexedDB 是浏览器提供的本地数据库,js原生支持创建和操作 IndexedDB。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。就数据库类型而言,IndexedDB 不属于关系型数据库,更接近 NoSQL 数据库(存储key-value)。

它具有如下特点:

  • 键值对储存
  • 异步。IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作。
  • 支持事务。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
  • 同源限制。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
  • 存储空间大。在默认使用短暂存储情况下,一组域名受到组限制,共享最小10MB,最大2GB的存储空间。
  • 支持二进制存储。如 ArrayBuffer 对象和 Blob 对象

二、基本概念

IndexedDB 是一个比较复杂的 API,涉及不少概念。它把不同的实体,抽象成一个个对象接口。学习这个 API,就是学习它的各种对象接口。

* 数据库:IDBDatabase 对象
* 对象仓库:IDBObjectStore 对象
* 索引: IDBIndex 对象
* 事务: IDBTransaction 对象
* 操作请求:IDBRequest 对象
* 指针: IDBCursor 对象
* 主键集合:IDBKeyRange 对象

下面是一些主要的概念。

(1) 数据库

数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。

IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除object store、索引或者主键),只能通过升级数据库版本完成。

(2) 对象仓库

每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表。

(3) 数据记录

对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。

{ id: 1, text: 'foo' }

上面是一个object store 中的一条数据,我们可以指定id属性作为该object store的主键,这条数据的数据体就是{ id: 1, text: 'foo' }

(4) 索引

为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。

(5) 事务

数据的增删改查、数据库版本的升级都要通过事务完成,总共有三种事务模式:readwritereadonlyversionchange

三、API 介绍

下面通过具体操作数据库时的流程,介绍相关 API。

3.1 打开数据库

使用 IndexedDB 的第一步是打开数据库,使用indexedDB.open()方法。

var request = window.indexedDB.open(databaseName, version);

这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1

indexedDB.open()方法返回一个 IDBRequest 对象。这个对象通过三种事件errorsuccessupgradeneeded,处理打开数据库的操作结果。errorsuccess事件表示打开数据库失败、成功,如果数据库不存在或者指定打开的版本大于实际的数据库版本,就会触发数据库升级事件upgradeneeded。这时通过事件对象的target.result属性,拿到数据库实例。

var db;

request.onsuccess = function (event) {
  db = request.result;
  console.log('数据库打开成功');
};

request.onerror = function (event) {
  console.log('数据库打开报错');
};

request.onupgradeneeded = function (event) {
  db = event.target.result;
}

注意:版本号是 unsigned long long 类型,不是浮点型,不能使用 2.4 作为版本号。

3.2 新建数据库

新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。

通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
}

上面代码中,数据库新建成功以后,新增一张叫做person的表格,主键是id。如果数据记录里面没有合适作为主键的属性,那么可以让 IndexedDB 自动生成主键。

var objectStore = db.createObjectStore(
  'person',
  { autoIncrement: true }
);

上面代码中,指定主键为一个递增的整数。

新建对象仓库以后,下一步可以新建索引。

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
  objectStore.createIndex('name', 'name', { unique: false });
  objectStore.createIndex('email', 'email', { unique: true });
}

上面代码中,IDBObject.createIndex()的三个参数分别为索引名称、索引所在的属性、配置对象(unique属性表示是否包含重复的值)。

3.3 数据操作

这里只介绍新增数据,其余操作与此类似,具体使用参考下一章节在项目中封装使用
新增数据指的是向对象仓库写入数据记录。这需要通过事务完成。

function add() {
  var request = db.transaction('person', 'readwrite')
    .objectStore('person')
    .add({ id: 1, name: '张三', age: 24, email: '[email protected]' });

  request.onsuccess = function (event) {
    console.log('数据写入成功');
  };

  request.onerror = function (event) {
    console.log('数据写入失败');
  }
}

add();

上面代码中,写入数据需要新建一个事务。新建时必须指定表格名称和操作模式(“只读"或"读写”)。新建事务以后,通过IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 对象,再通过表格对象的add()方法,向表格写入一条记录。

写入操作是一个异步操作,通过监听连接对象的success事件和error事件,了解是否写入成功。

3.4 使用索引

索引的意义在于,可以让你按任意字段搜索数据,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能按主键搜索。

假定新建表格的时候,对name字段建立了索引。

objectStore.createIndex('name', 'name', { unique: false });

现在,就可以从name找到对应的数据记录了。

var transaction = db.transaction('person', 'readonly');
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');

request.onsuccess = function (e) {
  var result = e.target.result;
  if (result) {
    // ...
  } else {
    // ...
  }
}

四、在项目中封装使用

4.1 打开(新建)数据库,DBInstance.ts: openDatabase

使用数据库的第一步就是打开或新建一个数据库

4.1.1 实例

创建一个学生数据库,其中有两个object store。一个名为studentInfoobject store记录学生的身份信息,以学生的id属性作为主键,同时对agename字段建立索引。另一个名为scoreobject store记录学生的分数信息,自动递增生成主键。

enum StoreName {
  Student = 'studentInfo',
  Score = 'score'
}
const studentSchema: Array<DBStoreType> = [
  {
    dbStore: {
      dbStoreName: StoreName.Student,
    },
    dbIndex: [
      {
        dbIndexName: 'age',
        keyPath: 'age',
      },
      {
        dbIndexName: 'name',
        keyPath: 'name'
      }
    ]
  },
  {
    dbStore: {
      dbStoreName: StoreName.Score,
      options: {autoIncrement: true}
    }
  }
]
const studentDatabase = await openDatabase('student', 1, studentSchema);
4.1.2 定义和用法

下面是openDatabase函数的函数体。

IndexedDB教程_第1张图片

openDatabase函数返回一个 Promise 对象,可异步获取打开的数据库对象实例。如果打开的对象不存在或者数据库版本号比实际版本大,将会触发upgradeneeded事件,在该事件中,首先删除旧有版本的所有object store,然后建立新版本的数据库。

4.1.3 参数说明
  • dbName:string

数据库名。

  • dbVersion:number

数据库版本号。

  • dbStores:Array

数据库信息,可以有多个object store,单个object store可声明多个索引。

type DBStoreType = {
  dbStore: DBStoreParameter;
  dbIndexs?: Array;
}
type DBStoreParameter = {
  dbStoreName: string;
  options?: IDBObjectStoreParameters;
}
type DBIndexParameter = {
  dbIndexName: string;
  keyPath: string | string[];
  options?: IDBIndexParameters;
}

上面代码中,可以看到DBStoreTypedbStoredbIndexs属性组成。

4.2 Object Store 封装类,DBObjectStore.ts: DBObjectStore

4.2.1 实例

实例化DBObjectStore,对studentDatabase中的student表进行各种数据操作,包括增删改查,迭代。

type StudentInfo = {
  id: number;
  name: string;
  age: number;
}
const StudentStore = DBObjectStore<[number],StudentInfo>

IndexedDB教程_第2张图片
IndexedDB教程_第3张图片

4.2.2 定义和用法

DBObjectStore 类初始化时,获得了数据库对象实例database以及要操作的object store名。

IndexedDB教程_第4张图片

DBObjectStore是一个泛型类,需要传递类型作为参数,以约束object store中存储数据的keyvalue的类型。

注意:当不设置主键时,使用默认递增的主键,它的类型为 number。设置主键后,value 的类型必须为 object 类型,不能是 number, string 等类型。

4.2.3 DBObjectStore 方法介绍
  • getStore(storeName: string, mode? IDBTransactionMode)

返回 object store,准备开始操作数据,数据操作在指定模式下的事务中进行。不知道事务模式,默认为 readonly。

2022-12-04-21-43-20

  • put(value: V, key? K): Promise

增加或修改数据,返回一个 Promise 对象,增加或修改成功传递 Event 事件参数回调。

增加还是修改取决于object store中是否含有该键指向的数据。

如果设置了指定字段作为主键,那么就不需要使用第二个参数,value 中已包含该主键值。未指定主键,按默认主键,如果设置为主键递增增加,则第二参数可要可不要。

IndexedDB教程_第5张图片

  • putBulk(value: Array): Promise

批量一次性添加多条数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。

2022-12-04-15-19-26

  • delete(query: K | IDBKeyRange): Promise

删除一条数据或多条数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。

IndexedDB教程_第6张图片

  • get(query: K): Promise

按主键查询一条数据。返回一个 Promise 对象,成功则传递查询结果回调。

IndexedDB教程_第7张图片

  • getRange(query: IDBKeyRange): Promise>

按主键范围返回查询数据。返回一个 Promise 对象,成功则传递查询结果回调。

IndexedDB教程_第8张图片

  • getAll(): Promise>

查询当前object store的所有数据。返回一个 Promise 对象,成功则传递查询结果回调。

IndexedDB教程_第9张图片

  • getByIndex(indexName: string, key: IDBValidKey): Promise

按索引中的 key 查询一条数据。返回一个 Promise 对象,成功则传递查询结果回调。

IndexedDB教程_第10张图片

  • getRangeByIndex(indexName: string, key: IDBKeyRange): Promise>

按索引中的 key范围查询多条数据。返回一个 Promise 对象,成功则传递查询结果回调。

IndexedDB教程_第11张图片

  • iterate(iterateCall: (value: V) => void,query?:K | IDBValidKey | IDBKeyRange | null, indexName?: string, direction?: IDBCursorDirection): Promise>

迭代object store或索引,可指定迭代范围,不指定迭代所有,也可指定迭代方向。迭代操作,通过传入的iterateCall完成

IndexedDB教程_第12张图片

  • clearData(): Promise

清空当前object store中的数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。

IndexedDB教程_第13张图片

五、数据库安全与管理

5.1 安全

IndexedDB遵守同源原则,这意味着某个源创建的数据库只能在该源里访问。但是也有特殊情况,下面这句话来自mdn文档:

Third party window content (e.g.