☰
IndexedDB 实战指南:从数据结构、事务索引到性能优化
2026/10/17 11:19:56 网站建设 项目流程

说实话,前端圈里聊到本地存储,大家的第一反应大多是 localStorage 或者 sessionStorage,只要数据量稍微大点儿就想着往后端塞。但真碰到需要离线使用、需要存几万条甚至几十万条结构化数据的场景,比如一个完整的工时台账、一套本地题库,或者一个带历史版本的图片编辑器,localStorage 那 5MB 到 10MB 的配额和同步读写的方式,用起来会非常难受。这时候就该 IndexedDB 登场了。

IndexedDB 是浏览器自带的一个 NoSQL 数据库,它能把大量结构化数据直接落在用户本地磁盘上,支持索引、事务、游标和范围查询。很多同学一查文档,看到IDBDatabase、IDBObjectStore、IDBTransaction这一堆连字符名词就头大,跟着教程抄一遍能跑,换个场景又不会了。这篇文章我不打算罗列 API 手册,也不会贴一堆用不到的配置代码,而是想从“这个数据库到底是怎么组织数据的”这个底层模型讲起,配合我实际开发中踩过的坑,把 IndexedDB 从打开到增删改查、从索引设计到性能优化这条线完整捋一遍。适合刚接触 IndexedDB 的前端开发,也适合用过一点但经常被版本升级、事务失效问题折腾的人。

1. 为什么是 IndexedDB 而不是 localStorage:先搞清它能解决什么问题

1.1 localStorage 的三宗罪

我在接手某个管理后台的离线模式改造时,第一步就是把所有存 localStorage 的代码揪出来。那个项目原本用 localStorage 存了大约 2000 条工单记录,每条记录还包括 JSON 序列化之后的附件信息。结果页面一打开就卡,因为 localStorage 的读写是同步的,读取 2000 条 JSON 字符串再JSON.parse,主线程被硬生生堵了一两秒,而且这个操作发生在点击工单列表的时候,用户体验非常割裂。

localStorage 的问题,最主要体现在三个方面。

第一是容量天花板太低。虽然标准没有强制限制,但各家浏览器基本卡在 5MB 左右,按 UTF-16 编码计算,实际能塞进的中文字符也就一百多万个,存点纯文本配置没问题,一旦涉及历史数据、日志快照或者文件内容缓存,立刻就会写满。

第二是同步阻塞。每一次localStorage.getItem()和setItem()都会阻塞主线程,数据量小的时候感知不强,数据量一上来整个页面就像被点了暂停。

第三是只能存字符串。存对象必须先 JSON 序列化,读出来再反序列化,等于每次访问都在做两次编解码。而 IndexedDB 的 structured clone 算法原生支持 ArrayBuffer、Blob、Date、Map、Set,还能保留对象内部的循环引用。所以如果你发现自己写了很多JSON.stringify和JSON.parse,就该考虑换数据库了。

1.2 IndexedDB 弥补的正是这三个缺口

IndexedDB 是一个真正的嵌入式数据库,运行在浏览器内,底层数据结构是对象仓库(object store),和表的概念类似,但更灵活。它支持索引,意味着你可以为对象的任意字段建立索引,实现类似 SQL 里where和order by的查询能力;它支持事务,保证了多步读写操作的原子性,不会写一半失败导致数据残缺;它的读写都是异步的,几乎所有操作都以请求对象的形式存在,不会阻塞 UI 渲染;存储空间则和浏览器可用磁盘空间挂钩,通常远大于 localStorage。

举一个最直观的例子。我之前做一个离线题库应用,需要把 8000 多道题和答题记录存在本地。用 IndexedDB 存一个questions仓库,字段包括题干、选项、知识点、难度、题目类型,再额外建一个按知识点查询的索引。用户在搜索框里输入关键词,通过索引做范围查询,返回速度在毫秒级。这个量级的数据如果用 localStorage 存,每道题序列化之后大概 2KB,总体积超过 16MB,直接写满,而且每次全量读取都要经历一次大 JSON 解析,页面会白屏很久。

1.3 什么场景适合 IndexedDB,什么场景不适合

适合的场景很清晰:

  • 离线优先的应用,比如待办事项、笔记、工时记录、邮件客户端,断网时数据照常读写,联网后同步。
  • 需要在本地存储大量结构化记录,比如历史版本、审计日志、消息记录。
  • 需要按多个字段组合查询的场景,比如“找出所有未完成的、优先级为高的任务”。
  • 需要存储二进制数据或者文件对象的场景,比如本地保存图片编辑历史、音频缓存。

