Skip to content

前言

MDN官网:IndexedDB - Web API | MDN

IndexedDB 是属于游览器的一个数据库。在游览器上有两种数据库:WebSQL 和 IndexedDB 数据库。但是现在 WebSQL 数据库已经就基本上废弃了

MDN 官网对于 IndexedDB 的解释:

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。

简单来说 IndexedDB 就是主要用来让客户端存储大量数据而生的,相对于 Cookie、SessionStorage、LocalStorage 等存储方式相比,它没有存储大小的限制

使用场景

在使用 IndexedDB 数据库的前提下是要确定将要存储大量的数据

  1. 变动性不强的数据可视化界面。大量数据每次请求都会让服务器造成很大的开销
  2. 即使通讯,把大量的聊天信息保存到本地
  3. 其他的存储方式不满足的情况下

特点

  1. 非关系型数据库(以键值对的形式存储数据)
  2. 持久化存储(像是 Cookie、SessionStorage、LocalStorage 在清除游览器缓存之后数据就会丢失,但是 IndexedDB 不会,除非手动删除数据库)
  3. 支持事务(跟 MySQL 一样,要么全部成功,否则只要有一步失败那么整个事务都会取消,数据回滚)
  4. 异步操作(IndexedDB 是异步操作的,他不会锁死游览器。LocalStorage是同步的)
  5. 同源策略(网页只能访问自身域名下的数据库,不能够跨域访问)
  6. 存储量大

相关概念

仓库(ObjectStore):IndexedDB 没有表的概念,他只有仓库(Store)的概念,可以把它理解成表,即一个仓库 = 一张表

索引(index):跟 MySQL 一样,都会为了加快数据的查找速率。可以在创建 仓库 的时候同时创建索引。在给某个字段设置索引之后,这个字段便 不能为空。如果不用索引,查询时代码就要查询出全部记录,然后对每个记录进行判断过滤,如果用索引,就能直接按指定参数查出指定条件的全部记录

游标(Cursor):游标是 IndexedDB 中的概念。可以理解为一个指针,如果要查询满足一个条件的所有数据时,就需要让游标一行一行往下走,游标走到的地方会返回一行数据。此时就可以对这行数据进行判断是否满足条件。可以同一时间拥有多个游标

IndexedDB 只能通过主键索引游标方式查询数据

MDN文档:IDBCursor - Web API | MDN

事务:IndexedDB 数据库所有针对仓库的操作都是基于事务的

数据库相关操作

创建/连接

javascript

/**
 * 打开数据库
 * @param {object} dbName 数据库的名字
 * @param {string} version 数据库的版本
 * @return {object} 该函数会返回一个数据库实例
 */
function openDB1(dbName, version = 1) {
  return new Promise((resolve, reject) => {
    // 兼容性获取 indexedDB 对象
    const indexedDB =
      window.indexedDB ||
      window.mozIndexedDB ||
      window.webkitIndexedDB ||
      window.msIndexedDB;

    if (!indexedDB) {
      return reject(new Error('当前浏览器不支持 IndexedDB'));
    }

    // 打开数据库
    const request = indexedDB.open(dbName, version);

    // 定义数据库实例和事务
    let db;

    // 打开成功回调
    request.onsuccess = (event) => {
      db = event.target.result;
      console.log(`数据库 [${dbName}] 打开成功`);
      resolve(db); // 返回可用的数据库实例
    };

    // 打开失败回调
    request.onerror = (event) => {
      reject(event.target.error || new Error('数据库打开失败'));
    };

    // 数据库升级回调
    request.onupgradeneeded = (event) => {
     // 巴拉巴拉......
    };
  });
}

indexedDB.open(dbName, version) 这一段后面的 Version 是代表数据库的版本,每当更新数据库的时候版本也要发生变化,可以理解为应用的版本号

版本(Version)

IndexedDB 数据库都有一个版本号(version),这是一个非负整数,用于确保数据库结构的更新和迁移能够平稳进行。当第一次创建数据库时,如果没有明确指定版本号,那么默认版本号为1。每次版本升级都会触发 onupgradeneeded 事件

js
const db = indexedDB.open("myDatabase", 1); // 打开 myDatabase 数据库,版本为 1
  • 如果数据库不存在则会触发 onupgradeneeded 事件创建一个新的数据库
  • 如果数据库存在,并且:
    • 传递的版本号 == 数据库的版本号:打开数据库
    • 传递的版本号 > 数据库的版本号:更新数据库
    • 传递的版本号 < 数据库的版本号:报错

