目录

第三部分:对象存储空间(Object Store)详解

3.1 对象存储空间概述

对象存储空间(Object Store)是 IndexedDB 中最核心的概念,它是实际存储数据的地方。理解对象存储空间的设计和使用方式,是掌握 IndexedDB 的关键。

3.1.1 什么是对象存储空间

对象存储空间可以类比为关系型数据库中的“表”,但有几个重要区别:

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'
});

优点:

缺点:

策略二:使用自增键(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);
};

注意事项:

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 使用结构化克隆算法序列化数据,支持以下类型:

3.5.2 不支持的类型

以下类型不能直接存储:

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 本章小结

本章深入介绍了对象存储空间:

继续阅读 第四部分:索引(Index)详解