IndexedDB对象存储替代方案盘点
本文深入解析了IndexedDB在使用动态对象存储时遇到的挑战,着重指出`createObjectStore`方法仅能在数据库版本升级时调用,这与`localStorage`多分区异步存储的需求存在冲突。为解决这一问题,文章提出了一种高效的替代方案:避免频繁修改数据库模式,转而利用数据内部属性或键前缀在单一ObjectStore中实现逻辑分区。通过详细的代码示例,展示了如何创建一个类似`localStorage`的异步存储,既满足了多分区需求,又规避了频繁版本升级带来的复杂性和性能开销。该方案符合IndexedDB的设计理念,提供了一种更健壮、易于维护的客户端存储解决方案,尤其适用于需要灵活管理多个逻辑存储的场景。
IndexedDB中动态ObjectStore创建的挑战
IndexedDB是一个强大的客户端存储解决方案,但其设计哲学与传统的文件系统或键值存储有所不同。在IndexedDB中,IDBDatabase.createObjectStore() 方法用于创建新的对象存储(Object Store),但此方法并非在任何时候都可以调用。它有一个严格的限制:只能在 IDBOpenDBRequest 的 onupgradeneeded 回调函数中被调用。
onupgradeneeded 事件仅在以下两种情况下触发:
- 数据库首次创建时。
- 调用 indexedDB.open() 时,指定的新版本号大于当前数据库的版本号时(即进行版本升级)。
这意味着,如果你的数据库已经存在且版本号没有变化,你将无法在 onsuccess 回调或其他运行时上下文中动态地创建新的对象存储。尝试在 onsuccess 中调用 db.createObjectStore() 会导致运行时错误,因为此时数据库的事务模式不允许进行模式修改操作。
原始问题中尝试在 onsuccess 中检查 db.objectStoreNames.contains(storeKey) 并调用 createObjectStore 的做法,以及尝试通过“假装”升级版本(例如 db.bumpVersion(db.version + 1))来触发 onupgradeneeded,都是不可行的。IndexedDB 没有提供直接在 onsuccess 中触发版本升级的API。
为什么应避免频繁修改数据库模式
从数据库设计的角度来看,频繁地修改数据库模式(Schema)通常是不推荐的。数据库模式(例如,创建、删除或修改表/对象存储)应该是相对稳定的。onupgradeneeded 回调的目的是处理数据库版本升级时的模式迁移,例如添加新的对象存储、创建索引或删除旧的存储。将每次数据逻辑分区都映射为一个新的对象存储,会导致以下问题:
- 版本管理复杂性: 每次添加新的逻辑分区都需要递增数据库版本号,并编写相应的 onupgradeneeded 逻辑。随着逻辑分区的增多,版本号会快速增长,onupgradeneeded 中的逻辑会变得异常复杂且难以维护。
- 性能开销: 数据库版本升级通常是一个重量级操作,可能涉及数据迁移和重新索引,这会带来显著的性能开销,尤其是在启动时。
- 不符合设计原则: IndexedDB 的对象存储更像关系型数据库中的“表”,它们用于存储特定类型的数据集合。将“分区”的概念提升到模式级别,而不是数据级别,违背了这一设计原则。
推荐方案:通过数据内部属性实现逻辑分区
为了实现类似 localStorage 的多分区异步存储,同时避免频繁修改IndexedDB模式,最佳实践是在一个或少数几个固定的对象存储中,通过数据内部的属性或键前缀来区分不同的逻辑分区。
例如,你可以创建一个名为 data 的单一对象存储。当需要存储属于不同“逻辑存储”的数据时,可以在存储的数据对象中添加一个 storeName 属性,或者在键本身添加一个前缀。
以下是一个基于此思想的 LocalStorageAsync 实现示例:
class LocalStorageAsync { #database; #logicalStoreName; // 用于区分逻辑分区的名称 static #DB_NAME = 'LocalStorageAsyncDB'; // 数据库名称 static #PHYSICAL_OBJECT_STORE = 'key_value_data'; // 唯一的物理对象存储名称 /** * 构造函数,初始化IndexedDB连接 * @param {string} logicalStoreName 逻辑存储的名称,例如 'default', 'foo' */ constructor(logicalStoreName = 'default') { this.#logicalStoreName = logicalStoreName; // 使用固定的数据库版本号,只有当数据库首次创建或需要结构性升级时才改变 const openRequest = indexedDB.open(LocalStorageAsync.#DB_NAME, 1); this.#database = new Promise((resolve, reject) => { openRequest.onupgradeneeded = (event) => { const db = event.target.result; // 仅在onupgradeneeded中创建物理对象存储 if (!db.objectStoreNames.contains(LocalStorageAsync.#PHYSICAL_OBJECT_STORE)) { // 使用keyPath,这里我们将组合键作为主键 db.createObjectStore(LocalStorageAsync.#PHYSICAL_OBJECT_STORE, { keyPath: 'fullKey' }); // 如果需要按逻辑存储名称查询,可以添加索引 // db.createIndex('by_logicalStoreName', 'logicalStoreName', { unique: false }); } }; openRequest.onsuccess = (event) => { resolve(event.target.result); }; openRequest.onerror = (event) => { console.error("IndexedDB open error:", event.target.errorCode, event.target.error); reject(new Error("Failed to open IndexedDB.")); }; }); } /** * 获取一个事务和对象存储实例 * @param {IDBTransactionMode} mode 事务模式 ('readonly' 或 'readwrite') * @returns {Promise<IDBObjectStore>} */ async #getTransactionStore(mode) { const db = await this.#database; const transaction = db.transaction(LocalStorageAsync.#PHYSICAL_OBJECT_STORE, mode); transaction.onerror = (event) => { console.error("Transaction error:", event.target.errorCode, event.target.error); }; return transaction.objectStore(LocalStorageAsync.#PHYSICAL_OBJECT_STORE); } /** * 生成用于存储的唯一键 * @param {string} key 用户提供的键 * @returns {string} 包含逻辑存储名称前缀的完整键 */ #generateFullKey(key) { return `${this.#logicalStoreName}__${key}`; } /** * 从指定的逻辑存储中获取值 * @param {string} key 要获取的键 * @returns {Promise<string|null>} 对应的值,如果不存在则为null */ async getItem(key) { const store = await this.#getTransactionStore('readonly'); const fullKey = this.#generateFullKey(key); const request = store.get(fullKey); return new Promise((resolve, reject) => { request.onsuccess = (event) => { const result = event.target.result; resolve(result ? result.value : null); }; request.onerror = (event) => { console.error("Error getting item:", event.target.errorCode, event.target.error); reject(event.target.error); }; }); } /** * 将键值对存储到指定的逻辑存储中 * @param {string} key 要设置的键 * @param {string} value 要设置的值 * @returns {Promise<void>} */ async setItem(key, value) { const store = await this.#getTransactionStore('readwrite'); const fullKey = this.#generateFullKey(key); // 存储一个包含fullKey、原始值和逻辑存储名称的对象 const dataToStore = { fullKey: fullKey, value: value, logicalStoreName: this.#logicalStoreName // 存储逻辑存储名称,方便未来查询或索引 }; const request = store.put(dataToStore); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(); request.onerror = (event) => { console.error("Error setting item:", event.target.errorCode, event.target.error); reject(event.target.error); }; }); } /** * 从指定的逻辑存储中移除键值对 * @param {string} key 要移除的键 * @returns {Promise<void>} */ async removeItem(key) { const store = await this.#getTransactionStore('readwrite'); const fullKey = this.#generateFullKey(key); const request = store.delete(fullKey); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(); request.onerror = (event) => { console.error("Error removing item:", event.target.errorCode, event.target.error); reject(event.target.error); }; }); } /** * 清空指定逻辑存储中的所有数据 * 注意:这会遍历并删除,而不是直接清空整个物理存储 * @returns {Promise<void>} */ async clear() { const store = await this.#getTransactionStore('readwrite'); const request = store.openCursor(); const keysToDelete = []; return new Promise((resolve, reject) => { request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { // 检查是否属于当前逻辑存储 if (cursor.value.logicalStoreName === this.#logicalStoreName) { keysToDelete.push(cursor.primaryKey); } cursor.continue(); } else { // 游标遍历完毕,开始删除 if (keysToDelete.length === 0) { resolve(); return; } let deleteCount = 0; const deleteNext = () => { if (deleteCount < keysToDelete.length) { const deleteRequest = store.delete(keysToDelete[deleteCount]); deleteRequest.onsuccess = () => { deleteCount++; deleteNext(); }; deleteRequest.onerror = (event) => { console.error("Error deleting item during clear:", event.target.errorCode, event.target.error); reject(event.target.error); }; } else { resolve(); } }; deleteNext(); } }; request.onerror = (event) => { console.error("Error opening cursor for clear:", event.target.errorCode, event.target.error); reject(event.target.error); }; }); } } // 使用示例 async function runExample() { console.log("--- Initializing LocalStorageAsync instances ---"); const defaultStore = new LocalStorageAsync(); const fooStore = new LocalStorageAsync('foo'); const barStore = new LocalStorageAsync('bar'); console.log("\n--- Setting items ---"); await defaultStore.setItem('user', 'Alice'); await defaultStore.setItem('theme', 'dark'); await fooStore.setItem('user', 'Bob'); await fooStore.setItem('settings', 'enabled'); await barStore.setItem('user', 'Charlie'); console.log("\n--- Getting items ---"); console.log("Default store - user:", await defaultStore.getItem('user')); // Expected: Alice console.log("Foo store - user:", await fooStore.getItem('user')); // Expected: Bob console.log("Bar store - user:", await barStore.getItem('user')); // Expected: Charlie console.log("Default store - theme:", await defaultStore.getItem('theme')); // Expected: dark console.log("Foo store - settings:", await fooStore.getItem('settings')); // Expected: enabled console.log("Default store - nonExistent:", await defaultStore.getItem('nonExistent')); // Expected: null console.log("\n--- Removing item from fooStore ---"); await fooStore.removeItem('user'); console.log("Foo store - user after removal:", await fooStore.getItem('user')); // Expected: null console.log("\n--- Clearing defaultStore ---"); await defaultStore.clear(); console.log("Default store - user after clear:", await defaultStore.getItem('user')); // Expected: null console.log("Default store - theme after clear:", await defaultStore.getItem('theme')); // Expected: null console.log("Foo store - settings (should remain):", await fooStore.getItem('settings')); // Expected: enabled } runExample().catch(e => console.error("Example failed:", e));
注意事项与总结
- 单一物理对象存储: 上述示例使用了一个名为 key_value_data 的单一物理对象存储来存放所有数据。这是推荐的做法,因为它避免了频繁的模式修改。
- 键前缀(Key Prefixing): 通过在用户提供的键前加上逻辑存储名称(例如 default__myKey),可以确保不同逻辑存储中的同名键不会冲突。这是实现逻辑分区最直接有效的方式。
- 数据内部属性: 除了键前缀,你也可以在存储的数据对象中额外添加一个 logicalStoreName 属性。这样做的好处是,如果未来需要查询某个特定逻辑存储的所有数据(例如,清空某个逻辑存储),可以通过遍历游标并检查 logicalStoreName 属性来实现,或者为 logicalStoreName 属性创建索引以提高查询效率。
- 性能考虑: 对于非常大的数据集,如果频繁需要按 logicalStoreName 查询所有数据,创建索引 (db.createIndex('by_logicalStoreName', 'logicalStoreName', { unique: false });) 会显著提高查询性能。
- 版本号管理: 数据库的版本号(在 indexedDB.open() 的第二个参数中指定)应该保持稳定,只在真正需要修改数据库模式(例如,添加新的物理对象存储或索引)时才递增。对于逻辑分区,无需更改版本号。
通过采用这种数据层面的分区策略,你可以灵活地管理多个逻辑存储,而无需承担 IndexedDB 模式频繁变更带来的复杂性和性能开销。这种方法更加符合 IndexedDB 的设计理念,并提供了更健壮、可维护的解决方案。
理论要掌握,实操不能落!以上关于《IndexedDB对象存储替代方案盘点》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!

- 上一篇
- PHP实现OAuth2.0断言的实用技巧

- 下一篇
- JavaScript异步错误处理技巧详解
-
- 文章 · 前端 | 6小时前 | CSS 滚动条 用户体验 美化 Squarespace
- Squarespace隐藏滚动条技巧分享
- 305浏览 收藏
-
- 文章 · 前端 | 6小时前 |
- AntDesign时间组件使用技巧分享
- 491浏览 收藏
-
- 文章 · 前端 | 7小时前 |
- 动态元素事件绑定方法详解
- 307浏览 收藏
-
- 文章 · 前端 | 7小时前 |
- JavaScript动态排序列表教程
- 364浏览 收藏
-
- 文章 · 前端 | 7小时前 |
- RESTfulAPI设计与实现详解
- 425浏览 收藏
-
- 文章 · 前端 | 7小时前 |
- JavaScript处理数据库换行符方法
- 449浏览 收藏
-
- 文章 · 前端 | 8小时前 |
- HTML5Canvas绘图基础教程详解
- 210浏览 收藏
-
- 文章 · 前端 | 8小时前 |
- 多行文本模板字符串写法详解
- 432浏览 收藏
-
- 文章 · 前端 | 8小时前 |
- JSObject.defineProperty详解教程
- 397浏览 收藏
-
- 文章 · 前端 | 8小时前 |
- 阿尔比恩异教徒要塞位置及探索指南
- 364浏览 收藏
-
- 文章 · 前端 | 8小时前 |
- tel输入框支持多格式电话号码输入
- 112浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 514次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 499次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- AI Mermaid流程图
- SEO AI Mermaid 流程图工具:基于 Mermaid 语法,AI 辅助,自然语言生成流程图,提升可视化创作效率,适用于开发者、产品经理、教育工作者。
- 64次使用
-
- 搜获客【笔记生成器】
- 搜获客笔记生成器,国内首个聚焦小红书医美垂类的AI文案工具。1500万爆款文案库,行业专属算法,助您高效创作合规、引流的医美笔记,提升运营效率,引爆小红书流量!
- 32次使用
-
- iTerms
- iTerms是一款专业的一站式法律AI工作台,提供AI合同审查、AI合同起草及AI法律问答服务。通过智能问答、深度思考与联网检索,助您高效检索法律法规与司法判例,告别传统模板,实现合同一键起草与在线编辑,大幅提升法律事务处理效率。
- 70次使用
-
- 迅捷AIPPT
- 迅捷AIPPT是一款高效AI智能PPT生成软件,一键智能生成精美演示文稿。内置海量专业模板、多样风格,支持自定义大纲,助您轻松制作高质量PPT,大幅节省时间。
- 56次使用
-
- 迅捷AI写作
- 迅捷AI写作,您的智能AI写作助手!快速生成各类文稿,涵盖新媒体、工作汇报。更兼具文字识别、语音转换、格式转换等实用功能,一站式解决文本处理难题,显著提升工作效率。
- 41次使用
-
- 优化用户界面体验的秘密武器:CSS开发项目经验大揭秘
- 2023-11-03 501浏览
-
- 使用微信小程序实现图片轮播特效
- 2023-11-21 501浏览
-
- 解析sessionStorage的存储能力与限制
- 2024-01-11 501浏览
-
- 探索冒泡活动对于团队合作的推动力
- 2024-01-13 501浏览
-
- UI设计中为何选择绝对定位的智慧之道
- 2024-02-03 501浏览