🗂 IndexedDB 入门 基础操作

77 阅读3分钟

什么是 IndexedDB

IndexedDB 是一个基于 JavaScript 的面向对象数据库,是一个事务型数据库系统。是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs)),允许存储和检索用索引的对象,使用索引实现对数据的高性能搜索。

与传统的 web storage 相比,IndexedDB 可以存储更大量的结构化数据,且支持复杂的查询。

本文章将介绍 IndexedDB 的基础知识,包括创建数据库、创建对象存储仓库、执行增删改查操作等。

基本概念定义

IndexedDB 是一个复杂的 API,涉及到很多概念,把不同的实体抽象成一个一个对象,下面就从操作数据库的流程中来介绍这些常用的对象。

  • 『IDBOpenDBRequest 打开数据库请求对象』 表示打开或删除数据库的请求。
  • IDBDatabase 数据库对象』 表示一个数据库连接。这是在数据库中获取事务的唯一方式。同一个时刻,数据库只能存在一个版本,如果版本不相同在对数据库进行增删改、索引等操作必须通过升级数据库来完成。
  • IDBObjectStore 仓库对象』 表示一个对象库,其中的记录根据其键值进行排序,可以实现快速插入,查找和有序检索。为了方便理解,可以把“对象存储空间”想象成关系数据库的“表”结构。
  • IDBIndex 索引对象』 表示数据存储对象的索引,用于在仓库对象中查找记录,索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。index 是一个持久的键——值存储,插入、更新或删除数据记录,索引就会自动更新。
  • IDBTransaction事务对象』 用来异步操作数据库事务,所有的读写操作都要通过这个对象进行。indexedDB 支持事务意味着在一系列的操作步骤中一旦有一步异常了,整个事务会取消恢复至事务发生之前的状态。
  • IDBCursor游标对象』 用于遍历数据库中的多条记录。游标有一个源,表示需要遍历的索引或者对象存储区。游标在所属区间范围内有一个位置,根据记录健(存储字段)的顺序递增或递减方向移动。
  • IDBKeyRange主键组对象』 代表数据仓库(object store)里面的一组主键。

操作数据库流程

打开数据库

// 打开我们的数据库
const request = window.indexedDB.open("MyTestDatabase", 3);

open 方法接受两个参数。

  • 第一个是数据库名称,如果数据库名称不存在则会创建一个;
  • 第二个是数据库版本号,可省略,省略时如果打开已有数据库则默认为当前数据库版本,新建数据库时默认为1

open 请求不会立即打开数据库或者开始一个事务,而是立即返回一个 IDBOpenDBRequest 对象 request,并异步执行打开操作。其中 IDBOpenDBRequest 对象有三个处理事件:onerroronsuccessupgradeneeded,包含处理结果或者错误。

request.onerror = (event) => {
  // 数据库打开报错
  console.error(`数据库错误:${event.target.errorCode}`);
};
request.onsuccess = (event) => {
  // 数据库打开成功
  const db = event.target.result;
};
request.onupgradeneeded = (event) => {
  // 创建一个新的数据库或者增加已存在的数据库的版本号
  const db = event.target.result;
};

每一个请求都有一个 readyState 属性,初始时为 pending,当请求完成或失败的时候,readyState 会变为 done。当状态值变为 done 时,每一个请求都会返回 result 和 error 属性,并且会触发一个事件,如果成功打开数据库一切顺利的话,会触发 onsuccess 事件;有任何错误的话,则会触发 onerror 事件;当你创建一个新的数据库或者增加已存在的数据库的版本号,都会触发 onupgradeneeded 事件。

event.target.result 是一个 IDBDatabase 的实例。IndexedDB 使用对象存储而不是表,并且一个数据库可以包含任意数量的对象存储

新建数据库

新建数据库与打开数据库是同一个操作,如果 open 指定的数据库不存在,就会新建。当新建一个数据库,或者指定一个版本号较高的版本时,会触发 onupgradeneeded 事件。

IDBVersionChangeEvent 对象会作为参数传递给绑定在request.result(例如示例中的 db)上的 onversionchange 事件处理器上。

