indexDB 不完全指南

介绍

引用 MDN 的介绍:

IndexedDB 是一个事务型数据库系统,类似于基于 SQL 的 RDBMS。 然而不同的是它使用固定列表,IndexedDB是一个基于JavaScript的面向对象的数据库。 IndexedDB允许您存储和检索用键索引的对象; 可以存储structured clone algorithm支持的任何对象。 您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务中的数据。

IndexedDB 是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB 提供了一个解决方案。

区别

这是我整理的 WebStorage 和 indexDB 的之间区别,有问题的地方还请指出。

用法

1. 打开数据库

const DB_NAME = 'Netease'
const DB_VERSION = 1
const OB_NAMES = {
  UseKeyPath: 'UseKeyPath',
  UseKeyGenerator: 'UseKeyGenerator'
}
/**
  * NOTE:
  * 1. 第一次打开可能会提示用户获取 indexDB 的权限
  * 2. 浏览器隐身模式不会存在本地,只会存储在内存中
  */
const request = window.indexedDB.open(DB_NAME, DB_VERSION)

indexDB.open 接收两个参数,分别为数据库名称和版本,返回的是一个 IDBOpenDBRequest 对象。可以以 DOM 事件的方式监听它的 success 和 error 来获取到它的结果。几乎所有对 indexDB 的异步操作都是这种以事件的方式进行,返回一个拥有结果或错误的 IDBRequest 对象。在这里,open 方法得到的结果是一个 IDBDatabase 的实例。

第二个参数是数据库的版本。版本决定了数据库的模式:存储在里面的 object store 和它们的结构。当第一次通过 open 方法打开数据库时,会触发一个 onupgradeneeded 事件,我们可以也只能在这里设置数据库模式。当数据库已经存在,而我们打开一个更高版本时,同样会触发 onupgradeneeded 事件,用来更新数据库模式。

添加处理方法

我们可以通过监听它的 success, error 以及 upgradeneeded 事件来做相应的操作。

request.onerror = function (event) {
  // Do something with request.errorCode!
  console.error('open request failed',event.target.error)
}
request.onsuccess = function (event) {
  // Do something with request.result!
  // console.log('open request success', event)
  var db = event.target.result
  db.onerror = function (e) {
    console.error('Database error: ', e.target.error)
  }
  db.onclose = e => {
    console.error('Database close:', e.target.error)
  }
}

可以在 success 事件里面拿到 db 对象,这个是后续操作的主体。

错误处理

由于是基于 DOM 事件模式的,所以所有的错误是会冒泡的。也就是说在一个特定 request 的上引发的错误会依次冒泡到事务,然后到 db 对象。
如果为了简化错误处理,可以直接在 db 对象上添加错误处理:

db.onerror = function (e) {
// 可以处理这个数据库上所有的错误
console.error('Database error: ', e.target.error)
}

2. 创建或更新数据库版本

前面已经说过,当创建或者增大数据库版本的时候,会触发 onupgradeneeded 事件。在事件内部,可以拿到 db 对象来创建或更新 object store , 具体如下。

    request.onupgradeneeded = function (event) {
      /**
       * NOTE:
       * 1. 创建新的 objectStore
       * 2. 删除旧的不需要的 objectStore
       * 3. 如果需要更新已有 objectStore 的结构,需要先删除原有的 objectStore ,然后重新创建
       */
      // The IDBDatabase interface
      console.log('onupgradeneeded', event)
      var db = event.target.result
      // Create an objectStore for this database
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
    keyPath: 'time'
  })
    }

3. 构建数据库

IndexDB 是以对象存储(object store)而不是以表结构存储的,一个数据库可以存储任意多个存储对象。每当有一个值存储在 object store 里面,就必须和一个 key 关联起来。有几种提供 key 的方法,取决于 object store 使用 key path 还是 key generator.

它们之间区别,借用 MDN 的一个表格来看一下:

Key Path (keyPath)Key Generator (autoIncrement)Description
NoNoThis object store can hold any kind of value, even primitive values like numbers and strings. You must supply a separate key argument whenever you want to add a new value.
YesNoThis object store can only hold JavaScript objects. The objects must have a property with the same name as the key path.
NoYesThis object store can hold any kind of value. The key is generated for you automatically, or you can supply a separate key argument if you want to use a specific key.
YesYesThis object store can only hold JavaScript objects. Usually a key is generated and the value of the generated key is stored in the object in a property with the same name as the key path. However, if such a property already exists, the value of that property is used as key rather than generating a new key.

来自 https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB

