IndexDB 前端数据库

存储方式对比

存储方式 存储容量 API难度 优点 缺点
LocalStorage / SessionStorage ~5MB-10MB/~5MB ⭐️️ 简单,API 同步、直接 setItem/getItem 就能用。 + 容量极小(一般只有 5MB 左右 ,不同浏览器有差异)。 + 同步阻塞 API,数据量大时会卡 UI 线程。 + 只能存字符串,不支持复杂对象结构,得自己做 JSON 序列化/反序列化。 + 没有事务、没有索引查询只能全量遍历 。 + 安全性差,容易被脚本直接读出。
Cookie ~4KB ⭐⭐ 最早期的存储方式,服务端自动可见(随请求发送) + 存储空间极小 (单个 cookie 4KB 限制,域名下总量也有限制)。 + 每次请求都会自动携带 → 增加请求头流量(性能差)。 + 安全风险大(容易被 XSS 窃取 ,除非设置 HttpOnly)。 + API 原始,读写不方便。 + 完全不适合存大数据。
WebSQL (已废弃) 几十MB ⭐⭐⭐ SQL 风格,关系型查询方便。 + 已废弃~~~~(W3C 早就停止维护,浏览器逐步移除)。 + 跨浏览器兼容性差。 + 未来不可用,不建议新项目使用。
Cache API (Service Worker 缓存) ~50MB以上 ⭐⭐⭐ 专门用于离线缓存请求响应(静态资源、接口数据)。 + 面向缓存,而不是数据存储 。 + 不能方便地按字段查询,只能按 request key 检索。 + 适合资源层面的缓存,不适合结构化数据。
文件系统 API (File System Access API) 操作本地文件 ⭐⭐⭐⭐ 直接访问本地文件系统,适合大文件存储(比如视频、图片)。 + 权限复杂 (需要用户明确授权)。 + 兼容性差,只有部分 Chromium 内核支持。 + 不适合做结构化数据的持久存储。
SQLite 理论上 140 TB(取决于文件系统和编译选项),对绝大多数应用都够用。几十 MB 到几 GB 都没压力 ⭐⭐⭐⭐ SQL 查询强大,可以 JOIN、事务灵活,API 是同步或 Promise 封装的异步,但本质上要操作文件 + 是一个嵌入式数据库,需要操作系统文件访问权限 + 浏览器里没法直接访问文件系统
IndexDB 几十MB - GB ⭐⭐⭐ 👇👇👇👇👇👇 👇👇👇👇👇👇👇

总而言之

  1. 存储空间太小,缺少事务/索引,安全性差

  2. 键值对总是以字符串形式存储,不支持其他数据格式

  3. 不支持异步操作,数据量大时进行I/O操作性能体验差

indexDB简介

MDN官网解释

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

官网上的这句话也很简单明了,意思就是IndexedDB主要用来客户端存储大量数据而生的 。如果数据量很大,且都需要客户端存储时,那么就可以使用IndexedDB数据库

Blob、ArrayBuffer,这种就可以使用indexDB来实现

  • 可以把 图片、音频、视频、PDF、Office 文件 之类直接放到 IndexedDB 里,而不是只存一堆 Base64。节省空间 + 提高性能。
  • IndexedDB + Blob = 可以实现"断网也能打开文件"的体验
  • 文件大时,可以把它切成 Blob chunk 存入 IndexedDB,上传时一块一块发给服务端
  • <font style="color:rgb(37, 41, 51);">FileReader.readAsArrayBuffer(file)</font> 把文件读成二进制然后自己处理。
  • WebCrypto API、音视频编解码器、压缩库,都直接操作 <font style="color:rgb(37, 41, 51);">ArrayBuffer</font>
  • 图形渲染需要把顶点坐标、纹理像素数据传到 GPU,用 <font style="color:rgb(37, 41, 51);">ArrayBuffer</font> / <font style="color:rgb(37, 41, 51);">TypedArray</font>

🔶 兼容性

特点

空间大小

这也是IndexedDB最显著的特点之一了,这也是不用localStorage等存储方式的最好理由。

IndexDB存储空间根据不同的设备而定,因为不同设备存储空间大小不一致,浏览器的最大存储空间是动态的,它完全取决于你的磁盘大小。全局限制为可用磁盘空间的 50%。

非关系型数据库(NoSql)