个人理解:数据库结构通过 js 代码创建出来并保存到游览器中。在之后的程序升级后可能现有的数据库结构不能够满足当前系统,所以要对当前数据库进行结构升级。在打开数据库的时候传递版本,当版本不一样时触发 onupgradeneeded 事件来达到自动升级的目的

升级

javascript
const request = indexedDB.open('myDatabase', 3);

// 数据库升级回调
request.onupgradeneeded = (event) => {
    db = event.target.result;
    const oldVersion = event.oldVersion;  // 老版本号
    let objectStore;  // 存储对象

    // 渐进式升级

    // 第一次初始化数据
    if (oldVersion < 1) {
        objectStore = db.createObjectStore('user', {
            keyPath: 'userId',
            autoIncrement: true
        });

        // 创建索引
        objectStore.createIndex('userId', 'userId', {unique: true}); // 唯一索引
        objectStore.createIndex('name', 'name');
        objectStore.createIndex('phone', 'phone');
        objectStore.createIndex('namePhone', ['name', 'phone']); // 复合索引
    }

    // 第一次更新
    if (oldVersion < 2) {
        objectStore ||=  db.transaction.objectStore('user');
        // 删除 phone 索引
        objectStore.deleteIndex('phone')
        // 添加 age 索引
        objectStore.createIndex('age', 'age');
    }

    // 第二次更新
    if (oldVersion < 3) {
        objectStore ||= db.transaction.objectStore('user');
        // 删除 age 索引
        objectStore.deleteIndex('age')
    }

    console.log(`数据库版本升级至 ${version}`);
};

下面是存储内容结构:

javascript
const user = {
    userId: 1,
    name: 'John Doe',
    email: {
        qq: '9527114541@qq.com'
        // 其他邮箱
    },
    age: 30,
    phone: '1234567890',
    // 其他字段
}

通过判单老版本号来进行渐进式升级,就是先判断老版本号是在什么阶段。如果老版本号是 1,那么就从 oldVersion < 2 的阶段开始升级

删除

javascript
/**
 * 删除数据库
 * @param dbName 数据库的名字
 * @returns {Promise<unknown>} Promise
 */
function deleteDB(dbName) {
  return new Promise((resolve, reject) => {
    // 兼容性获取 indexedDB 对象
    const indexedDB =
      window.indexedDB ||
      window.mozIndexedDB ||
      window.webkitIndexedDB ||
      window.msIndexedDB;
    // 删除数据库
    const request = indexedDB.deleteDatabase(dbName);
    // 删除成功
    request.onsuccess = () => resolve()
    // 删除失败
    request.onerror = (event) => reject(event.target.error)
  })
}

仓库相关操作

创建仓库

javascript
const objectStore = db.createObjectStore('storeName', {
    keyPath: 'id',	// 主键
    autoIncrement: true	// 是否自增
})

通过 db 实例调用 createObjectStore 方法来创建仓库。仓库创建完成后会返回一个操作该仓库的对象,可以通过这个对象来进行[创建/删除]索引CRUD数据等操作

创建索引

在创建完仓库后会返回一个对象。拿着这个对象创建索引,使用 createIndex 方法,接收三个参数

  1. 索引名称
  2. 键路径
  3. 选项对象

索引名称(indexName)

索引的名称,用来标识该索引。必须是唯一的,不能和其他索引或者对象存储空间的名称冲突

键路径(keyPath)

它有两种写法:纯字符串和字符串数组。

纯字符串(单索引):取顶级的值,如上的 name

字符串数组(复合索引):同样是取顶级的值,但是可以取多个,如上面的 namePhone

IndexedDB 没有嵌套索引,所有的索引都必须是顶级的,比如:

javascript
const user = {
   userId: 1,
   // ...
   email: {
          qq: '9527114541@qq.com'
          // ...
   },
   // ...
}

按照以上创建索引的方式 xxx.createIndex('xxxx', ['userId', 'email.qq']); 这种方式是错误的,不支持 'email.qq'

选项对象(options)

一个可选的对象,用于定义索引的行为,跟 MySQL 中的约束条件差不错

unique

类型:boolean

描述:如果设置为 true,则索引的值必须唯一。如果尝试插入重复值,会抛出错误。默认值为 false。