当存储的是不是基础类型而是 js 对象的时候,我们还可以给 object store 创建索引,这样就可以通过索引属性来查找某一个具体的对象。而且, 索引还能再一定程度上约束要存储的对象。当创建索引的时候通过设置唯一标识,可以确保不会存储拥有两个相同索引值的对象。
看一个例子,假设我们有如下日志数据需要存储:

const TestData = [
  {
    event: 'NE-TEST1',
    level: 'warning',
    errorCode: 200,
    url: 'http://www.example.com',
    time: '2017/11/8 下午4:53:039',
    isUploaded: false
  },
  {
    event: 'NE-TEST2',
    msg: '测试2',
    level: 'error',
    errorCode: 1000,
    url: 'http://www.example.com',
    time: '2017/11/8 下午4:53:042',
    isUploaded: false
  },
  {
    event: 'NE-TEST3',
    msg: '测试3',
    level: 'info',
    errorCode: 3000,
    url: 'http://www.example.com',
    time: '2017/11/8 下午4:53:043',
    isUploaded: false
  },
  {
    event: 'NE-TEST4',
    mgs: '测试4',
    level: 'info',
    url: 'http://www.example.com',
    time: '2017/11/8 下午4:53:0423',
    isUploaded: false
  }
]

这里有两种存储方式,分别是通过 key path 和 key generator.

function obUseKeypath (db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
    keyPath: 'time'
  })
  objectStore.createIndex('errorCode', 'errorCode', { unique: false })
  objectStore.createIndex('level', 'level', { unique: false })
}

function obUseKeyGenerator (db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyGenerator, {
    autoIncrement: true
  })
  objectStore.createIndex('errorCode', 'errorCode', { unique: false })
  objectStore.createIndex('time', 'time', { unique: true })
  objectStore.createIndex('level', 'level', { unique: false })
}

4. 增删改查

为了方便介绍这部分的内容,我们先把上一节的代码包装一下,为了方便后续例子的讲解:

function openIndexDB () {
  // The call to the open() function returns an IDBOpenDBRequest object with a result (success) or error value that you handle as an event.
  return new Promise((resolve, reject) => {
    /**
     * NOTE:
     * 1. 第一次打开可能会提示用户获取 indexDB 的权限
     * 2. 浏览器隐身模式不会存在本地,只会存储在内存中
     */
    const request = window.indexedDB.open(DB_NAME, DB_VERSION)
    request.onerror = function (event) {
      // Do something with request.errorCode!
      console.log('open request failed', event)
      console.error(event.target.error)
    }
    request.onsuccess = function (event) {
      // Do something with request.result!
      // console.log('open request success', event)
      var db = event.target.result
      db.onerror = function (e) {
        console.error('Database error: ', e.target.error)
        reject(e.target.error)
      }
      db.onclose = e => {
        console.error('Database close:', e.target.error)
        reject(e.target.error)
      }
      resolve(db)
    }
    request.onupgradeneeded = function (event) {
      /**
       * NOTE:
       * 1. 创建新的 objectStore
       * 2. 删除旧的不需要的 objectStore
       * 3. 如果需要更新已有 objectStore 的结构,需要先删除原有的 objectStore ,然后重新创建
       */
      // The IDBDatabase interface
      console.log('onupgradeneeded', event)
      var db = event.target.result
      // Create an objectStore for this database
      obUseKeypath(db)
      obUseKeyGenerator(db)
      /**
       * NOTE:
       * transaction
       * 三个事件:
       * 1. error
       * 2. abort
       * 3. complete
       * 两个方法:
       * 1. abort
       * Rolls back all the changes to objects in the database associated with this transaction. If this transaction has been aborted or completed, then this method throws an error event.
       * 2. objectStore
       * Returns an IDBObjectStore object representing an object store that is part of the scope of this transaction.
       */
      db.transaction.oncomplete = function (e) {
        console.log('obj create success', e)
      }
    }
  })
}
function obUseKeypath (db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
    keyPath: 'time'
  })
  objectStore.createIndex('errorCode', 'errorCode', { unique: false })
  objectStore.createIndex('level', 'level', { unique: false })
}
function obUseKeyGenerator (db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyGenerator, {
    autoIncrement: true
  })
  objectStore.createIndex('errorCode', 'errorCode', { unique: false })
  objectStore.createIndex('time', 'time', { unique: true })
  objectStore.createIndex('level', 'level', { unique: false })
}

这样每次我们需要对数据库做操作的话只需要调用 openIndexDB 方法就可以了。

事务

