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]);

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

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

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

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