都知道MySQL等数据库都是关系型数据库,它们的主要特点就是数据都以一张二维表的形式存储,而Indexed DB是非关系型数据库,主要以键值对的形式存储数据。

持久化存储

cookie、localStorage、sessionStorage等方式存储的数据当清除浏览器缓存后,这些数据都会被清除掉的,而使用IndexedDB存储的数据则不会,除非手动删除该数据库。(注意:选择删除其它数据还是会清除的)

异步操作

IndexedDB操作时不会锁死浏览器,用户依然可以进行其他的操作,这与localstorage形成鲜明的对比,后者是同步的。

支持事务

IndexedDB支持事务(transaction),这意味着一系列的操作步骤之中,只要有一步失败了,整个事务都会取消,数据库回滚的事务发生之前的状态,这和MySQL等数据库的事务类似。

  • 主键冲突:add() 操作要求主键唯一,如果重复则报错 → 触发事务回滚
  • 唯一索引冲突:设置了 unique: true 的索引冲突 → 回滚
  • 手动 abort()
  • 访问不存在的 store 或索引

安全机制

1️⃣ 同源策略(Same-Origin Policy)

  • 同源限制访问:不同域名、协议或端口的网页无法访问你的 IndexedDB 数据。
  • 跨标签页共享:同一个域名下的多个标签页可以共享数据库,但其他网站无法访问。

🆚 不同之处:多标签页同时操作时,事务会排队执行,避免数据竞争,更安全可靠

✅ 安全性体现:防止跨站点直接读取你的数据。

2️⃣ 存储权限和浏览器控制

  • IndexedDB 数据由浏览器管理,无法被普通网页脚本绕过
  • 浏览器会限制:
    • 存储空间(Quota)
    • 用户清理(清理浏览器缓存/数据可以删除 IndexedDB 数据)
    • 私密模式下通常不会长期存储 IndexedDB 数据。

✅ 安全性体现:用户和浏览器可控制存储生命周期和大小,减少持久化风险。

3️⃣ 隔离与沙箱机制

  • IndexedDB 数据存放在浏览器沙箱内,网页无法直接访问文件系统
  • 只有通过浏览器提供的 API 才能操作数据。

✅ 安全性体现:防止网页/恶意脚本直接修改本地磁盘数据。

普通沙箱 🆚 indexDB沙箱

1️⃣ 普通沙箱(所有网页都有)

  • 网页脚本(JS)没有直接访问本地文件系统的权限。
  • 你必须通过 <input type="file"> 或 File System Access API(并且要用户交互授权)才能读写。
  • 这是浏览器普遍的安全规则。

2️⃣IndexedDB 的「额外沙箱隔离」

IndexedDB 本质上是浏览器在本地存储了一个数据库文件 (在 Chrome、Firefox、Safari 的 profile 目录里都能找到物理文件)。

但网页 JS 看不到真实的文件路径 、也不能用 fs 方式直接访问:

  • 只能通过 IndexedDB API 访问数据。
  • 物理层面的存储文件对开发者不可见、不可控。
  • 数据库文件放在浏览器内部的「profile 沙箱」中,和系统文件完全隔离。

总结:IndexedDB 虽然是本地数据库,但对网页代码来说只有 API 层,不会暴露文件层

4️⃣ 数据敏感性与加密

  • IndexedDB 不自动加密数据,存储的是明文 JSON / Blob 等。
  • 如果存储敏感信息(密码、身份证号、金融信息),建议:
    • 在写入 IndexedDB 前先做 加密处理
    • 结合 Crypto API 或自己管理加密密钥

⚠️ 风险:恶意本地程序或浏览器插件可能读取明文数据。

浏览器统一支持

  • 所有现代浏览器(Chrome、Firefox、Safari、Edge)都支持 IndexedDB。
  • 相比 File System Access API、OPFS 等新 API,它的兼容性最好。

不同电脑配置的影响

🤔

依赖电脑配置,但是不是主要的瓶颈。IndexDB的存储效率(写入/查询性能,可存储容量)等影响程度和纬度要分开来看:

1️⃣ 硬件层面

🔸 CPU 性能

  • 写入/查询时需要 JS 主线程参与(序列化/反序列化、建索引、事务管理等),CPU 性能越好,IndexedDB 操作越快。
  • 特别是处理大批量 JSON 或 Blob 时,弱 CPU 会成为瓶颈。