不适合的场景同样存在。比如极度高频的临时键值存取,只是一个插件开关、一个页面配置,几十个字段撑到天也就几十 KB,用 IndexedDB 反而是杀鸡用牛刀, indexDB 的异步逻辑会带来额外的编码复杂度,启动和事务提交的开销也要比 localStorage 高一个量级。另外,如果需要全表扫描和复杂的关联查询,IndexedDB 并不是一个好选择,它的数据结构偏文档型,关联查询需要你在代码层自己维护外键关系。

2. IndexedDB 的数据组织模型:数据库、仓库、索引和游标的关系

2.1 用书架类比理解四个核心概念

很多教程上来就贴 API,直接把人劝退。其实 IndexedDB 的抽象层特别简单,完全可以类比成图书馆里的书架系统。

整个浏览器的 IndexedDB 可以想象成图书馆大楼,里面有很多个独立的图书馆,也就是 database。每个 database 有名字和版本号,名字用来区分用途,版本号用来标记结构变化的代次。一个 database 底下有多个对象仓库,也就是 object store,可以理解成一个个书架。每个书架专门放一类书,对应项目里的一类对象。书架里的每本书都有一个编号,就是主键 keyPath;书的内部有一个或几个标签字段,对应索引 index,方便你按不同维度快速检索。当你需要按顺序翻阅某些书,或者在支架间移动书时,使用的就是游标 cursor。

这样理解之后,再回来看 API 就顺理成章了:indexedDB.open('library')是打开图书馆;db.createObjectStore('books')是摆一个书架;store.createIndex('by_author')是给书架加一套按作者排序的标签;store.getAll()是取全部书;IDBKeyRange是查询时圈定范围。

2.2 主键的三种定义方式

每个 object store 都必须有一个主键来唯一标识一条记录。主键的定义方式有三种,这个经常有人说不清楚。

第一种是直接指定 keyPath,比如每本书对象里有个id字段,定义时写{ keyPath: 'id' },数据库会自动把对象的id字段当作主键。第二种是使用自增主键,配置{ keyPath: 'id', autoIncrement: true },你插入对象时可以不传 id,IndexedDB 自动生成递增整数。第三种是外部提供主键,不设置 keyPath 和 autoIncrement,插入数据时把主键作为第二个参数传进去:store.add(value, key)。

这三种方式各有适用场景。业务数据自带唯一 ID 时,比如服务器返回过来的 UUID,直接用 keyPath 最省事。本地增量数据、没有天然唯一标识的,比如一条日志、一条操作记录,用 autoIncrement 更合适。第三种方式最灵活但最容易出错,因为需要你手动保证 key 唯一,在实际项目中我很少用,除非数据源给的 key 本身就是正确且稳定的。

值得一提的是,主键不一定要是数字或字符串,一个 Date 实例也可以作为 key,因为 IndexedDB 内置了 key 的排序规则,数字按数值大小,字符串按字典序,Date 按时间先后。

2.3 keyPath、key、value 之间的关系容易踩的坑

把对象写进 store 的时候,新手最容易误以为 key 就是对象本身。实际上 key 是 key 字段的值,value 是除 key 字段之外的整个对象体。当设置了keyPath之后,IndexedDB 会读取对象里这个字段作为 key,同时这个字段也会保存在对象体里。当你读取一条记录时,返回的对象仍然包含 key 字段。

有个特别容易踩的坑:如果你用put()覆盖一条数据,但传入的对象里缺少 keyPath 字段,会直接抛DataError。这在更新场景里很常见,比如你先从 store 里 query 出一条旧数据,改了其中几个字段,然后忘记把主键字段原样带回,再 put 进去就会报错。我自己的习惯是,只要涉及 update,一律先store.get(key)拿到旧对象,展开后修改再put,并显式检查返回对象的 key 字段是否还在。

3. 从打开数据库到增删改查:完整走一遍 IndexedDB 核心 API

3.1 打开数据库:版本号是一把双刃剑

打开数据库的调用方式是indexedDB.open(name, version),第二个参数可以不传,默认走当前已有版本。这里有个非常关键的机制:当指定的版本号大于当前版本时,浏览器会触发onupgradeneeded事件,这就是你建仓库、建索引的唯一时机;当版本号和当前版本一致时,直接触发onsuccess;当版本号小于当前版本时,直接触发onerror,并且报错显示版本冲突。