request.onupgradeneeded = (event) => {
  // 创建一个新的数据库或者增加已存在的数据库的版本号
  const db = event.target.result;
  db.onversionchange = () => {
    console.log('数据库版本变更');
  };
};

删除数据库

deleteDatabase()方法用于删除一个数据库,参数为数据库的名字。它会立刻返回一个IDBOpenDBRequest对象,对数据库进行异步删除。调用deleteDatabase()方法以后,当前数据库的其他已经打开的连接都会接收到versionchange事件。删除不存在的数据库并不会报错。

const request = window.indexedDB.deleteDatabase('MyTestDatabase');
request.onsuccess = function (event) {
    console.log('数据库删除成功');
};
request.onerror = function (event) {
    console.log('数据库删除失败');
};

新建对象仓库

在数据库新建完成后,通常会创建一个对象仓库,

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // 创建一个对象存储来存储人员信息,我们将“id”作为键路径。
  const objectStore = db.createObjectStore("person", {
      keyPath: "id",
      autoIncrement: false,
  });
}

IndexedDB 使用对象存储而不是表,并且一个数据库可以包含任意数量的对象存储。每当一个值被存入一个对象存储时,它会与一个相关联。其中键取决于对象存储是使用键路径还是键生成器 分成以下几种不同的创建方式:

image.png

autoIncrement属性表示,是否使用自动递增的整数作为主键(第一个数据记录为1,第二个数据记录为2,以此类推),默认为false

🔔 通常情况下可以增加条件判断,当前要创建的表格是否存在,不存在再创建。

if (!db.objectStoreNames.contains('person')) {
    objectStore = db.createObjectStore('person', { keyPath: 'id' });
}

启动事务

事务是一组数据库操作的逻辑单元,它要么全部成功执行,要么全部回滚。具有以下特性:

  • 原子性(Atomicity):事务中的所有操作要么全部成功执行,要么全部回滚,不会出现部分执行的情况。
  • 一致性(Consistency):事务执行前后,数据库的状态保持一致,不会破坏数据的完整性和约束条件。
  • 隔离性(Isolation):并发执行的多个事务之间应该相互隔离,每个事务都应该感觉不到其他事务的存在。
  • 持久性(Durability):事务一旦提交成功,对数据库的修改应该永久保存,即使系统发生故障也不会丢失。

IDBDatabase.transaction(objectName, [mode]) 方法方法接受两个参数(一个是可选的)并返回的就是一个 IDBTransaction 对象。通过IDBTransaction.objectStore(name)方法,可以获取到 IDBObjectStore 对象。

  • 第一个参数 objectName 是一个数组,表示事务希望跨越的对象存储空间的列表。
  • 第二个参数 mode 可选,默认 readonly 表示只读事务;如果想写入数据则需要传入 readwrite

事务包含三个事件,errorabort 和 complete

  • error 接收由它产生的所有请求所产生的错误,并且错误会中断它所处的事务
  • abort 如果事务中没有处理一个已发生的错误事件或者调用 abort() 方法,那么该事务会被回滚,并触发 abort 事件。
  • complete 在所有请求完成后,事务的 complete 事件会被触发。
request.onupgradeneeded = (event) => {
    const db = event.target.result;
  
    // 在所有数据添加完毕后的处理
    transaction.oncomplete = (event) => {
      console.log("全部请求完成了!");
    };

    transaction.onerror = (event) => {
      // 错误处理!
    };

    // 开启一个可写的事务 transaction,并从中获取一个对象存储 objectStore
    const transaction = db.transaction(["person"], "readwrite");
    const objectStore = transaction.objectStore("person");
}

增删查改操作

增加数据

add(value, key)用于向对象仓库添加数据,返回一个 IDBRequest 对象。该方法只用于添加数据,如果主键相同会报错,因此更新数据必须使用put()方法。该方法返回一个 IDBRequest 对象。

  • value 是待添加的数据
  • key 是主键,该参数可选,如果省略默认为 null
function addData = () {
    const request = db.transaction(['person'], 'readwrite')
        .objectStore('person')
        .add({ id: 1, name: '张美丽', age: 30, email: 'zhangmeili@qq.com' });

    request.onsuccess = function (event) {
        // event.target.result 被添加的数据的键
        console.log('数据写入成功');
    };
    request.onerror = function (event) {
        console.log('数据写入失败');
    }
}