🔸 磁盘类型/速度

  • IndexedDB 底层依赖浏览器的存储引擎(Chromium 用 LevelDB,Firefox 用 SQLite),最终还是落到磁盘上。
  • SSD 比 HDD 快得多,批量写入/查询性能差异明显。

🔸 内存大小

  • 批量查询或大对象写入时需要先经过内存缓冲,如果内存小,GC 频繁,会拖慢速度。
  • IndexedDB 不会无限制吃内存,但缓存不足时会频繁写回磁盘,效率下降。

2️⃣ 浏览器层面

  • 不同浏览器用的存储引擎不同(Chrome/Edge → LevelDB,Firefox → SQLite,Safari → WebKit 自研),性能差异明显。
  • 同样的数据量,在 Chrome/Safari/Firfox表现都是也许不同。

3️⃣ 系统层面

🔸 操作系统的文件系统实现

  • NTFS、APFS、ext4 在小文件、大文件、随机读写上的表现不一样,导致 IndexedDB IO 性能差异。

🔸 磁盘配额限制

  • 浏览器通常给 IndexedDB 分配磁盘空间上限(全局限制为可用磁盘空间的 50%)

4️⃣ 应用设计层面(往往比硬件更关键)

  • 批量写入时是否用事务、是否拆分成小块写入。
  • 是否滥用 JSON.stringify / parse。
  • 是否给查询字段建索引(避免全表扫描)。
  • 是否在主线程里做了大量处理,导致 UI 卡顿。

结论

  • 硬件配置(CPU/磁盘/内存)会直接影响 IndexedDB 的效率,尤其是在处理大数据量时。
  • 但实际瓶颈往往是实现和应用设计,例如不建索引、大量同步操作、一次性写入几十 MB。
  • 如果设计得好,即便在配置一般的电脑上,IndexedDB 也能应付几十万甚至百万级数据存储和检索。

核心概念

