IndexedDB教程

  • Post author:
  • Post category:其他




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) 事务

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

readwrite



readonly



versionchange



三、API 介绍

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



3.1 打开数据库

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

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

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

1


indexedDB.open()

方法返回一个 IDBRequest 对象。这个对象通过三种事件

error



success



upgradeneeded

,处理打开数据库的操作结果。

error



success

事件表示打开数据库失败、成功,如果

数据库不存在

或者指定打开的版本大于实际的数据库版本,就会触发数据库升级事件

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: 'zhangsan@example.com' });

  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

。一个名为

studentInfo



object store

记录学生的身份信息,以学生的

id

属性作为主键,同时对

age



name

字段建立索引。另一个名为

score



object 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

函数的函数体。

2022-12-04-14-02-53


openDatabase

函数返回一个 Promise 对象,可异步获取打开的数据库对象实例。如果打开的对象不存在或者数据库版本号比实际版本大,将会触发

upgradeneeded

事件,在该事件中,首先删除旧有版本的所有

object store

,然后建立新版本的数据库。



4.1.3 参数说明



  • dbName:string

数据库名。



  • dbVersion:number

数据库版本号。



  • dbStores:Array<DBStoreType>

数据库信息,可以有多个

object store

,单个

object store

可声明多个索引。

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

上面代码中,可以看到

DBStoreType



dbStore



dbIndexs

属性组成。



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>

2022-12-04-14-41-26

2022-12-04-14-45-44



4.2.2 定义和用法


DBObjectStore

类初始化时,获得了数据库对象实例

database

以及要操作的

object store

名。

2022-12-04-14-55-07


DBObjectStore

是一个泛型类,需要传递类型作为参数,以约束

object store

中存储数据的

key



value

的类型。


注意

:当不设置主键时,使用默认递增的主键,它的类型为 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<Event>

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

增加还是修改取决于

object store

中是否含有该键指向的数据。

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

2022-12-04-15-09-42



  • putBulk(value: Array<V>): Promise<Event>

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

2022-12-04-15-19-26



  • delete(query: K | IDBKeyRange): Promise<Event>

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

2022-12-04-15-22-30



  • get(query: K): Promise<V>

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

2022-12-04-15-28-02



  • getRange(query: IDBKeyRange): Promise<Array<V>>

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

2022-12-04-15-29-12



  • getAll(): Promise<Array<V>>

查询当前

object store

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

2022-12-04-15-29-51



  • getByIndex(indexName: string, key: IDBValidKey): Promise<V>

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

2022-12-04-15-31-12



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

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

2022-12-04-15-36-59



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

迭代

object store

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

iterateCall

完成

2022-12-04-15-41-05



  • clearData(): Promise<Event>

清空当前

object store

中的数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。

2022-12-04-15-43-25



五、数据库安全与管理



5.1 安全

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

mdn

文档:


Third party window content (e.g. <iframe> content) cannot access IndexedDB if the browser is set to

never accept third party cookies

(see

bug 1147821

.)

这段英文不太能理解,另外我又在微软的技术文档里找到了另一种表述。

2022-12-04-21-16-56

经过实验,发现是页面能访问 <iframe> 中的 indexedDB,但是<iframe>无法访问页面的 indexedDB。



5.2 数据库管理

indexedDB 创建的数据库内容始终是保留在用户端的,每当我们想改变数据库的结构,就要考虑用户端本地原来保存的是什么样的数据库。

最简单的方法,就是在数据库版本提升时,清空旧的 object store。这样当用户再次访问我们网站时,就会执行这段脚本,清空旧数据。

当某个数据库弃用时也需要,写一段脚本去删除。

(window as any)._indexedDB.deleteDatabase('databaseName');

数据库结构对应 openDatabase 函数的 dbStores:Array<DBStoreType> 参数,当其改变时,提升打开的数据库版本号以触发 onupgradeneeded 事件。



参考链接

  • 浏览器数据库 IndexedDB 入门教程,阮一峰,https://www.ruanyifeng.com/blog/2018/07/indexeddb.html
  • IndexedDB API,mdn web docs,https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API



版权声明:本文为w1405270578原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。