multiEntry

类型:boolean

描述:如果设置为 true,并且 keyPath 指向一个数组,则数组中的每个元素都会被单独索引。默认值为 false。

事务

方法:transaction

使用:db.transaction('storeName', 'readonly')

参数:

  1. 仓库名(storeNames)有两种写法,字符串、字符串数组
    • 字符串:表示单个对象存储的名称
    • 字符串数组:表示多个对象存储的名称列表
  2. 事务模式(mode)
    • 只读:"readonly"(默认值)
    • 读写:"readwrite"
    • 版本变更事务:"versionchange"

版本变更事务:这是一种特殊的事务类型,主要用于数据库的结构修改,例如创建、删除对象存储或索引。这种事务只能在数据库版本升级时使用

一般只用只读或者读写

数据的CRUD

添加

js
/**
 * 添加数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} data 数据
 */
function addData(db, storeName, data) {
  const request = db
    .transaction([storeName], "readwrite")
    .objectStore(storeName)
    .add(data);

  // 添加成功
  request.onsuccess = (event) => console.log("数据添加成功");

  // 添加失败
  request.onerror = (event) => console.log("数据添加失败")
}

删除

通过主键删除:

js
/**
 * 根据主键删除数据
 * @param {Object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} key 主键
 */
function delDataById(db, storeName, key) {
  const request = db
    .transaction([storeName], "readwrite")
    .objectStore(storeName)
    .delete(key);

  // 删除成功
  request.onsuccess = (event) => console.log("数据删除成功");

  // 删除失败
  request.onerror = (event) => console.log("数据删除失败");
}

通过索引或者游标来删除数据:

js
/**
 * 通过索引和游标删除指定的数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} indexName 索引名
 * @param {object} indexValue 索引值
 */
function deleteDataByIndexAndCursor(db, storeName, indexName, indexValue) {
  const store = db
    .transaction(storeName, "readwrite")
    .objectStore(storeName);

  // 通过索引和游标查询数据
  const request = store
    .index(indexName)
    .openCursor(IDBKeyRange.only(indexValue));

  request.onsuccess = function (e) {
    const cursor = e.target.result;

    // cursor 必须为真,否则遍历结束
    if (cursor) {
      // 删除当前记录
      const deleteRequest = cursor.delete();
      // 打印提示
      deleteRequest.onsuccess = () => console.log("游标删除记录成功");
      deleteRequest.onerror = () => console.log("游标删除记录失败");

      // 继续遍历下一条记录
      cursor.continue();
    }
  };

  request.onerror = (e) => console.log("游标删除失败")
}

修改

js
/**
 * 更新数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {object} data 数据
 */
function updateData(db, storeName, data) {
  const request = db
    .transaction([storeName], "readwrite")
    .objectStore(storeName)
    .put(data);

  // 更新成功
  request.onsuccess = () => console.log("数据更新成功");

  // 更新失败
  request.onerror = () => console.log("数据更新失败");
}

直接使用 put() 方法即可更新。如果数据不存在的话他就会创建一条新数据,反之则更新数据。

判断数据是否存在:在需要更新数据的时候把主键写上(就写到 data 中)

查询

主键查询

js
/**
 * 通过主键读取数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} key 主键值
 */
function getDataByKey(db, storeName, key) {
  return new Promise((resolve, reject) => {
    const request = db.transaction([storeName]) // 事务
      .objectStore(storeName) // 仓库对象
      .get(key); // 通过主键获取单个对象

    // 读取成功
    request.onsuccess = (event) => resolve(request.result);
    // 读取失败
    request.onerror = (event) => reject(event.target.error);
  });
}

不仅仅只有 get() 方法,还有:

  • objectStore.get():用于根据主键获取单个对象。参数是主键值,可以是字符串、数字等
  • objectStore.getAll():用于获取所有记录或指定范围内的记录。可以不带参数获取全部,或带查询参数和数量限制。例如,getAll() 返回所有,getAll(query, count) 返回匹配查询的记录,最多 count 条;query 参数可以是 IDBKeyRange 对象
  • objectStore.getKey():根据主键获取对应的主键值,通常用于检查是否存在某个主键
  • objectStore.getAllKeys():获取所有主键或指定范围内的主键。同样可以不带参数或带查询和数量限制

游标查询

js
/**
 * 通过游标读取数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 */