数据库(Database

数据库是一系列相关数据的容器,每个协议+域名+端口都可以新建任意个数据库

indexDB数据库有版本的概念,同一个时刻只能有一个版本的数据库存在,如果要修改数据库结构,只能通过升级数据库版本

🛑 版本升级注意点:

  • 不允许跨版本修改
  • 如果原先数据库里 store 很多、索引复杂,浏览器就需要 检查现有结构并准备新结构(保证数据一致性)。升级操作会稍微慢一些。

对象仓库(objectStore

IndexedDB没有表的概念,它只有仓库store的概念,大家可以把仓库理解为表即可,即一个store是一张表。

操作请求(Request

操作请求对象,indexedDB 每个操作都是异步的,也就是说每个请求会先返回这个这个对象,然后根据这个对象的回调去进行后续的处理。

基本属性 说明 事件回调
result 操作成功后,结果会保存在这里 onsuccess onerror
error 操作失败时的错误对象
readyState 请求的当前状态
source 发起请求的来源
transaction 请求所属的事务对象

索引(index

  • 在关系型数据库当中也有索引的概念,可以给对应的表字段添加索引,以便加快查找速率。
  • 在IndexedDB中同样有索引,可以在创建store的时候同时创建索引,在后续对store进行查询的时候即可通过索引来筛选
  • 索引可以设置unique: true,保证索引字段的值唯一。这样就能实现类似数据库"唯一索引"的功能,防止重复数据。
  • 索引不仅支持精确匹配查询,还能做范围查询,比如查找年龄在20到30岁之间的人。

IndexedDB 的索引实现基本都依赖 LevelDB(Chromium / Edge / Opera)SQLite(Firefox / Safari)

这些数据库的索引结构通常是 B+Tree 或 SkipList

  • 键存储在有序结构里,支持范围查询(>=, <=, between)。
  • 叶子节点按顺序存储,游标可以顺序遍历(正序/倒序)。
  • 插入数据时,主表和索引表会分别写入:
    • 主表存完整对象(JSON / Blob)
    • 索引表存「索引键 → 主键 ID」的映射

当你建一个 createTime 索引时,IndexedDB 背后实际上维护了一个单独的 B+Tree,key 是时间戳,value 是记录的主键(比如自增 id)。

索引范围(KeyRange

索引范围对象,主要用来批量查询数据,或者批量删除数据的时候使用。

游标(cursor

  • 游标是IndexedDB数据库新的概念,大家可以把游标想象为一个指针,比如要查询满足某一条件的所有数据时,就需要用到游标,让游标一行一行的往下走,游标走到的地方便会返回这一行数据,此时便可对此行数据进行判断,是否满足条件。
  • 适合没法直接用主键或索引精确定位时,遍历筛选数据。
  • 游标(cursor)通过索引遍历数据,能按索引字段排序,这对分页、筛选非常有用。

【注意】:IndexedDB查询不像MySQL等数据库方便,它只能通过主键、索引、游标方式查询数据。

注意: IndexedDB 在做分页查询时,确实有两条典型路线:

  1. 直接用索引 + getAll / getAllKeys 搭配 offset/limit
  2. 用游标(openCursor / openKeyCursor)手动遍历并截断

内存与性能

  • getAll :会一次性把符合条件的所有记录读到内存里,然后你再在 JS 层做 slice 分页。
    • 如果数据量大,比如几万甚至几十万条,内存消耗和前端性能压力非常明显。
  • 游标 :是逐条拉取,你可以边遍历边决定是否 push 到结果集,到够 pageSize 就立刻停止,不会把无关数据都拉出来。
    👉 优势:分页时只取需要的部分,内存开销小,性能更稳定。

灵活性

  • getAll 只能获取到一批数据,你想跳页(比如第 100 页)只能整批加载前 100 页的数据再 slice,非常低效。
  • 游标 可以很自然地做到"跳过前 N 条,然后取 M 条",类似数据库里的 OFFSET + LIMIT
    👉 优势:适合大数据量的深度分页。

排序支持

  • IndexedDB 的排序是基于 索引顺序 的,openCursor 时可以指定 "next" / "prev",天然支持正序倒序。
  • getAll 则只能按索引自然顺序取全量,再在前端手动翻转或排序。
    👉 优势:游标分页时可以利用底层 B+Tree 的顺序遍历,性能更高。

停止控制

  • getAll 没法中途停,必须全拉完。
  • 游标 可以随时 cursor.continue() 或者 break,拿够了就停。
    👉 优势:只取需要的数据,不浪费 IO。

总结一句话

  • 数据量小(几百条以内)getAll 简单粗暴,代码量少。
  • 数据量大、需要分页/排序/跳页游标才是正解,节省内存,性能稳定,支持深度分页和双向遍历。

事务(Transaction)

IndexedDB支持事务,即对数据库进行操作时,只要失败了,都会回滚到最初始的状态,确保数据的一致性。

触发事务条件:

  • 主键/唯一索引冲突冲突
  • 手动调用.abort,主动结束事务
  • 磁盘写入/存储空间错误
  • 访问了不存在对象仓库或者索引

局限性

多表关联

IndexedDB 没有像 MySQL 那样的 JOIN 功能,两个表(Object Store)之间的"关联"只能靠你自己去实现,本质上就是在一个表里存另一个表的主键,然后代码里手动去查第二个表

javascript 复制代码
function getUserWithOrders(userId) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(['users', 'orders'], 'readonly');
    const usersStore = tx.objectStore('users');
    const ordersStore = tx.objectStore('orders').index('userId');

    // 1. 查用户
    const userReq = usersStore.get(userId);

    userReq.onsuccess = () => {
      const user = userReq.result;
      if (!user) {
        resolve(null);
        return;
      }

      // 2. 查订单
      const orders = [];
      const orderReq = ordersStore.openCursor(IDBKeyRange.only(userId));

      orderReq.onsuccess = (e) => {
        const cursor = e.target.result;
        if (cursor) {
          orders.push(cursor.value);
          cursor.continue();
        } else {
          resolve({ ...user, orders });
        }
      };

      orderReq.onerror = reject;
    };

    userReq.onerror = reject;
  });
}
javascript 复制代码
{
  "id": 1,
    "name": "小包子",
    "orders": [
    { "id": 101, "userId": 1, "product": "电脑", "price": 5000 },
    { "id": 102, "userId": 1, "product": "耳机", "price": 500 }
  ]
}

索引查询

查询能力有限,不支持复杂搜索

  • 不支持全文搜索:只能做基于主键或索引的精确匹配或范围查询,没法直接支持模糊匹配、正则、全文检索。
  • 不支持多字段复杂查询:复合索引有限,复杂条件组合(多字段且模糊匹配)很难实现高效查询。

查询性能瓶颈明显

  • 索引类型单一:只能建立单字段或有限的复合索引,没办法支持复杂查询策略。
  • 游标遍历代价大:复杂查询时必须用游标遍历整个数据集合,数据量大时性能急剧下降。
  • 事务锁导致并发查询受限:事务机制会阻塞其它操作,影响并发性能。

解决方案

查询类型 IndexedDB 支持情况 性能瓶颈 解决办法
精确匹配 支持,走索引 较快 直接索引查询
前缀匹配 支持范围查询 还可以 IDBKeyRange.bound
模糊包含关键字 不支持 需全表扫描 性能极差 1. 自建倒排索引 2. 结合全文搜索库

实际操作

创建/连接数据库、创建索引

javascript 复制代码
/**
 * 打开数据库
 * @param {object} dbName 数据库的名字
 * @param {string} storeName 仓库名称
 * @param {string} version 数据库的版本
 * @return {object} 该函数会返回一个数据库实例
 */
export function openDB(dbName, version = 1) {
  return new Promise((resolve, reject) => {
    //  兼容浏览器
    var indexedDB =
      window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
    let db
    // 打开数据库,若没有则会创建
    const request = indexedDB.open(dbName, version)
    // 数据库打开成功回调
    request.onsuccess = function (event) {
      db = event.target.result // 数据库对象
      console.log('数据库打开成功')
      resolve(db)
    }
    // 数据库打开失败的回调
    request.onerror = function (event) {
      console.log('数据库打开报错')
      reject(event)
    }
    // 数据库有更新时候的回调
    request.onupgradeneeded = function (event) {
      // 数据库创建或升级的时候会触发
      console.log('onupgradeneeded')
      db = event.target.result // 数据库对象
      var objectStore
      // 创建存储库
      objectStore = db.createObjectStore('ragFile', {
        keyPath: 'fileId' // 这是主键
        // autoIncrement: true // 实现自增
      })
      // 创建索引,在后面查询数据的时候可以根据索引查
      // (索引名,索引对应字段,唯一性限制)
      objectStore.createIndex('fileId', 'fileId', { unique: false }) //文件主键
      objectStore.createIndex('title', 'title', { unique: false }) //文件名称
    }
  })
}

将创建数据库的操作封装成了一个函数,并且该函数返回一个promise对象,使得在调用的时候可以链式调用,函数主要接收两个参数:数据库名称、数据库版本。函数内部主要有三个回调函数,分别是:

  • onsuccess:数据库打开成功或者创建成功后的回调,这里将数据库实例返回了出去。
  • onerror:数据库打开或创建失败后的回调。
  • onupgradeneeded:当数据库版本有变化的时候会执行该函数,比如想创建新的存储库(表),就可以在该函数里面操作,更新数据库版本即可。

插入数据

javascript 复制代码
/**
 * 新增数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} data 数据
 */
function addData(db, storeName, data) {
  var request = db
    .transaction([storeName], "readwrite") // 事务对象 指定表格名称和操作模式("只读"或"读写")
    .objectStore(storeName) // 仓库对象
    .add(data);

  request.onsuccess = function (event) {
    console.log("数据写入成功");
  };

  request.onerror = function (event) {
    console.log("数据写入失败");
  };
}

ndexedDB插入数据需要通过事务来进行操作,插入的方法也很简单,利用IndexedDB提供的add方法即可,这里同样将插入数据的操作封装成了一个函数,接收三个参数,分别如下:

  • db:在创建或连接数据库时,返回的db实例,需要那个时候保存下来。
  • storeName:仓库名称(或者表名),在创建或连接数据库时就已经创建好了仓库。
  • data:需要插入的数据,通常是一个对象

**【注意】:**插入的数据是一个对象,而且必须包含声明的索引键值对。

通过主键读取数据

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

    request.onerror = function (event) {
      console.log("事务失败");
    };

    request.onsuccess = function (event) {
      console.log("主键查询结果: ", request.result);
      resolve(request.result);
    };
  });
}

