K 的一隅

浏览器 浏览器核心机制

浏览器里的小型数据库:IndexedDB 到底适合存什么

从仓库档案和查找索引说起,理解 IndexedDB 的对象仓库、事务、索引与异步读写。

5 分钟阅读 更新于 2026-07-24
本文目录

便利贴适合记一个开关,仓库档案却需要编号、分类、批量盘点和断电后仍能找到。IndexedDB 是浏览器提供的异步结构化数据存储,适合离线数据、草稿、缓存的业务索引和较大的本地记录,不是把所有东西塞进一个字符串柜子。

先看数据如何进仓库

IndexedDB 的 API 以 request 和事件为中心,现代代码通常会在项目里再包一层 Promise,让调用方式更容易组合。

object store 不是表格的完全复制品

对象仓库保存 JavaScript 结构化值,可以用 keyPath 从对象中取主键,也可以单独提供 key。索引(index)让你按某个字段查找,但索引不是自动存在的;需要在数据库升级时显式创建。

js
const request = indexedDB.open('notes', 1)

request.onupgradeneeded = () => {
  const db = request.result
  const store = db.createObjectStore('notes', { keyPath: 'id' })
  store.createIndex('by-updated', 'updatedAt')
}

request.onsuccess = () => {
  const db = request.result
  const tx = db.transaction('notes', 'readwrite')
  tx.objectStore('notes').put({ id: 'a1', title: '草稿', updatedAt: Date.now() })
}

事务是一次有边界的操作

事务(transaction)决定了读写范围和模式:readonly 只能读取,readwrite 可以修改。事务完成前,浏览器会把这组操作作为一个整体处理;中途错误可能导致修改回滚。不要把一个事务跨越用户长时间等待,也不要在事务里依赖不可控的异步交互。

数据库升级由版本号驱动。多个标签页同时打开旧版本时,新版本升级可能等待旧连接关闭。应用需要监听 blockedversionchange,并在页面不再使用旧连接时调用 db.close()

它和 localStorage 有什么不同

localStorage API 简单,但同步读写会占用主线程,值也只能是字符串。IndexedDB 异步、支持结构化数据和索引,适合更大的数据集合;代价是 API 更复杂,调试和迁移也需要更认真地设计。

IndexedDB 也不是永久磁盘。浏览器可能因为用户清理站点数据、存储压力或隐私策略删除它。关键数据仍应有服务器或导出方案,离线副本要允许重建。

先设计数据,再设计 API

离线应用常见的做法是把服务器实体按业务主键保存到对象仓库,例如 notesmessagesoutboxoutbox 可以保存尚未同步的操作,网络恢复后按顺序发送;服务器确认成功后,再在一个事务里删除已完成的操作。这样页面刷新不会丢掉“用户已经点击但还没发出去”的意图。

同步冲突不能交给 IndexedDB 自动决定。你需要为记录保存版本号、更新时间或操作序列,并定义“服务器优先”“最后写入优先”或人工合并等规则。数据库只负责可靠地保存本地状态,不知道哪一份内容更符合业务。

事务边界会影响可靠性

一次事务里可以完成互相依赖的多次读写,例如读取草稿、更新同步状态和写入日志。把相关操作拆成多个事务,中间一旦页面关闭,就可能留下半完成状态。反过来,事务也不宜无限扩大:大量写入会阻塞其他操作,升级数据库时还可能让多个标签页互相等待。

使用 IndexedDB 时,值得同时记录数据版本和迁移步骤。升级函数应能从旧版本逐步迁移到新版本,不能只假设用户一定从版本 1 直接打开最新版。开发阶段还要测试数据库为空、升级被阻塞、事务失败和配额不足这几条路径。

IndexedDB 的价值还在于它能把“页面状态”和“服务器状态”之间的间隙保存下来。用户填写表单时,先写入本地草稿;提交时生成一条待同步操作;同步成功后再标记完成。页面因此不必把所有数据都放在内存里,也不必把每次按键都立即发到服务器。只要为数据设置过期策略、容量上限和清理入口,本地数据库就能成为可靠的缓冲层,而不是一座无人管理的垃圾仓库。

查询设计也应该从页面真正的读取方式出发。若页面总是按 id 取记录,主键已经足够;若要按状态筛选、按时间排序,再考虑对应索引。一次读取很多记录时,可以使用游标或分页,而不是把整个仓库一次性加载到内存。数据规模增大后,最值得优化的往往不是某个 API 写法,而是记录结构、索引数量和读取范围。

调试时可以在 Application 面板查看数据库、对象仓库和索引,但不要只在开发环境验证。真实用户可能有旧版本数据库、多个标签页同时打开,甚至在存储空间紧张的设备上使用页面。迁移脚本要可重复测试,失败时要有明确的恢复策略;否则一次字段改名就可能让离线用户永远打不开自己的草稿。

应用还应给用户一个清理或导出入口,尤其是离线编辑器和缓存型应用。存储空间不是无限的,数据也可能因为隐私设置被清除。让用户知道哪些内容只在本机、哪些内容已经同步,通常比单纯增加一个更大的仓库更重要——本地数据的可见性和可删除性,也应成为产品设计的一部分。

容易踩的坑

IndexedDB 不是 localStorage 的大号版本:它有对象仓库、索引、事务和版本升级。写入成功也不保证永久保存,配额与隐私策略都能清掉数据。索引更不是越多越好——每条索引都有写入与存储成本,只为真实查询路径建立。