这个机制设计得既妙又坑。妙在它把数据库结构的变更纳入了版本管理,每个版本对应一套 schema,应用旧代码打开新库不会因为缺字段而崩溃。坑在很多初学者把版本号当成“数据版本”来用,每次都写open('db', Date.now()),结果每次刷新页面都触发 upgrade 事件,等到 upgrade 里发现对象仓库已经存在,又抛出ConstraintError。

正确的做法是:版本号只在 schema 变化时手动增加一次,平时保持常量。比如你写死const DB_VERSION = 1;,开发过程中发现需要增加索引,改成 2,发布后下次用户打开,浏览器检测到版本从 1 变成 2,自动执行升级,如果你改成了 3,那它毫不知情,也不会有人触发升级,数据就一直停留在旧结构,直到老用户手动清理站点数据。

3.2 建库建表建索引的完整示例

下面给出一段我写过的、可以直接运行的基础代码,包含建库、建 store、建索引、插入和查询:

// 打开数据库,版本 1 const request = indexedDB.open('my-daily-notes', 1); let db; request.onupgradeneeded = function (event) { const db = event.target.result; // 如果仓库不存在,则创建 if (!db.objectStoreNames.contains('notes')) { const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true, }); // 按标题建索引,支持精确匹配 store.createIndex('title', 'title', { unique: false }); // 按创建时间建索引,支持范围查询和排序 store.createIndex('createdAt', 'createdAt', { unique: false }); } }; request.onsuccess = function (event) { db = event.target.result; console.log('数据库打开成功'); }; request.onerror = function (event) { console.error('数据库打开失败', event.target.error); };

打开成功后,任何对数据库结构的修改都不能再来一次,比如在onsuccess里调用db.createObjectStore会直接抛错。这是初学者最容易误解的地方:他们以为打开后随时可以加字段加表,其实 IndexedDB 的 schema 以版本为单位固化了下来。

3.3 事务驱动的读写操作

IndexedDB 的所有读写都必须发生在一次事务中。拿到数据库实例后,你调用db.transaction('notes', 'readwrite')获得一个事务对象,再通过这事务去拿 store 和 index,操作完后事务自动提交。

我简化后的增删改查示例:

// 新增一条记录 function addNote(note) { const tx = db.transaction('notes', 'readwrite'); const store = tx.objectStore('notes'); const request = store.add(note); request.onsuccess = () => console.log('写入成功', request.result); request.onerror = (event) => console.error('写入失败', event.target.error); } // 根据主键查询 function getNote(id) { const tx = db.transaction('notes', 'readonly'); const store = tx.objectStore('notes'); const request = store.get(id); request.onsuccess = () => { if (request.result) { console.log('查到了', request.result); } else { console.log('没有这条数据'); } }; } // 更新:先取旧数据再替换 function updateNote(id, newContent) { const tx = db.transaction('notes', 'readwrite'); const store = tx.objectStore('notes'); const getRequest = store.get(id); getRequest.onsuccess = () => { const old = getRequest.result; if (!old) return; const updated = { ...old, ...newContent }; store.put(updated); }; } // 删除 function deleteNote(id) { const tx = db.transaction('notes', 'readwrite'); const store = tx.objectStore('notes'); store.delete(id); }

这里有一个常常被忽略但非常实用的细节:store.add()和store.put()的区别。add 在遇到相同主键时会直接抛错,而 put 会覆盖旧数据。如果你需要实现“存在则更新,不存在则插入”的 upsert 语义,用 put 就行;如果你要防范重复写入,add 更稳。

3.4 事务何时自动提交:这是最容易翻车的点

IndexedDB 的事务是自动提交的,不需要你手动调 commit。提交的时机是所有在这个事务上发起的请求都执行完毕,并且微任务队列清空之后。这意味着你不能等着一个请求完成之后在它的回调里再密集发起新请求,否则新请求会掉到事务提交之后,这时再访问事务里的 store 就会抛出TransactionInactiveError。

我在刚接触 IndexedDB 时犯过一个很典型的错误:在一个事务里先删掉所有旧数据,然后循环插入新数据。我当时在图省事,直接在 delete 请求的onsuccess回调里启动循环逐条 add,结果前几条能写,后面的循环里事务已经自动提交,再拿 store 就抛异常。正确做法是,把需要等待前一步结果才能执行的后置操作,放到新的事务里重新发起,而不是试图延长同一个事务的生命周期。虽然 IndexedDB 2.0 之后允许通过tx.oncomplete和异步循环来延长一点生命周期,但依赖这种机制非常不可靠,建议事务的粒度尽量小,一步一个事务。