主键即刚刚在创建数据库时声明的keyPath,通过主键只能查询出一条数据。

通过游标查询数据

javascript 复制代码
/**
 * 通过游标读取数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 */
function cursorGetData(db, storeName) {
  let list = [];
  var store = db
    .transaction(storeName, "readwrite") // 事务
    .objectStore(storeName); // 仓库对象
  var request = store.openCursor(); // 指针对象
  // 游标开启成功,逐行读数据
  request.onsuccess = function (e) {
    var cursor = e.target.result;
    if (cursor) {
      // 必须要检查
      list.push(cursor.value);
      cursor.continue(); // 遍历了存储对象中的所有内容
    } else {
      console.log("游标读取的数据:", list);
    }
  };
}

上面函数开启了一个游标,然后逐行读取数据,存入数组,最终得到整个仓库的所有数据。

通过索引查询数据

索引名称即创建仓库的时候创建的索引名称,也就是键值对中的键,最终会查询出所有满足传入函数索引值的数据。

javascript 复制代码
/**
 * 通过索引读取数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} indexName 索引名称
 * @param {string} indexValue 索引值
 */
function getDataByIndex(db, storeName, indexName, indexValue) {
  var store = db.transaction(storeName, "readwrite").objectStore(storeName);
  var request = store.index(indexName).get(indexValue);
  request.onerror = function () {
    console.log("事务失败");
  };
  request.onsuccess = function (e) {
    var result = e.target.result;
    console.log("索引查询结果:", result);
  };
}

