抹桥的博客
3261 字
16 分钟
indexedDB 不完全指南

介绍#

引用 MDN 的介绍:

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

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

区别#

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

用法#

1. 打开数据库  #

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

indexedDB.open 接收两个参数,分别为数据库名称和版本,返回的是一个 IDBOpenDBRequest 对象。可以以 DOM 事件的方式监听它的 success 和 error 来获取到它的结果。几乎所有对 indexedDB 的异步操作都是这种以事件的方式进行,返回一个拥有结果或错误的 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. 构建数据库#

indexedDB 是以对象存储(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 openindexedDB() {
  // 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. 第一次打开可能会提示用户获取 indexedDB 的权限
     * 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 })
}

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

事务#

所有对数据库的操作都是建立在事务(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 openindexedDB().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 openindexedDB().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 openindexedDB().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 openindexedDB().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 openindexedDB().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 openindexedDB().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)
indexedDB 不完全指南
https://blog.kisnows.com/2017/12/06/step-into-indexdb/
作者
Kisnows
发布于
2017-12-06
许可协议
CC BY-NC-ND 4.0