所有对数据库的操作都是建立在事务(transaction)上的,有三种模式(mode):readonly, readewrite, versionchange.
要修改数据库结构的 schema ,必须在 versionchange 模式下。读取和修改对应另外两种模式。
通过 IDBDatabase.transaction 打开一个 transaction, 接收两个参数:storeNames, mode.
所有对数据库的操作都遵循以下流程:

  1. Get database object
  2. Open transaction on database
  3. Open object store on transaction
  4. Perform operation on object store

加速事务操作:

  • 当定义作用域时(scope), 只定义需要的 object stores. 这样,就可以在不重叠的作用域上并行的执行多个事务。
  • 只有在需要的时候才开启一个 readwrite 事务。因为在重叠的作用域上可以并发执行多个 readonly 事务,但只能有一个 readwrite 事务。

方法如下:

  1. 首先拿到 db 对象
  2. 然后打开一个 readwrite 事务
  3. 通过事务拿到 object store 对象
  4. 执行添加数据操作
/**
* 添加数据
* @param {array} docs 要添加数据
* @param {string} objName 仓库名称
*/
function addData (docs, objName) {
  if (!(docs && docs.length)) {
    throw new Error('docs must be a array!')
  }
  return openIndexDB().then(db => {
    const tx = db.transaction([objName], 'readwrite')
    tx.oncomplete = e => {
      console.log('tx:addData onsuccess', e)
      return Promise.resolve(docs)
    }
    tx.onerror = e => {
      e.stopPropagation()
      console.error('tx:addData onerror', e.target.error)
      return Promise.reject(e.target.error)
    }
    tx.onabort = e => {
      console.warn('tx:addData abort', e.target)
      return Promise.reject(e.target.error)
    }
    const obj = tx.objectStore(objName)
    docs.forEach(doc => {
      const req = obj.add(doc)
      /**
       * NOTE:
       * request
       * 两个事件:
       * 1. success
       * 2. error
       */
      // req.onsuccess = e => console.log('obj:addData onsuccess', e.target)
      req.onerror = e => {
        console.error('obj:addData onerror', e.target.error)
      }
    })
  })
}

如果要把上面的 TestData 同时使用 key generator 和 key path 方式保存到数据库中,那么方法:

addData(TestData, OB_NAMES.UseKeyGenerator)
.then(() => addData(TestData, OB_NAMES.UseKeyPath))

流程和添加一样:

/**
* 删除指定 key 的数据
* @param {string} objName 仓库名称
* @param {*} key 要删除数据的 primary key 值
*/
function deleteData (objName, key) {
  return openIndexDB().then(db => {
    const tx = db.transaction([objName], 'readwrite')
    const obj = tx.objectStore(objName)
    const req = obj.delete(key)
    req.onsuccess = e => {
      console.log(`readData success. key:${key},result:`, e.target.result)
      return Promise.resolve(e.target.result)
    }
    req.onerror = e => {
      console.error(`readData error. key:${key},error: ${e.target.errorCode}`)
      return Promise.reject(e.target.error)
    }
  })
}

假如要删除 UserKeyGenerator 里 key 为 1 值,那么:

deleteData(OB_NAMES.UseKeyGenerator, 1)
.then(doc => console.log(doc))

方法:

/**
* 读取给定 key 的数据
* @param {string} objName 仓库名称
* @param {*} key 要读取数据的 primary key 值
*/
function readData (objName, key) {
  return openIndexDB().then(db => {
    const tx = db.transaction([objName])
    const obj = tx.objectStore(objName)
    const req = obj.get(key)
    req.onsuccess = e => {
      console.log(`readData success. key:${key},result:`, e.target.result)
      return Promise.resolve(e.target.result)
    }
    req.onerror = e => {
      console.error(`readData error. key:${key},error: ${e.target.errorCode}`)
      return Promise.reject(e.target.error)
    }
  })
}

例子:

readData(OB_NAMES.UseKeyGenerator, 1)

方法:

/**
* 更新指定 key 的数据
* @param {string} objName 仓库名称
* @param {*} key 指定的 key
* @param {object} changes 要修改的属性值
*/
function updateData (objName, key, changes) {
  return openIndexDB().then(db => {
    return new Promise((resolve, reject) => {
      const tx = db.transaction([objName], 'readwrite')
      const obj = tx.objectStore(objName)
      const req = obj.get(key)
      req.onsuccess = e => {
        let doc = e.target.result
        let newDoc = Object.assign(doc, changes)
        const req = obj.put(newDoc)
        req.onsuccess = e => {
          console.log(`updateData success, newDoc:`, newDoc, e)
          resolve(e.target.result)
        }
        req.onerror = e => {
          resolve(e.target.result)
        }
      }
      req.onerror = e => {
        reject(e.target.error)
      }
    })
  })
}