通过索引和游标查询数据

通过5.4节和5.5节发现,单独通过索引或者游标查询出的数据都是部分或者所有数据,如果想要查询出索引中满足某些条件的所有数据,那么单独使用索引或游标是无法实现的。当然,你也可以查询出所有数据之后在循环数组筛选出合适的数据,但是这不是最好的实现方式,最好的方式当然是将索引和游标结合起来。

javascript 复制代码
/**
 * 通过索引和游标查询记录
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} indexName 索引名称
 * @param {string} indexValue 索引值
 */
function cursorGetDataByIndex(db, storeName, indexName, indexValue) {
  let list = [];
  var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 仓库对象
  var request = store
    .index(indexName) // 索引对象
    .openCursor(IDBKeyRange.only(indexValue)); // 指针对象
  request.onsuccess = function (e) {
    var cursor = e.target.result;
    if (cursor) {
      // 必须要检查
      list.push(cursor.value);
      cursor.continue(); // 遍历了存储对象中的所有内容
    } else {
      console.log("游标索引查询结果:", list);
    }
  };
  request.onerror = function (e) {};
}

上面函数接收四个参数,分别是:

  • db:数据库实例
  • storeName:仓库名
  • indexName:索引名称
  • indexName:索引值

利用索引和游标结合查询,可以查询出索引值满足传入函数值的所有数据对象,而不是只查询出一条数据或者所有数据。

过索引和游标分页查询

IndexedDB分页查询不像MySQL分页查询那么简单,没有提供现成的API,如limit等,所以需要自己实现分页。

代码如下:

javascript 复制代码
/**
 * 通过索引和游标分页查询记录
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} indexName 索引名称
 * @param {string} indexValue 索引值
 * @param {number} page 页码
 * @param {number} pageSize 查询条数
 */
function cursorGetDataByIndexAndPage(
  db,
  storeName,
  indexName,
  indexValue,
  page,
  pageSize
) {
  let list = [];
  let counter = 0; // 计数器
  let advanced = true; // 是否跳过多少条查询
  var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 仓库对象
  var request = store
    .index(indexName) // 索引对象
    .openCursor(IDBKeyRange.only(indexValue)); // 指针对象
  request.onsuccess = function (e) {
    var cursor = e.target.result;
    if (page > 1 && advanced) {
      advanced = false;
      cursor.advance((page - 1) * pageSize); // 跳过多少条
      return;
    }
    if (cursor) {
      // 必须要检查
      list.push(cursor.value);
      counter++;
      if (counter < pageSize) {
        cursor.continue(); // 遍历了存储对象中的所有内容
      } else {
        cursor = null;
        console.log("分页查询结果", list);
      }
    } else {
      console.log("分页查询结果", list);
    }
  };
  request.onerror = function (e) {};
}

这里用到了IndexedDB的一个API:advance。该函数可以让的游标跳过多少条开始查询。假如的额分页是每页10条数据,现在需要查询第2页,那么就需要跳过前面10条数据,从11条数据开始查询,直到计数器等于10,那么就关闭游标,结束查询。

