〇、前言
前文已经介绍了*,基础键值存储 Local storage 和 Session storage,*那么本文再介绍另一新的类别:结构化数据库与缓存。
结构化数据库与缓存,涵盖 IndexedDB、Cache storage 和 Storage buckets。
- IndexedDB 是一个事务型数据库 ,支持存储大量结构化数据,适合复杂应用如离线文档编辑器或媒体播放器,属于应用数据存储;
- Cache storage 主要用于 Service Worker 缓存网络资源 ,实现离线访问和快速加载,属于资源性能存储;
- Storage buckets 则是更底层的存储分区机制 ,允许开发者按策略管理存储空间,常用于大型应用的资源隔离与配额控制,属于资源性能存储。
下面来详细介绍下。
一、什么是 IndexedDB?
1.1 简介
IndexedDB 是浏览器提供的客户端事务型 NoSQL 数据库,专为存储大量结构化数据而设计,支持异步操作、事务机制和索引查询,适用于构建离线优先的 Web 应用(如:PWA-Progressive Web App,渐进式 Web 应用)。
其核心价值在于突破 localStorage 的容量与性能限制,允许直接存储复杂对象、二进制数据(Blob),并通过索引实现高效查询,同时避免阻塞主线程。
IndexedDB 是 W3C 推荐的浏览器原生客户端数据库标准,作为 WebSQL 被弃用后的主流替代方案。它属于 NoSQL 类型数据库,以对象仓库(Object Store)而非表格形式组织数据,数据以键值对形式存储,支持 JavaScript 对象、数组、Blob 等复杂类型,无需序列化。
若需简化开发,可使用封装库(如:Dexie.js、localForage),它们提供 Promise API 和自动降级支持,大幅降低原生 IndexedDB 的复杂度。实际项目中,应优先评估数据规模与查询需求------当数据量 >100KB 或需复杂查询时,IndexedDB 是比 localStorage 更可靠的选择。
与 localStorage/WebSQL 的关键区别:
| 对比维度 | localStorage / WebSQL | IndexedDB |
|---|---|---|
| 容量上限 | 通常限制为 5MB,且同步操作易阻塞 UI | 无固定上限(一般 ≥250MB,可达设备空闲磁盘的 50%~60%) |
| 数据操作方式 | 仅支持字符串键值对,无索引和事务支持 | 通过事务机制保证数据一致性,并支持多字段索引加速查询 |
| 同步性 | 操作是同步的,大数据量时会导致界面卡顿 | 完全异步,通过事件或 Promise 执行,不阻塞主线程 |
1.2 关键特性
1)核心能力
事务支持:操作必须在事务中执行,支持 readonly 和 readwrite 模式。 事务具备原子性------任一操作失败则整个事务回滚,确保数据一致性。
索引优化查询:可为对象仓库的任意字段创建索引 (如:name、updated_at),避免全表扫描。时间范围查询需配合 IDBKeyRange.bound() 使用。
二进制数据存储:直接存储 Blob/File 对象(如:图片、视频),无需转换为 Base64 字符串,适用于离线资源缓存。
2)限制与约束
同源策略:数据库仅限创建它的同源页面访问 ,无法跨域读写。
版本控制机制:数据库通过整数版本号 管理结构变更。版本号必须为整数(如:2.1 会被取整为 2),升级时需触发 onupgradeneeded 事件迁移数据。
隐身模式行为:数据仅驻留在内存中,关闭窗口后自动清除,且配额通常 ≤120MB(常规模式可达数 GB)。
1.3 核心概念架构
1)核心组件
| 组件 | 说明 |
|---|---|
| 数据库 | 顶层容器,每个数据库有唯一名称和整数版本号。 |
| 对象仓库 | 类似 NoSQL 的"集合",存储键值对数据。必须指定主键路径(keyPath)。 |
| 索引 | 基于字段的快速查询通道,可设为唯一(unique: true)或非唯一。 |
| 事务 | 操作的原子单元,限定作用域(如:db.transaction(['users'], 'readwrite'))。 |
2)数据组织逻辑
主键设计:优先使用业务主键 (如:user_id、message_id)作为 keyPath,避免依赖自增 ID(autoIncrement)导致多端同步冲突 。
分域存储:按业务拆分对象仓库(如:user_settings、chat_messages),避免单表膨胀导致锁竞争。
1.4 基本操作:初始化、增、删、改、查、索引查询
// 【初始化】
const request = indexedDB.open("MyDB", 1); // 名称+版本号
// 首次创建或升级版本时触发
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象仓库(主键为 id)
const store = db.createObjectStore("users", { keyPath: "id" });
// 为name字段添加索引
store.createIndex("name", "name", { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result; // 获取数据库实例
};
// 关键点:版本升级逻辑必须在 onupgradeneeded 中完成,否则无法修改结构
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
// 【添加数据 add】
const request = store.add({ id: 1, name: "Alice", age: 25 }); // 主键需唯一
request.onsuccess = () => console.log('数据添加成功');
request.onerror = (event) => console.error('数据添加失败:', event.target.errorCode);
// 【删除数据 delete】
const request = store.delete(id);
request.onsuccess = () => console.log('数据删除成功');
request.onerror = (event) => console.error('数据删除失败:', event.target.errorCode);
// 【修改】
const request = store.put({ id: 1, name: "Alice", age: 26 });
request.onsuccess = () => console.log('数据更新成功');
request.onerror = (event) => console.error('数据更新失败:', event.target.errorCode);
// 【简单查询 get】
const request = store.get(id);
request.onsuccess = (event) => {
if (event.target.result) {
console.log('查询结果:', event.target.result);
} else {
console.log('未找到 ID 为', id, '的数据');
}
};
request.onerror = (event) => console.error('数据查询失败:', event.target.errorCode);
// 【索引查询】
const index = store.index("name");
// 查询所有 name = "Alice" 的记录
index.getAll("Alice").onsuccess = (e) => console.log(e.target.result);
除了 getAll(),IndexedDB 的索引对象还提供了其他几种常用的查询方法,以应对不同的业务场景:
| 方法 | 说明 | 返回值 |
|---|---|---|
index.get(value) |
获取匹配该索引值的第一条记录 | 单个对象或 undefined |
index.getAll(value) |
获取匹配该索引值的所有记录 | 数组 [] |
index.getAllKeys(value) |
获取匹配该索引值的所有主键 | 主键数组 [] |
index.count(value) |
统计匹配该索引值的记录数量 | 数字 Number |
注意:
事务上下文:在执行 store.get() 或 store.index() 之前,请确保当前处于一个有效的 readwrite 或 readonly 事务(Transaction)中。如果事务已经结束或发生错误,调用这些方法会抛出异常。
索引必须提前创建:store.index("name") 中的 "name" 必须在数据库初始化(onupgradeneeded)时通过 objectStore.createIndex('name', 'name', { unique: false }) 创建过,否则会报错。
分批提交:单次事务写入 500~2000 条,避免长时间阻塞。
主动让出主线程:用 queueMicrotask 或 setTimeout 间隔执行批次。
1.5 典型应用场景
1)离线优先应用(PWA)
离线数据持久化:用户操作实时存入 IndexedDB,网络恢复后同步至服务器。
配合 Service Worker:缓存静态资源(Cache API),IndexedDB 存储动态数据,实现完整离线体验。
2)大数据量客户端缓存
结构化数据缓存:商品目录、用户历史记录等 >100KB 的数据,避免频繁请求服务器。
性能敏感场景:对比测试显示,2 万条商品数据(约 4.8MB)的读写,IndexedDB 比 localStorage 快 3.8~6.3 倍,索引查询效率提升 ≥16 倍。
3)二进制资源管理
图片/文档本地缓存:存储 Blob 数据,实现"秒开"体验(如文档查看器、图片库)。
游戏状态保存:关卡进度、玩家属性等需结构化存储的场景。
1.6 使用注意事项与最佳实践
1)工程化设计原则
按业务域拆分仓库:避免将用户配置、日志、临时数据混存于同一对象仓库。
索引精简化:仅高频查询字段建索引,长文本字段(如:content)不适合索引,全文检索应交由专用库(如:FlexSearch)。
2)容量与清理策略
主动监控配额:通过 navigator.storage.estimate() 检测用量,当 usage/quota > 0.8 时触发清理。
分级清理机制:优先删除带 expiresAt 的临时数据(如:日志),再清理旧快照,避免误删核心数据。
3)隐私与安全
敏感数据加密:IndexedDB 本身 无内置加密,存储密码等信息需先通过 Web Crypto API 加密。
隐身模式限制:数据仅内存驻留,关闭窗口即销毁,不可依赖其持久化能力。
二、什么是 Cache storage?
2.1 简介
Cache Storage 是浏览器提供的专用于缓存网络请求/响应对(Request/Response)的持久化存储机制,由开发者通过 JavaScript 显式控制,独立于 HTTP 缓存头,主要用于实现离线优先的 Web 应用(如:PWA)。
其核心价值在于完全自主管理资源缓存策略(如:离线访问、网络回退),而非依赖服务器设置的缓存规则。
Cache Storage 将缓存控制权交给开发者,使其能精准实现离线场景需求。
实际使用时,应避免将其视为"自动缓存"工具------所有缓存操作必须显式编码,且需严格管理生命周期。
对于简单场景,可结合 workbox 等库简化策略实现;若仅需缓存静态资源,优先通过 HTTP 头配置强缓存,而非过度依赖 Cache Storage。
Cache Storage 是 W3C Service Workers 规范的一部分,但 不限于 Service Worker 环境,也可在主线程通过 caches 全局对象访问。它专为存储HTTP 请求-响应对设计,直接缓存二进制资源(如:HTML、CSS、JS、图片),而非结构化数据(后者应使用 IndexedDB)。
Cache Storage 与 HTTP 缓存的关键区别:
| 对比维度 | HTTP 缓存 | Cache Storage |
|---|---|---|
| 控制权归属 | 由服务器通过 Cache-Control / Expires 响应头控制。 |
完全由开发者通过代码管理,忽略 HTTP 缓存头。 |
| 缓存粒度 | 基于 URL 和响应头自动生效。 | 需显式调用 API 添加/匹配资源,支持自定义匹配逻辑(如:忽略查询参数)。 |
| 适用场景 | 适用于常规页面加载加速。 | 专为离线场景设计(如:PWA 的离线资源预加载)。 |
2.2 核心特性与限制
1)关键能力
持久化存储:缓存数据跨页面刷新和浏览器会话保留,除非开发者主动删除 。
独立于网络请求 :可通过 caches.match(request) 手动检查缓存,无需实际发起网络请求。
灵活匹配策略:支持自定义匹配逻辑(如:忽略 URL 查询参数),通过 Cache.match(request, options) 的 options 参数控制。
2)重要限制
仅支持 GET 请求 :Cache.add()/addAll()/put() 无法缓存 POST 等非安全请求。
HTTPS 强制要求:生产环境中必须通过 HTTPS 服务 (本地开发可使用 http://localhost),否则 API 调用会抛出 SecurityError。
存储空间受配额约束:浏览器对单域名缓存总量有上限(通常为设备空闲磁盘的 50%~60%),需通过 navigator.storage.estimate() 监控用量。
2.3 核心 API 与工作流程
基础操作方法有:
| 方法 | 作用 |
|---|---|
caches.open(cacheName) |
打开指定名称的缓存仓库,若不存在则自动创建。 |
cache.addAll(urls) |
批量缓存资源列表,任一失败则全部回滚。 |
cache.put(request, response) |
手动存入请求-响应对(常用于动态缓存)。 |
caches.match(request) |
跨所有缓存仓库匹配请求,返回首个匹配的响应。 |
cache.keys() |
获取当前缓存仓库中所有请求的列表。 |
典型工作流程(Service Worker 场景)
1)安装阶段预缓存资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then(cache =>
cache.addAll(['/index.html', '/style.css', '/app.js'])
)
);
});
// 关键点:通过 waitUntil 确保缓存完成前 Service Worker 不进入激活状态
2)拦截请求并返回缓存
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cachedResponse =>
cachedResponse || fetch(event.request) // 无缓存则走网络
)
);
});
// 扩展策略:可实现 cache-first(优先缓存)、network-first(优先网络)等混合策略
2.4 典型应用场景
1)PWA 离线资源管理
预加载核心资源 :在安装阶段缓存 Shell(HTML/CSS/JS),确保 首次加载后完全离线可用。
动态资源缓存:用户访问时缓存图片/数据,后续离线时仍可查看历史内容。
2)自定义缓存策略
忽略查询参数 :通过 new Request(url, { ignoreSearch: true }) 匹配忽略 ?v=1.2.3 的资源。
版本化缓存:按版本号命名缓存仓库(如:app-v2),旧版本缓存需手动清理,避免空间浪费。
3)网络优化
降级体验保障:网络请求失败时返回缓存内容 (stale-while-revalidate 模式)。
减少重复请求:对静态资源 (如:字体、第三方库)实现跨页面/会话的持久缓存。
2.5 注意事项和最佳实践
1)缓存管理规范
必须主动清理旧缓存:Service Worker 更新时,旧缓存不会自动删除,需在 activate 事件中清理:
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => !['v1','v2'].includes(k)).map(k => caches.delete(k)))
)
);
});
避免缓存污染:仅缓存明确需要离线支持的资源,避免无差别缓存导致配额耗尽。
2)安全与兼容性
HTTPS 强制约束:非安全上下文(HTTP)中,Chrome 等浏览器完全禁用 Cache API (本地开发除外)。
隐身模式行为:数据仅驻留在内存中,关闭窗口后自动清除,且配额通常 ≤120MB。
3)与 HTTP 缓存协同
双重缓存风险:若资源同时被 HTTP 缓存和 Cache Storage 存储,浏览器会优先使用 HTTP 缓存(因更接近网络层) 。
合理设置 HTTP 头:对 Cache Storage 中的资源,建议设置 Cache-Control: max-age=0,避免 HTTP 缓存干扰版本更新。
三、什么是 Storage buckets?
注意:截至目前,Storage Buckets API 仍处于 WICG 提案草案阶段,尚未在任何主流浏览器中实现。这意味着它目前无法在生产环境中直接调用,仅供了解其思路。
Storage Buckets(存储桶)是 Web 存储技术中一个极具前瞻性的全新存储管理概念。它的核心设计目标是允许开发者将不同类型的数据分组到不同的"存储桶"中,并为每个桶配置独立的配额、持久化策略和清理规则。
在传统的 Web 存储方式中,一个域名(Origin)下的所有数据(如:localStorage、IndexedDB、Cache API 等)通常混在一起,共享浏览器的总存储配额。这导致了两个主要痛点:配额竞争和清理策略一刀切 。Storage Buckets 通过将数据分类到独立的"桶"中,完美解决了上述问题。每个存储桶提供了精细化的控制能力:独立配额管理(Quota)、持久化策略(Persisted)、耐用性级别(Durability)。
然而,在深入了解其强大能力之前,必须明确一个关键的现状:截至目前,Storage Buckets API 仍处于 WICG 提案草案阶段,尚未在任何主流浏览器中实现。 这意味着它目前无法在生产环境中直接调用,现阶段无法依靠标准 Web API 实现模块级的配额硬隔离,浏览器也不提供按模块切分配额的粒度,但它是未来 Web 存储精细化管理的重要发展方向。
如果当前确实需要实现类似"模块数据隔离"的效果,建议采用以下替代策略:
模拟隔离:使用 IndexedDB 创建独立的数据库(如:module-a-db),并在键名中强制加上模块标识前缀,通过封装读写函数来避免跨模块误读。
物理隔离:将模块拆分为不同的子域名(如:auth.example.com 和 media.example.com),利用 origin 的天然隔离获得独立的存储配额。
服务端托管:由后端按模块制定存储配额策略(如:限制 Redis 内存),前端仅存储临时句柄或 token。
Storage Buckets 描绘了 Web 存储精细化管理的未来蓝图。虽然目前它还只是一个"名字很好听的幻影",但前端开发者可以提前了解其分层存储的设计思想,并在当前架构中通过命名空间封装等方式做好数据隔离的准备工作,以便在未来 API 落地时能够无缝迁移。
四、IndexedDB 和 Cache Storage 两者的对比
在构建现代 Web 应用(尤其是 PWA 和离线优先应用)时,IndexedDB 和 Cache Storage 是两种最核心的本地存储机制。虽然它们都支持大容量存储和异步操作,但其设计初衷和适用场景截然不同。
注:渐进式 Web 应用(PWA)是一种使用现代 Web 技术(HTML、CSS、JavaScript)构建,但能提供类似原生应用体验的 Web 应用程序。其架构设计的核心目标是实现跨平台兼容、离线可用、快速加载和原生级交互。
- 三个核心区别:
1)设计初衷与存储对象不同
IndexedDB 是浏览器内置的完整 NoSQL 数据库。它主要用于存储结构化的业务数据(如:JSON 对象、聊天记录、用户文档、配置信息等)。它支持复杂的数据模型、多字段索引以及事务(ACID),非常适合需要频繁增删改查(CRUD)和条件过滤的场景。
Cache Storage(缓存存储 API)是一个专门用于存储 HTTP 响应的映射表。它主要用于存储静态资源文件(如 HTML、CSS、JavaScript、图片等)。它通常与 Service Worker 配合使用,通过拦截 fetch 请求来实现离线加载和页面秒开。
2)数据查询与操作方式不同
IndexedDB 具备强大的查询能力。开发者可以创建索引,使用游标(Cursor)和键范围(Key Range)进行复杂的数据检索、排序和过滤。
Cache Storage 本质上是一个键值对存储(Key-Value Store),其"键"是 Request(请求),"值"是 Response(响应)。它没有索引功能,仅能通过精确的 URL 或自定义的 match 逻辑(如:忽略查询参数)来匹配资源。
3)底层存储引擎不同
IndexedDB 在用户磁盘上使用了真实的数据库引擎。例如,基于 Chromium 的浏览器(Chrome、Edge)使用 LevelDB(LSM 树),而 Firefox 和 Safari 使用 SQLite(B树),这保证了其在处理大型数据集时的高性能和持久性。
Cache Storage 则更偏向于文件系统的响应映射,专门针对网络请求的缓存与驱逐策略进行了优化。
| 核心特性对比维度 | IndexedDB | Cache Storage |
|---|---|---|
| 存储内容 | 结构化对象、JSON、二进制文件(Blob) | 完整的 HTTP 响应(Request/Response 对) |
| 主要用途 | 离线业务数据、复杂应用状态、用户消息 | 静态资源(JS/CSS/图片)、离线应用外壳(HTML) |
| 查询能力 | 强大(支持索引、键范围查询、游标遍历) | 较弱(仅能通过 URL 或自定义规则进行匹配) |
| API 类型 | 事务性、对象存储、异步回调/Promise | 基于 Promise 的请求/响应映射 |
| 存储容量 | 非常大(通常可达设备空闲磁盘的 50%~60%) | 非常大(受浏览器整体配额管理限制) |
| 典型缓存策略 | 数据级同步(如后台同步队列、增量保存) | 网络级拦截(缓存优先、网络优先、SWR等) |
在实际的 PWA 架构中,这两者通常不是"二选一"的关系,而是协同工作:
- Service Worker 作为协调器拦截请求。
- 当请求是 /styles.css 或 /app.js 时,Service Worker 从 Cache Storage 中返回资源,确保应用 UI 瞬间加载。
- 当请求是 /api/user-data 时,Service Worker 可能会从 IndexedDB 中读取上次缓存的业务数据返回给用户,同时在后台发起网络请求更新 IndexedDB 中的最新数据。
五、小小的总结
本文系统梳理了现代 Web 应用的三大核心存储支柱,旨在帮助开发者根据具体场景选择最合适的存储策略:
- IndexedDB(结构化数据库) :
- 定位 :浏览器内的事务型 NoSQL 数据库。
- 适用 :存储大量结构化数据(如用户文档、聊天记录、离线业务数据)。
- 特点 :容量大(可达 GB 级)、支持索引查询、事务安全、异步不阻塞 UI。
- Cache Storage(资源缓存) :
- 定位 :专用于缓存网络请求/响应对(Request/Response)。
- 适用 :实现 PWA 离线访问、静态资源(HTML/CSS/JS/图片)预加载、自定义网络策略。
- 特点 :由开发者代码完全控制、独立于 HTTP 缓存头、通常与 Service Worker 配合使用。
- Storage Buckets(存储分区 - 提案阶段) :
- 定位 :未来的存储配额与策略管理机制。
- 现状 :目前仍处于 WICG 提案草案阶段,主流浏览器尚未实现。
- 价值 :旨在解决配额竞争和清理策略"一刀切"的问题,允许按桶(Bucket)进行精细化管理。
最终建议 :
- 存数据用 IndexedDB :当需要存储复杂的业务对象且需要检索时。
- 存资源用 Cache API :当需要拦截网络请求、实现离线包或加速静态资源加载时。
- 关注未来 Buckets :虽然目前无法直接使用,但在架构设计上应提前考虑数据隔离的思想。