IndexedDB 是一种大容量客户端存储方案,它可以存储结构化数据,包括 FileBlob 等二进制数据,比如图片、视频等。

localStorage 最多存储 5MiB 的容量,IndexedDB 的容量要大得多。按照 MDN 的说法,Chrome 浏览器可用的存储容量是总磁盘的 60%。假如你的硬盘总容量是 1TB,那么可用的 IndexedDB 上限是 600GB。

IndexedDB 是一种键值数据库,属于非关系型数据库。它通过简单的键值对(key-value)存储数据,存取灵活,性能高。

IndexedDB 的 API 多为异步风格,好处是不会阻塞界面渲染,坏处是可读性差,写起来费劲。

打开数据库

使用数据库的第一步是打开数据库。

使用 indexedDB.open(name, version) 方法打开数据库。name 表示数据库的名称,version 表示数据库的版本。返回值是一个 IDBOpenDBRequest 类型的实例。

const dbName = 'codeman'
const dbVersion = 1
const req = indexedDB.open(dbName, dbVersion)

version 是数据库的版本号,用于数据库的创建和升级,它是整数类型,默认为 1。传入不同的版本号,可能会出现以下三种后果之一:

一、如果数据库不存在,或 open() 中的版本号大于数据库当前版本号,会触发 reqonupgradeneeded 回调函数。在回调函数内可以创建或调整数据库存储结构,如果执行顺利,会接着触发 reqonsuccess 回调函数。

二、如果 open() 的版本号小于数据库当前版本号,会抛出错误,触发 reqonerror 回调函数。

三、如果 open() 中的版本号等于数据库当前版本号,只触发 reqonsuccess 回调函数。

在回调函数中,变量 e.target 指向发起请求的 req 对象,如果连接成功,它的 result 属性就是期望的数据库连接对象,类型是 IDBDatabase

const dbName = 'codeman'
const dbVersion = 2

// 数据库连接对象
let db;
const req = window.indexedDB.open(dbName, dbVersion)

// 监听错误
req.onerror = (e) => {
  console.error('error', e.target.error?.message)
}

req.onupgradeneeded = (e) => {
  console.log('upgrade needed', e)
  db = e.target.result
  // TODO: 创建或调整数据库的存储结构
}

// 数据库连接成功
req.onsuccess = (e) => {
  console.log('success', e)
  db = e.target.result
}

数据库连接对象很重要,无论是创建对象存储,还是使用事务,都离不开它。通常会把它存储为独立的变量(如上文中的 db)。

创建对象存储

在关系型数据库中,一个数据库可以包含多个表格(Table)。在 IndexedDB 中,一个数据库可以包含多个对象存储object store)。可以把对象存储想象成一个超大的数组,其中可以存储对象类型的数据。

使用数据库连接对象的 createObjectStore(name, options) 方法创建对象存储。

其中的 name 是对象存储的名称,options 是存储选项。常用的选项属性是 keyPath,用于指定使用对象的哪个属性当作键。

// ...

req.onupgradeneeded = (e) => {
  db = e.target.result
  createStore(db)
}

/**
* 创建对象存储
* @param {IDBDatabase} db
*/
function createStore(db) {
  const store = db.createObjectStore('tasks', { keyPath: 'id' })
}

增删改查

添加数据

在 IndexedDB 中添加数据必须使用事务(transaction)。事实上,所有的 IndexedDB 操作(增删改查)都离不开事务。

事务是一个数据库术语,它指的是一系列数据库操作,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务的主要目的是确保数据库的一致性和完整性。

在 IndexedDB 中,事务的具体类型是 IDBTransaction,来自数据库连接对象的 transaction(storeNames, mode) 方法。

其中的参数含义如下:

  • storeNames 本次事务操作涉及的对象存储名称,数组类型。如果只涉及一个,也可以传入字符串类型
  • mode 本次事务操作涉及的工作模式,可选值有 "readonly"(只读)、 "readwrite"(读和写)等

因为我们要添加数据,所以需要选择的模式为 "readwrite"

const transaction = db.transaction(['tasks'], 'readwrite')
// 或者
const transaction = db.transaction('tasks', 'readwrite')

拿到事务后,使用它的 objectStore(name) 方法获取即将操作的对象存储实例。其中的 name 是存储实例的名称。

const transaction = db.transaction('tasks', 'readwrite')
const objectStore = transaction.objectStore('tasks')

存储实例的 add(value) 方法用于向数据库添加数据。添加数据是异步操作,返回值是一个请求对象。需要监听它的 onsuccessonerror 才能确定成功或失败。

把前面的逻辑组合起来,添加数据的完整代码如下:

req.onsuccess = (e) => {
  db = e.target.result
  addItem(db)
}

/**
* 添加一条数据
* @param {IDBDatabase} db
*/
function addItem(db) {
  // 创建一个事务,划定受影响的对象存储范围和工作模式
  const transaction = db.transaction('tasks', 'readwrite')
  // 选中一个对象存储
  const objectStore = transaction.objectStore('tasks')
  // 添加数据
  const addReq = objectStore.add({
    id: 1,
    title: 'Learn IndexedDB usage',
    completed: false
  })

  addReq.onerror = (e) => {
    console.error('add item error', e.target.error?.message)
  }

  addReq.onsuccess = (e) => {
    console.log('add item success', e)
  }
}

查询数据

查询数据和添加数据的主流程基本一致,但是需要调整工作模式为 "readonly"

如果你想查询所有的数据,可以使用对象存储实例的 getAll() 方法。查询结果在 onsuccess 回调函数中,通过 e.target.result 获取。

req.onsuccess = (e) => {
  db = e.target.result
  getAllItems(db)
}

/**
* 查询所有数据
* @param {IDBDatabase} db
*/
function getAllItems(db) {
  const transaction = db.transaction('tasks', 'readonly')
  const objectStore = transaction.objectStore('tasks')
  const getReq = objectStore.getAll()

  getReq.onerror = (e) => {
    console.error('get item error', e.target.error?.message)
  }

  getReq.onsuccess = (e) => {
    console.log('get all items success', e.target.result)
  }
}

如果你想根据键值查询某一个对象,使用 get(key) 方法。

const getReq = objectStore.get(1)

// 略

更新数据

更新数据,使用对象存储实例的 put(value) 方法。它和 add() 的用法类似,只不过会按照键更新原有数据,而非新增数据。

const putReq = objectStore.put({
    id: 1,
    title: 'Learn IndexedDB usage',
    completed: true
})

删除数据

如果想根据键删除某个数据,可以使用 delete(key) 方法。

const deleteReq = objectStore.delete(1)

如果想删除对象存储的所有数据,使用 clear() 方法。

const clearReq = objectStore.clear(1)

索引

pass

游标

pass

Dixie.js

Dixie.js 是一个 IndexedDB 的封装库,体积不大 25k。

pass