4. 版本升级的完整机制与升级迁移策略

4.1 onupgradeneeded 里的流程

很多教程建议“数据库和仓库都在 onupgradeneeded 里创建”,这让不少开发者误以为每次打开数据库都会触发这个事件。前面已经说过,只有在版本号变大时才会触发。触发时,事件对象里可以拿到三个关键对象:event.target.result是数据库实例;event.target.transaction是当前升级流程的自动事务;event.oldVersion表示旧版本号。

在这个回调里,你可以做三件事:建新 store、建新索引、迁移旧数据。迁移旧数据的方法比较特殊,虽然事务是自动开启的,但在升级期间,你可以从别的 store 里读取旧结构,再把处理后的数据写进新 store,由于当前处于versionchange事务中,所有操作在同一事务里完成,天然具备原子性,不会出现改了结构写到一半失败了导致数据长错的情况。

如果结构改坏了,想让浏览器从零重建,最简单的办法是indexedDB.deleteDatabase(dbName),删除后重新 open。但注意 deleteDatabase 是异步的,而且如果当前有页面还持有数据库连接,删除操作会等待连接关闭,这里的关闭需要你显式调用db.close()。

4.2 我在生产环境里遇到过的版本升级事故

有次我在线上项目里给一个 store 新增索引,把版本号从 2 改成 3,自测没问题就发布了。结果第二天收到用户反馈说数据全丢了。排查了很久才发现,我的升级代码里写的是:

request.onupgradeneeded = function (event) { const db = event.target.result; const store = db.createObjectStore('tasks', { keyPath: 'id' }); store.createIndex('dueDate', 'dueDate'); };

问题出在:这个 store 在版本 1 时已经建过了,版本升级到 3 时,旧 store 还在,此时再createObjectStore同名仓库就会抛ConstraintError,整个升级事务回滚。而抛出约束错误之后,浏览器认为升级失败,打开的数据库请求也进入错误状态,用户现有的数据库虽然没被删掉,但应用逻辑已经没法继续了,在用户感知里就是“数据没了,功能坏了”。

教训有两条。第一,升级代码里必须有保护性判断,用db.objectStoreNames.contains()检查某个 store 或索引是否已经存在,存在就不重复创建。第二,升级事务和普通事务一样,任何未捕获的错误会导致事务中止。升级过程中如果出现意料之外的数据格式,最好在事务出错时不立刻中断,而是记录错误状态并尝试让应用以旧版逻辑兼容运行,至少给用户留一个导出数据的入口。

4.3 如何安全地设计一套可平滑升级的结构

经验之谈,建议把所有结构变更都做成幂等的:无论同一版本触发多少次,执行结果都一样。关键在于判断老结构,而不是依赖“第一次执行”这种假设。每次升级时先列出新版本号相比所有旧版本号的差异,把迁移逻辑写成表格驱动:

新版本号store 变更索引变更数据迁移
1创建 tasks无无
2无为 tasks 添加 status 索引无
3创建 teams无把 tasks 里的 teamName 同步到 teams

设计这套表的目的是让代码结构清晰,每加一版只增加对应的逻辑分支,避免在旧分支基础上频繁修补。升级逻辑尽量短小,不要在 onupgradeneeded 里做大规模数据清洗,那会让升级耗时过长,升级期间其他标签页的数据库连接都会收到onversionchange事件,如果没处理,可能导致其他标签页崩溃。

5. 索引与游标:查询性能的胜负手

5.1 为什么查询高维数据离不开索引

在没有索引的情况下,IndexedDB 的查询方式只有两种:按主键 get 单条数据,或者 getAll 全量拉取。一旦数据量上了几万,全量 get 后内存中做 filter 会带来明显的卡顿和内存压力。而索引的作用,本质上就是维护一个额外排序的键值列表,让查询可以直接跳到目标区域,不需要全表扫描。

比如要查“所有今天创建的任务”,主键是自增 id,和创建时间没关系,如果只靠主键,只能遍历所有记录,在内存里比对 createdAt。加了 createdAt 索引之后,一句index.getAll(IDBKeyRange.bound(今天0点, 今天23点59分59秒))就能直接拿到目标集合。

