====== 第三部分:对象存储空间(Object Store)详解 ====== ===== 3.1 对象存储空间概述 ===== 对象存储空间(Object Store)是 IndexedDB 中最核心的概念,它是实际存储数据的地方。理解对象存储空间的设计和使用方式,是掌握 IndexedDB 的关键。 ===== 3.1.1 什么是对象存储空间 ===== 对象存储空间可以类比为关系型数据库中的"表",但有几个重要区别: * **无模式(Schema-less)**:同一对象存储空间中的对象不需要有相同的结构 * **对象导向**:存储的是 JavaScript 对象,而非行记录 * **主键必需**:每个对象必须有一个唯一的主键 * **索引可选**:可以创建索引加速查询,但不是必须的 ===== 3.1.2 对象存储空间的特性 ===== ^ 特性 ^ 说明 ^ | 存储类型 | 结构化克隆算法支持的任何 JavaScript 对象 | | 主键要求 | 每个对象必须有唯一主键 | | 数据类型 | 对象、数组、日期、Blob、ArrayBuffer 等 | | 容量限制 | 受浏览器存储配额限制 | | 并发访问 | 通过事务控制,同一时间只有一个写入事务 | ===== 3.2 创建对象存储空间 ===== ===== 3.2.1 createObjectStore 方法 ===== // 基本语法 const store = db.createObjectStore(storeName, options); // options 参数 { keyPath: string | string[], // 指定主键属性路径 autoIncrement: boolean // 是否自动生成主键 } ===== 3.2.2 主键设计策略 ===== **策略一:使用内联键(keyPath)** // 单一属性作为主键 db.createObjectStore('users', { keyPath: 'userId' }); // 使用对象时 store.add({ userId: 'U001', name: '张三', email: 'zhangsan@example.com' }); 优点: * 主键与数据紧密关联 * 查询直观,知道主键就知道数据位置 缺点: * 需要应用层生成唯一 ID * 不能更改主键值 **策略二:使用自增键(autoIncrement)** // 自动生成递增整数 db.createObjectStore('logs', { autoIncrement: true }); // 插入数据时不需要指定主键 store.add({ message: '用户登录', timestamp: new Date(), level: 'info' }); // 自动分配主键 1, 2, 3... 优点: * 无需应用层管理主键 * 保证唯一性和顺序性 缺点: * 主键是数字,不携带业务信息 * 删除数据后主键不会复用 **策略三:组合使用 keyPath 和 autoIncrement** // 自动生成主键并存储在对象的 id 属性中 db.createObjectStore('items', { keyPath: 'id', autoIncrement: true }); // 插入后对象会自动包含 id 属性 const request = store.add({ name: '商品A', price: 100 }); request.onsuccess = () => { console.log('分配的主键:', request.result); // 1 }; **策略四:复合主键** // 使用多个属性组成复合主键 db.createObjectStore('memberships', { keyPath: ['userId', 'projectId'] }); // 插入数据 store.add({ userId: 'U001', projectId: 'P100', role: 'admin', joinedAt: new Date() }); // 查询时使用数组作为键 store.get(['U001', 'P100']); ===== 3.3 对象存储空间配置详解 ===== ===== 3.3.1 创建时的索引配置 ===== request.onupgradeneeded = function(event) { const db = event.target.result; const store = db.createObjectStore('products', { keyPath: 'sku' }); // 普通索引 store.createIndex('categoryIndex', 'category', { unique: false }); // 唯一索引 store.createIndex('barcodeIndex', 'barcode', { unique: true }); // 多入口索引(用于数组属性) store.createIndex('tagIndex', 'tags', { multiEntry: true }); // 复合索引 store.createIndex('priceCategoryIndex', ['price', 'category']); }; ===== 3.3.2 对象存储空间属性 ===== const store = transaction.objectStore('users'); // 获取对象存储空间名称 console.log(store.name); // 'users' // 获取主键路径 console.log(store.keyPath); // 'id' 或 null // 检查是否使用自增主键 console.log(store.autoIncrement); // true 或 false // 获取索引列表 console.log(store.indexNames); // DOMStringList ['nameIndex', 'emailIndex'] ===== 3.4 数据操作基础 ===== ===== 3.4.1 添加数据(add) ===== const request = store.add(value, key); // 示例 const addRequest = store.add({ id: 'user001', name: '张三', email: 'zhangsan@example.com', profile: { age: 25, city: '北京' } }); addRequest.onsuccess = function() { console.log('添加成功,主键:', addRequest.result); }; addRequest.onerror = function() { // 如果主键已存在,会触发错误 console.error('添加失败:', addRequest.error); }; 注意事项: * 如果主键已存在,add() 会失败(触发 ConstraintError) * 如果不指定 key 参数,使用 keyPath 或 autoIncrement * 数据通过结构化克隆算法存储,函数和某些 DOM 对象不能被存储 ===== 3.4.2 更新/插入数据(put) ===== // put 会覆盖已存在的记录,或插入新记录 const request = store.put(value, key); // 更新现有记录 store.put({ id: 'user001', // 相同主键 name: '张三(已更新)', email: 'newemail@example.com' }); // 插入新记录(如果不存在) store.put({ id: 'user002', name: '李四', email: 'lisi@example.com' }); add() vs put() 对比: ^ 方法 ^ 主键已存在 ^ 主键不存在 ^ | add() | 错误 (ConstraintError) | 插入新记录 | | put() | 覆盖现有记录 | 插入新记录 | ===== 3.4.3 删除数据(delete) ===== // 删除指定主键的记录 const deleteRequest = store.delete(key); // 示例 store.delete('user001'); // 也可以使用 IDBKeyRange 删除范围 store.delete(IDBKeyRange.bound('A', 'M')); ===== 3.4.4 清空对象存储空间(clear) ===== // 删除所有记录 const clearRequest = store.clear(); clearRequest.onsuccess = function() { console.log('对象存储空间已清空'); }; ===== 3.4.5 获取数据(get) ===== // 通过主键获取 const getRequest = store.get(key); getRequest.onsuccess = function() { if (getRequest.result) { console.log('找到数据:', getRequest.result); } else { console.log('数据不存在'); } }; // 获取所有记录(配合游标,见第七部分) // 获取记录数量 const countRequest = store.count(); countRequest.onsuccess = () => { console.log('记录数:', countRequest.result); }; ===== 3.5 数据类型支持 ===== ===== 3.5.1 结构化克隆算法 ===== IndexedDB 使用结构化克隆算法序列化数据,支持以下类型: * **基本类型**:Boolean, Number, String, null, undefined * **日期**:Date 对象 * **正则表达式**:RegExp(但不支持标志) * **二进制数据**:Blob, File, FileList, ArrayBuffer, TypedArray, DataView * **图像数据**:ImageData * **集合**:Array, Object, Map, Set * **特殊对象**:Error(部分属性可能丢失) ===== 3.5.2 不支持的类型 ===== 以下类型**不能**直接存储: * **Function**:函数对象 * **DOM 对象**:大多数 DOM 节点 * **某些内置对象**:如 WeakMap, WeakSet * **循环引用**:对象间存在循环引用时会抛出 DataCloneError ===== 3.5.3 处理不支持的数据 ===== // 方案一:将函数转换为字符串 const obj = { name: 'test', // 存储前转换 fn: myFunction.toString() }; // 方案二:使用 JSON 序列化(丢失类型信息) const serialized = JSON.stringify(obj); // 存储字符串 store.add({ id: 1, data: serialized }); // 方案三:自定义序列化 function serialize(obj) { return JSON.stringify(obj, (key, value) => { if (value instanceof Date) { return { __type: 'Date', value: value.toISOString() }; } return value; }); } function deserialize(str) { return JSON.parse(str, (key, value) => { if (value && value.__type === 'Date') { return new Date(value.value); } return value; }); } ===== 3.6 对象存储空间设计模式 ===== ===== 3.6.1 单类型存储模式 ===== 每个对象存储空间只存储一种类型的数据: // 用户存储 const userStore = db.createObjectStore('users', { keyPath: 'id' }); // 订单存储 const orderStore = db.createObjectStore('orders', { keyPath: 'orderId' }); // 产品存储 const productStore = db.createObjectStore('products', { keyPath: 'sku' }); 优点: * 结构清晰,易于维护 * 索引针对特定类型优化 * 事务粒度合理 ===== 3.6.2 多态存储模式 ===== 在同一个对象存储空间中存储不同类型,通过 type 字段区分: const store = db.createObjectStore('items', { keyPath: 'id' }); store.createIndex('typeIndex', 'type', { unique: false }); // 存储用户 store.add({ id: 'u1', type: 'user', name: '张三' }); // 存储产品 store.add({ id: 'p1', type: 'product', title: 'iPhone' }); // 存储文章 store.add({ id: 'a1', type: 'article', title: 'IndexedDB 教程' }); 优点: * 可以统一查询所有类型 * 适合日志等类型相似的数据 缺点: * 索引设计复杂 * 类型检查需要在应用层处理 ===== 3.6.3 关系存储模式 ===== 模拟关系型数据库的外键关系: // 用户表 const userStore = db.createObjectStore('users', { keyPath: 'userId' }); // 订单表,包含 userId 作为外键 const orderStore = db.createObjectStore('orders', { keyPath: 'orderId' }); orderStore.createIndex('userIdIndex', 'userId', { unique: false }); // 订单项表 const itemStore = db.createObjectStore('orderItems', { keyPath: 'itemId' }); itemStore.createIndex('orderIdIndex', 'orderId', { unique: false }); ===== 3.7 本章小结 ===== 本章深入介绍了对象存储空间: * 对象存储空间是 IndexedDB 的核心数据容器 * 主键设计有内联键、自增键、复合键等多种策略 * 基本操作包括 add、put、delete、clear、get * 使用结构化克隆算法存储数据,注意不支持函数等类型 * 设计模式包括单类型、多态和关系存储 继续阅读 [[part04-index|第四部分:索引(Index)详解]]。