删除数据

delete(key) 方法用于删除指定主键的记录。该方法返回一个 IDBRequest 对象。

function deleteData () {
    const request = db.transaction(['person'], 'readwrite')
        .objectStore('person')
        .delete(1);

    request.onsuccess = function (event) {
        console.log('数据删除成功');
    };
    request.onerror = function (event) {
        console.log('数据删除失败');
    }
}

clear()删除当前对象仓库的所有记录,不需要参数,该方法返回一个 IDBRequest 对象。

function deleteData () {
    const request = db.transaction(['person'], 'readwrite')
        .objectStore('person')
        .clear();

    request.onsuccess = function (event) {
        console.log('数据清空成功');
    };
    request.onerror = function (event) {
        console.log('数据清空失败');
    }
}

修改数据

put(value, key) 可以对数据进行修改,方法用于更新某个主键对应的数据记录,如果对应的键值不存在,则插入一条新的记录。该方法返回一个 IDBRequest 对象。

  • value 为待更新的数据
  • key 为主键,该参数可选,且只在自动递增时才有必要提供,因为那时主键不包含在数据值里面
function putData () {
    const request = db.transaction(['person'], 'readwrite')
        .objectStore('person')
        .put({ id: 1, name: '张美丽', age: 31, email: 'zhangmeili@qq.com' });

    request.onsuccess = function (event) {
        console.log('数据更新成功');
    };
    request.onerror = function (event) {
        console.log('数据更新失败');
    }
}

读取数据

get(key) 用于获取主键对应的数据记录,该方法返回一个 IDBRequest 对象。

function getData () {
    const request = db.transaction(['person'])
        .objectStore('person')
        .get(1);

    request.onsuccess = function (event) {
        console.log('数据获取成功', event.target.result);
        // { id: 1, name: '张美丽', age: 31, email: 'zhangmeili@qq.com' }
    };
    request.onerror = function (event) {
        console.log('数据获取失败');
    }
}

遍历数据表

get(key) 要求你知道想要检索的是哪一个键,如果你想要遍历对象存储空间中的所有值,那么你可以使用游标。

function getAllData () {
    const objectStore = db.transaction("person")
        .objectStore("person");

    objectStore.openCursor().onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
            console.log(`id: ${cursor.key} , name: ${cursor.value.name}`);
            cursor.continue();
        } else {
            console.log("没有更多记录了!");
        }
    };
}

openCursor() 用来获取一个游标的指针对象。游标对象 event.target.result 的 key 和 value 表示实际的键和值。continue()方法将游标指针移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则游标指针指向null

openCursor() 函数接受两个参数:

  • 第一个 query 限制被检索的项目的范围,默认为所有的记录,可以指定为主键值,或者一个 IDBKeyRange 对象。
  • 第二个 direction 确定迭代的方向,默认值为next,其他可能的值为prevnextuniqueprevunique

IDBCursor 对象有以下方法:

  • advance(n):指针向前移动 n 个位置。
  • continue():指针向前移动一个位置。它可以接受一个主键作为参数,这时会跳转到这个主键。
  • continuePrimaryKey():该方法需要两个参数,第一个是key,第二个是primaryKey,将指针移到符合这两个参数的位置。
  • delete():用来删除当前位置的记录,返回一个 IDBRequest 对象。该方法不会改变指针的位置。
  • update():用来更新当前位置的记录,返回一个 IDBRequest 对象。它的参数是要写入数据库的新的值。

创建并使用索引

indexedDB 索引的意义在于允许通过任意属性获取数据记录,对于某些数据,一个对象存储空间可能需要指定多个键,可以将其中一个设置为主键,其他属性设置为索引。

下面是对 name 字段创建了索引,后面就可以通过 name 来获取数据记录了。

