====== 第三部分:对象存储空间(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)详解]]。