更新数据

IndexedDB更新数据较为简单,直接使用put方法,值得注意的是如果数据库中没有该条数据,则会默认增加该条数据,否则更新。有些小伙伴喜欢更新和新增都是用put方法,这也是可行的。

代码如下:

javascript 复制代码
/**
 * 更新数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {object} data 数据
 */
function updateDB(db, storeName, data) {
  var request = db
    .transaction([storeName], "readwrite") // 事务对象
    .objectStore(storeName) // 仓库对象
    .put(data);

  request.onsuccess = function () {
    console.log("数据更新成功");
  };

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

put方法接收一个数据对象。

通过主键删除数据

主键即创建数据库时申明的keyPath,它是唯一的。

代码如下:

javascript 复制代码
/**
 * 通过主键删除数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {object} id 主键值
 */
function deleteDB(db, storeName, id) {
  var request = db
    .transaction([storeName], "readwrite")
    .objectStore(storeName)
    .delete(id);

  request.onsuccess = function () {
    console.log("数据删除成功");
  };

  request.onerror = function () {
    console.log("数据删除失败");
  };
}

该种删除只能删除一条数据,必须传入主键。

通过索引和游标删除指定数据

有时候拿不到主键值,只能只能通过索引值来删除,通过这种方式,可以删除一条数据(索引值唯一)或者所有满足条件的数据(索引值不唯一)。

代码如下:

javascript 复制代码
/**
 * 通过索引和游标删除指定的数据
 * @param {object} db 数据库实例
 * @param {string} storeName 仓库名称
 * @param {string} indexName 索引名
 * @param {object} indexValue 索引值
 */
function cursorDelete(db, storeName, indexName, indexValue) {
  var store = db.transaction(storeName, "readwrite").objectStore(storeName);
  var request = store
    .index(indexName) // 索引对象
    .openCursor(IDBKeyRange.only(indexValue)); // 指针对象
  request.onsuccess = function (e) {
    var cursor = e.target.result;
    var deleteRequest;
    if (cursor) {
      deleteRequest = cursor.delete(); // 请求删除当前项
      deleteRequest.onerror = function () {
        console.log("游标删除该记录失败");
      };
      deleteRequest.onsuccess = function () {
        console.log("游标删除该记录成功");
      };
      cursor.continue();
    }
  };
  request.onerror = function (e) {};
}

上段代码可以删除索引值为indexValue的所有数据,值得注意的是使用了IDBKeyRange.only()API,该API代表只能当两个值相等时,具体API解释可参考MDN官网。

关闭数据库

在使用 IndexedDB 时,关闭数据库连接是个好习惯,主要有以下几个好处:

  • 释放资源:每个数据库连接都消耗系统资源,尤其是在数据较多时。如果你不关闭连接,可能会浪费内存和其他系统资源,导致性能下降。
  • 避免连接泄露:如果忘记关闭连接,可能会导致数据库连接泄露。尤其在单页面应用(SPA)中,如果频繁打开数据库连接而没有关闭,可能会引发浏览器崩溃或变得迟钝。
  • 事务完成后的清理:如果数据库连接没有在操作完成后关闭,可能导致事务没有完全提交或者数据库处于不稳定状态。关闭连接能确保所有的操作都完成并提交。
  • 允许数据库升级:关闭连接后,你可以更容易地维护操作。如果连接一直保持着,就无法进行这些操作。
  • 防止并发冲突:IndexedDB 是异步操作,长时间保持连接可能引发并发操作冲突。如果你不及时关闭连接,可能会增加数据库锁定或事务冲突的几率,影响应用的可靠性。

总之,关闭连接可以保持应用的高效、稳定,并防止一些潜在的性能和安全问题。

代码如下:

javascript 复制代码
/**
 * 关闭数据库
 * @param {object} db 数据库实例
 */
function closeDB(db) {
  db.close();
  console.log("数据库已关闭");
}

删除数据库

如果需要删库,删除操作也很简单。

代码如下:

javascript 复制代码
/**
 * 删除数据库
 * @param {object} dbName 数据库名称
 */
function deleteDBAll(dbName) {
  console.log(dbName);
  let deleteRequest = window.indexedDB.deleteDatabase(dbName);
  deleteRequest.onerror = function (event) {
    console.log("删除失败");
  };
  deleteRequest.onsuccess = function (event) {
    console.log("删除成功");
  };
}

总结

操作 API 说明
打开数据库 indexedDB.open(name, version) 打开或创建数据库。 有数据库就打开,没有就创建;如果传入更高的 version,会触发 onupgradeneeded 事件。
创建仓库对象 db.createObjectStore(store_name, { keyPath, autoIncrement }) onupgradeneeded 里调用。 参数: • store_name:仓库(表)名称 • keyPath:指定主键字段 • autoIncrement:是否自动生成主键
创建索引 store.createIndex(index_name, keyPath, { unique }) 为仓库里的字段创建索引,加快查询。 参数: • index_name:索引名称 • keyPath:要索引的字段 • unique:是否唯一
插入 store.add(value, key?) 向仓库里插入新数据。 如果主键冲突会报错。
主键读取 store.get(key) 通过主键读取单条记录。
游标查询 store.openCursor(range?, direction?) 遍历仓库里的数据。 可以加范围 IDBKeyRange 限定查询区间,可选 nextprev 进行排序等。
索引查询 store.index(index_name).get(value) 通过索引来查询数据。
更新数据 store.put(value, key?) 新增或更新数据。 主键存在时覆盖,主键不存在时插入。
删除数据 store.delete(key) 按主键删除一条记录。
关闭数据库 db.close() 关闭当前连接的数据库。
删除数据库 indexedDB.deleteDatabase(name) 删除整个数据库,异步操作。

使用场景

大数据量存储

用途localStorage 存储容量太小(5MB 左右),IndexedDB 可以存储 几十 MB 到 GB 级别 的结构化数据。

示例

  • 大型游戏资源文件(图片、音频)本地缓存
  • 电子书阅读器(如 EPUB 本地解包缓存)
  • 地图数据(如离线地图块)

媒体文件本地缓存与处理

用途:存储 Blob(二进制文件)如图片、视频、音频等,支持文件分片、断点续传。

示例

  • 上传视频断点续传
  • 摄像头采集数据缓存
  • 本地图像编辑器(如 Canva 离线草稿)

表单缓存 & 自动保存草稿

用途:提升用户体验,避免表单丢失或中断填写。

示例

  • 电商下单流程信息暂存
  • 博客撰写时自动保存草稿
  • 简历填写进度记录

加速应用启动 & 网络请求优化

用途:将请求数据缓存到本地,提高首次加载速度,并减少网络请求。

示例

  • 首页卡片数据缓存(推荐列表等)
  • 配置项缓存(字典数据、地区树等)
  • 用户访问记录或预热数据

数据分析 & 可视化缓存

用途:本地缓存数据后用于图表绘制、趋势分析,避免每次请求都重新拉取。

示例

  • 工业/物联网设备数据记录
  • 股票行情本地分析
  • 仪表盘历史数据对比

离线应用(Offline Web App / PWA)

用途:在无网络或网络不稳定时,仍能访问和操作数据。

示例

总结

  • 轻量级增强:表单缓存、草稿、配置缓存
  • 中量级存储:电商订单列表缓存、离线地图块
  • 重量级存储:视频分片、PDF、图像编辑器草稿、PWA 离线应用
相关推荐
然我2 小时前
前端正则面试通关指南:一篇吃透所有核心考点,轻松突围面试
前端·面试·正则表达式
LFly_ice2 小时前
学习React-11-useDeferredValue
前端·学习·react.js
亮子AI3 小时前
【npm】npm 包更新工具 npm-check-updates (ncu)
前端·npm·node.js
信看3 小时前
实用 html 小工具
前端·css·html
Yvonne爱编码3 小时前
构建高效协作的桥梁:前后端衔接实践与接口文档规范详解
前端·git·ajax·webpack·node.js
王源骏3 小时前
Laya使用VideoNode动态加载视频,可以自定义播放视频此处以及位置
前端
一只小风华~3 小时前
Vue: ref、reactive、shallowRef、shallowReactive
前端·javascript·vue.js
阿杆3 小时前
文心快码 3.5S 发布!实测插件开发,Architect 模式令人惊艳
前端·后端·文心快码
文心快码BaiduComate3 小时前
我用Comate搭建「公园找搭子」神器,再也不孤单啦~
前端·后端·微信小程序