5.2 游标的分类与使用细节

游标是 IndexedDB 里最有特色也最容易被忽略的 API。游标不是把数据全拉到内存,而是像指针一样逐条迭代,非常适合分批处理大量记录。

openCursor()可以在 store 或 index 上调用,参数是查询范围和方向。方向有四种:next按 key 升序;nextunique跳过重复的索引 key;prev降序;prevunique降序且跳过重复索引 key。需要注意,如果在 store 上直接 openCursor,排序依据是主键;如果在 index 上 openCursor,默认排序依据是索引字段值,主键只作为第二位排序。

游标回调里,你需要在每次onsuccess里判断event.target.result,如果结果不是 null,则用cursor.value处理当前值,然后调用cursor.continue()移动指针。这段逻辑虽然略显繁琐,但它给了你绝佳的中断机会——比如遍历到第十万条发现已经找到目标,直接cursor.continue()不再调用即可停止,内存里只保留了一小部分数据。

5.3 复合索引和唯一索引的实战用法

索引不仅可以建立在单字段上,还可以建立在多个字段上,形成一个复合索引。复合索引的排序规则是先按第一个字段排,再按第二个字段排。一个典型的场景是“按日期分组并按优先级排序”:创建索引['createdAt', 'priority'],这样在同一个 createdAt 范围内,priority 优先级高的记录会排在前面。

复合索引的一个限制是,范围查询只能作用于最左边的字段。比如你建立了['createdAt', 'priority']索引,那么可以按 createdAt 做范围查询,再在结果里看 priority 排序,但不能单独用 priority 做范围查询。这个限制和关系型数据库联合索引的“最左前缀原则”非常像,设计索引时优先考虑查询条件最常用的字段。

唯一索引则可以用来保证某个字段不重复。比如用户表里希望邮箱唯一,可以建store.createIndex('email', 'email', { unique: true })。插入重复邮箱时会抛ConstraintError。注意一点:unique 索引在字段值为 null 或 undefined 时,多个 null 记录并不会被判定为冲突,这是 IndexedDB 一个比较隐蔽的语义,应用层最好自己再做一次空值判断。

5.4 索引不是越多越好

每建一个索引,写入时就要多维护一份排序结构,写入性能会下降,数据库体积也会膨胀。我的一个比较极端案例:某个状态管理模块为了灵活查询,一口气为十几张虚拟表的每个组合字段都建了索引,结果写入全量表级数据时耗时从 3 秒涨到 15 秒。后来我砍掉了超过一半的索引,优先保留高频率的等值和范围查询字段,剩下所有自定义 filter 都在内存里做,写入性能才恢复可接受水平。

6. 数据安全、异常处理和调试工具

6.1 隐私模式、存储配额与数据清除的边界

IndexedDB 在很多场景下被视为“持久存储”,但它并不能保证数据一定永久存在。浏览器有存储压力时,会根据 LRU 算法驱逐标记为可清除的数据,尤其当用户开启了无痕/隐私模式时,所有的 IndexedDB 数据都会在会话结束时被清零。

另外需要知道,IndexedDB 受同源策略限制,每个源下的数据彼此隔离。你不能跨域名访问另一个站点的 IndexedDB 数据库。给用户做数据导入导出的时候,如果涉及跨域,必须绕道服务端或者用户手动导出文件,不能直接读对方域名的 IndexedDB。

针对“被清除”的风险,有一个兜底方案:定期把重要数据同步到远端,或者提供导出 JSON 的功能。我在做离线笔记工具时,每次用户切换出页面时自动把全量笔记序列化为 JSON,体积不大就塞进 localStorage 作为第二副本,体积大就触发一次下载备份,这样即便 IndexedDB 被清理,也能从 localStorage 里救回来大部分数据。

6.2 最常见的错误类型与定位方法

结合多年的踩坑经验,IndexedDB 的报错虽然五花八门,但高频的其实就那么几类:

  • AbortError:常见于事务被中断或 aborted,比如在事务中添加了重复主键时,整个事务默认中止。
  • ConstraintError:数据冲突,比如 add 到已有主键、unique 索引重复,或者重复建 store/index。
  • DataError:传入的数据不符合 key 规则,最常见的是 KeyPath 字段缺失或类型不对。
  • TransactionInactiveError:事务已经自动提交,但代码还在使用事务中的 store 或 index,这是异步边界把握不好导致的。
  • VersionError:要打开的版本号小于当前已有版本号。