function getDataByCursor(db, storeName) {
  const request = db
    .transaction(storeName, "readwrite") // 事务
    .objectStore(storeName) // 仓库对象
    .openCursor() // 开启游标
  
  // 存储容器
  const list = [];

  // 游标开启成功,逐行读数据
  request.onsuccess = (e) => {
    const cursor = e.target.result;
    if (cursor) {
      list.push(cursor.value);  // 保存对象
      cursor.continue(); // 下一条数据
    } else console.log("游标读取的数据:", list);
  }
  
  // 游标开启失败
  request.onerror = (e) => console.log("游标读取失败")
}

对于 openCursor() 方法的参数:

  • range:IDBKeyRange 对象
  • direction:遍历方向
    • "next":按升序遍历(从最小到最大)。
    • "nextunique":按升序遍历,但跳过重复键。
    • "prev":按降序遍历(从最大到最小)。
    • "prevunique":按降序遍历,但跳过重复键。

索引查询

js
/**
 * 通过索引读取数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} indexName 索引名称
 * @param {string} indexValue 索引值
 */
function getDataByIndex(db, storeName, indexName, indexValue) {
  const request = db
    .transaction(storeName, "readwrite")
    .objectStore(storeName)
    .index(indexName)
    .get(indexValue);

  // 查询成功
  request.onsuccess = (e) => console.log("索引查询结果:", e.target.result);

  // 查询失败
  request.onerror = () => console.log("事务失败");
}

上满 index() 后面的 get()主键查询中的 get() 是一样的。不仅仅能使用 get 也能使用另外的三个方法

索引+游标查询

javascript
/**
 * 通过索引和游标读取数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} indexName 索引名称
 * @param {string} indexValue 索引值
 */
function getDataByIndexAndCursor(db, storeName, indexName, indexValue) {
  const request = db
    .transaction(storeName, "readwrite")
    .objectStore(storeName)
    .index(indexName)
    .openCursor(IDBKeyRange.only(indexValue))

  // 容器
  const list = [];

  // 查询成功
  request.onsuccess = (e) => {
    const cursor = e.target.result;
    if (cursor) {
      list.push(cursor.value);
      cursor.continue();
    } else console.log("索引查询结果:", list);
  }

  // 查询失败
  request.onerror = () => console.log("事务失败");
}

当单独使用索引查询时使用 getAll() 的话会把所有内容都出来,如果想过滤结果就可以通过游标一条一条过滤

IDBKeyRange 对象

它用于定义数据库查询时的键值范围

使用场景

  • 精确匹配某个键值。
  • 查询大于、小于或介于特定范围的键。
  • 结合游标(cursor)遍历或筛选数据。

静态方法

共有四个静态方法:

IDBKeyRange.only(key):精确匹配单个键

IDBKeyRange.lowerBound(lower, [open=false]):定义下限范围 >= lower 如果第二个参数为 true 那么就是 > lower

IDBKeyRange.upperBound(upper, [open=false]):定义上限范围 <= upper 如果第二个参数为 true 那么就是 < upper

IDBKeyRange.bound(lower, upper, [lowerOpen=false], [upperOpen=false]):同时定义上下限范围,后面的 open 跟上面一样

键的定义:在以上的方法中最终都是针对键来匹配的,但是对象存储(objectStore)的主键(primary key)和索引(index)的键(key)都可能使用IDBKeyRange。因此,匹配时比如使用 IDBKeyRange.only 既可以匹配主键,也可以匹配索引的键,具体取决于调用的上下文。例如,如果在对象存储上使用,就是主键;如果在索引上使用,则是该索引的键

注意事项

键类型一致性:键值类型需与对象存储或索引的键类型一致(如数字、字符串、Date),混合类型可能导致意外结果。

复合键的处理:若使用复合键(数组形式,如 [name, age]),范围需按数组顺序逐级比较。例如:

javascript
IDBKeyRange.bound(['Alice', 20], ['Bob', 30]);

游标方向与范围:游标的遍历方向(nextprev)影响数据顺序,需结合范围设置合理使用。

性能优化:在大型数据集中,精确的范围定义和索引使用能显著提升查询效率。

错误处理:非法键值(如未定义)会抛出错误,需用 try...catch 捕获:

javascript
try {
  const range = IDBKeyRange.only(undefined);
} catch (e) {
  console.error("无效键值");
}