request.onupgradeneeded = (event) => {
    const db = event.target.result;

    // 创建一个对象存储来存储人员信息,我们将“id”作为键路径。
    const objectStore = db.createObjectStore('person', { keyPath: 'id' });
    // 对 name 字段创建索引,name 不唯一
    objectStore.createIndex("nameIndex", "name", { unique: false });

    const request = db.transaction(['person'])
        .objectStore('person')
        .index('nameIndex')
        .get('张美丽');

    request.onsuccess = function (event) {
        console.log('数据获取成功', event.target.result);
        // { id: 1, name: '张美丽', age: 31, email: 'zhangmeili@qq.com' }
    };
    request.onerror = function (event) {
        console.log('数据获取失败');
    }
}

createIndex() 接收三个参数:

  • indexName索引名称
  • keyPath索引所在的属性
  • options配置对象
    • unique 表示是否唯一;
    • multiEntry 当索引属性是一个数组时,设置为true,则createIndex将在每个数组元素的索引中添加一个条目。否则,它会添加一个包含该数组的条目。

IDBIndex 对象有以下异步的方法,立即返回的都是一个 IDBRequest 对象。

  • count():用来获取记录的数量。它可以接受主键或 IDBKeyRange 对象作为参数,这时只返回符合主键的记录数量,否则返回所有记录的数量。
  • get(key):用来获取符合指定主键的数据记录。
  • getKey(key):用来获取指定的主键。
  • getAll():用来获取所有的数据记录。它可以接受两个参数,都是可选的,第一个参数用来指定主键,第二个参数用来指定返回记录的数量。如果省略这两个参数,则返回所有记录。由于获取成功时,浏览器必须生成所有对象,所以对性能有影响。如果数据集比较大,建议使用 IDBCursor 对象。
  • getAllKeys():该方法与IDBIndex.getAll()方法相似,区别是获取所有主键。
  • openCursor():用来获取一个 IDBCursor 对象,用来遍历索引里面的所有条目。
  • openKeyCursor():该方法与IDBIndex.openCursor()方法相似,区别是遍历所有条目的主键。

下面是通过 IDBCursor 对象来遍历根据索引获取到的所有数据。

request.onupgradeneeded = (event) => {
    const db = event.target.result;

    // 创建一个对象存储来存储人员信息,我们将“id”作为键路径。
    const objectStore = db.createObjectStore('person', { keyPath: 'id' });
    // 对 name 字段创建索引,name 不唯一
    objectStore.createIndex("nameIndex", "name", { unique: false });

    const indexRequest = objectStore.transaction(['person'])
        .objectStore('person')
        .index('nameIndex');

    indexRequest.openCursor().onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
            console.log(`id: ${cursor.key} , name: ${cursor.value.name}`);
            cursor.continue();
        } else {
            console.log("没有更多记录了!");
        }
    };
}

IDBKeyRange 对象

IDBKeyRange 对象表示一组主键,根据这一组主键可以获取到仓库对象或者索引中的一组记录。它有四个静态方法,用来指定主键的范围:

  • IDBKeyRange.lowerBound():指定下限。
  • IDBKeyRange.upperBound():指定上限。
  • IDBKeyRange.bound():同时指定上下限。
    • 支持四个参数 IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen) lowerOpen upperOpen 分别表示下限/下限是否为开区间。
  • IDBKeyRange.only():指定只包含一个值。

IDBKeyRange 实例对象生成以后,将它作为参数传给 IDBIndex 对象的openCursor()方法,就可以在所设定的范围内读取数据。

function getAllData () {
    const indexRequest = objectStore.transaction(['person'])
        .objectStore('person')
        .index('nameIndex');

    const keyRangeValue = IDBKeyRange.bound('A', 'K', false, false);
    indexRequest.openCursor(keyRangeValue).onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
            console.log(`id: ${cursor.key} , name: ${cursor.value.name}`);
            cursor.continue();
        } else {
            console.log("没有更多记录了!");
        }
    };
}

IDBKeyRange 有一个实例方法includes(key)返回一个布尔值,表示某个主键是否包含在当前这个主键组之内。

var keyRangeValue = IDBKeyRange.bound('A', 'K', false, false);
keyRangeValue.includes('F') // true
keyRangeValue.includes('K') // false
keyRangeValue.includes('M') // false

DEMO 演示

image.png

DEMO

CODE