当遇到类似报错,优先考虑是不是异步时序问题,而不是急着查官方文档。我常用的调试手段是给每个事务的oncomplete、onerror、onabort都挂上 log,打印当前事务的 mode 和 store 名称。事务层面能看出是提交前还是提交后触发的问题。

6.3 浏览器 DevTools 的 Application 面板怎么用

浏览器的开发者工具里,IndexedDB 的调试支持其实相当完善。打开 Application 面板,左侧会列出当前源下的所有 IndexedDB 数据库,点进去能看到每个 store 的记录列表,右上角搜索框支持按主键查找记录,双击记录可以编辑值,也可以直接清空整个 store。

有一个我建议每个项目都加上的调试思路:在应用中内置一个“导出数据库”功能。用循环遍历所有 store,把每条记录转成 JSON 收集到一个对象里,然后生成 Blob 下载。遇到线上 bug 但用户现场无法快速排查时,让用户导出当前库发给分析,远比让用户在 Console 里手动执行命令高效。

7. 性能优化思路与封装策略

7.1 批量写入的权衡

IndexedDB 的写入性能受限于事务的原子性和磁盘 I/O。许多开发者把多条 add 放在同一个事务里,期待能比逐条提交快很多,但实际情况是需要看代码怎么写的。如果在一个事务里串行连续 add,每次 add 都会触发一个请求,事务要等待所有请求完成,此时如果把 add 请求全部同步发起,速度会快很多,但也要小心内存里一次性塞入太多大对象导致占用飙升。

我的做法是,把一次需要写入的列表按每 2000 条一个 chunk 切成多组,每个 chunk 开启独立事务,chunk 内所有 add 请求同步发起,等最后一个 chunk 的事务完成后回调处理下一批。这种模式在写入 10 万条记录时,比“每条一个事务”快大约两个数量级。

7.2 读取阶段的常见优化

读取阶段最常见的瓶颈是“取全量后在内存中过滤”。能使用索引和 IDBKeyRange 的查询就尽量用,尽量减少 getAll 然后 filter 的场景。另外在查询结果特别大时,不要一次性getAll()返回所有值,可以改用openCursor()分批遍历,配合cursor.update()或cursor.delete()直接在遍历中修改数据,省掉再走一遍写事务的往返。

7.3 一个轻量封装思路:把 Promise 化做到位

IndexedDB 是事件驱动的,但现代前端习惯用 Promise。官方虽然有基于 Promise 的新版 API 提案,但现有浏览器上主流的做法仍然是封装。我建议的最小封装包含几个函数:openDB负责打开并维护一个全局的 db 实例、addItem/putItem/getItem/deleteItem/clearStore提供基础的 CRUD、queryByIndex包一层索引查询。封装的核心价值是统一处理事务生命周期,把所有 onsuccess/onerror 转成 resolve/reject,同时把事务里取 store 的操作集中绑定,避免我们日常手动造事务时因异步边界导致各种隐性问题。

比如我常用的 get 封装是这么写的:

function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getItem(storeName, key) { if (!db) return null; const tx = db.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); return promisifyRequest(store.get(key)); }

不过封装文件要注意别过度设计。很多轮子会把 Schema 也抽象出来,反而把每个项目的版本升级伪装得比直接用 API 更复杂。我最终留下的封装只有几层薄壳,能完成日常 90% 的简单读写和索引查询就够用。

7.4 展望一下 IndexedDB 的演进方向

IndexedDB 已经存在了很多年,如今浏览器对它的事务和游标模型仍在持续改进。比如有环境已经支持了基于 Promise 的新 API 风格;还有私有的文件系统存储方案,让应用可以创建可写文件,适合做离线下载大文件、本地图片管理这类场景。现在去规划前端本地存储方案,我更建议把 IndexedDB 作为底层基础设施,在上面做一层适配层,无论未来标准怎么变化,业务代码不会频繁重写。

存储是一个工程的底座,Online 的时候谁能更快从远端拿数据,Offline 的时候谁能把数据稳稳地放在用户手里,这恰恰是产品质量的分水岭。IndexedDB 不是银弹,也不适合所有场景,但当你真的需要把几万条甚至几十万条结构化数据放进本地,并且还需要可靠、高效地查询时,它几乎就是标准答案。至少在这几年的实践中,我对这个结论从没有怀疑过。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询