例子:

updateData(OB_NAMES.UseKeyGenerator, 1, {time: '123'})
.then(doc => console.log(doc))
.catch(console.error)

使用游标 (cursor)

使用 get 方法需要预先知道一个 key. 如果需要步进整个 object store 的值,那么可以使用 cursor.

/**
* 通过 cursor 获取制定仓库下的所有数据
* @param {string} objName 仓库名称
* @param {function} [cb] 回调函数,对每次得到的 cursor 进行操作
* @returns {Promise.<array.<object>} 包含所有数据的数组
*/
function getAllByCursor (objName, cb) {
  return openIndexDB().then(db => {
    const arr = []
    const tx = db.transaction([objName])
    const obj = tx.objectStore(objName)
    return new Promise(resolve => {
      obj.openCursor().onsuccess = e => {
        const cursor = e.target.result
        if (cursor) {
          arr.push(cursor.value)
          cb && cb(cursor)
          cursor.continue()
        } else {
          return resolve(arr)
        }
      }
    })
  })
}

openCursor() 方法接收多个参数。

  • 第一个参数,可以传入一个 key range 对象来限制需要获取值的范围。
  • 第二个参数,可以设置迭代的方向。

成功回调函数的 result 值就是 cursor 本身,当前遍历值可以通过 cursor 对象的 keyvalue 值获取。如果要继续往下遍历,那么调用 cursor 的 continue() 方法,当遍历结束时, cursor 也就是 event.target.result 的值为 undefined.
如果要获取 UseKeyGenerator 仓库下所有的值,那么可以这样:

getAllByCursor(OB_NAMES.UseKeyGenerator).then(console.log)

除了 openCursor() 外,还可以使用 openKeyCursor() 来获取所有存储对象的主键值,使用方法和 openCursor 一样,

使用索引 (index)

在建立 object store 时,如果我们给它创建了索引。这时,就可以使用索引来查找某个特定属性的值:

function getByIndex (objName, indexName, key) {
  return openIndexDB().then(db => {
    const index = db.transaction(objName).objectStore(objName).index(indexName)
    return new Promise(resolve => {
      index.get(key).onsuccess = e => {
        console.log('getByIndex', e.target.result)
        return resolve(e.target.result)
      }
    })
  })
}

比如,我们要查找 levelinfo 的日志,那么可以这样:

getByIndex(OB_NAMES.UseKeyGenerator, 'level', 'info').then(doc =>
  console.log(doc)
)

设置范围 (range) 和游标 (cursors) 的方向

如果需要设置 cursor 遍历的范围,可以使用 IDBKeyRange 对象并把作为第一个参数给 openCursor() 或者 openKeyCursor() .
一个 key range 的值,可以认为是一个区间,区间的类型可以是开区间也可以是闭区间。看一些例子:

/**
* NOTE:
* 只包括给定的值
*/
getWithRangeByCursor( OB_NAMES.UseKeyGenerator,'errorCode',
  IDBKeyRange.only(1000))
  .then(console.log)
  .catch(console.error)
/**
* NOTE:
* 默认是闭区间
*/
getWithRangeByCursor( OB_NAMES.UseKeyGenerator,'errorCode',
  IDBKeyRange.lowerBound(1000))
  .then(console.log)
  .catch(console.error)
/**
* NOTE:
* 设置第二个可选参数为 true,则为开区间
*/
getWithRangeByCursor(OB_NAMES.UseKeyGenerator,'errorCode',
  IDBKeyRange.lowerBound(1000, true))
  .then(console.log)
  .catch(console.error)
/**
* NOTE:
* 闭区间,如果索引是数字,那么按照数字大小决定升序
*/
getWithRangeByCursor(OB_NAMES.UseKeyGenerator,'errorCode',
  IDBKeyRange.bound(1000, 2000)
)
  .then(console.log)
  .catch(console.error)
/**
* NOTE:
* 左闭右开区间:如果索引是字符串,那么安装 array.sort() 的方式决定升序
*/
getWithRangeByCursor(OB_NAMES.UseKeyGenerator,'time',
  IDBKeyRange.bound('2017/11/8 下午4:53:042', '2017/11/8 下午4:53:043', false, true)
)
  .then(console.log)
  .catch(console.error)

降序迭代

默认情况下,迭代的方向是升序的。如果需要按照降序规则迭代,那么只要把 prev 作为 openCursor() 的第二个参数就可以了:

getWithDescendingByCursor(OB_NAMES.UseKeyGenerator, 'time')
  .then(console.log)
  .catch(console.error)