前端 IndexedDB 完全指南

目录

  1. [IndexedDB 简介](#IndexedDB 简介)
  2. 核心概念与API概览
  3. 环境准备与兼容性
  4. 基础操作:打开数据库
  5. [对象仓库(Object Store)操作](#对象仓库(Object Store)操作)
  6. [CRUD 完整示例](#CRUD 完整示例)
  7. 索引与查询
  8. 游标与分页
  9. 事务管理
  10. 实战案例:离线笔记应用
  11. 性能优化与最佳实践
  12. 常见问题与调试技巧

1. IndexedDB 简介

1.1 什么是 IndexedDB?

IndexedDB 是浏览器内置的非关系型数据库(NoSQL),用于在客户端存储大量结构化数据。它提供以下特性:

  • 容量大:通常可达 250MB 以上,远超 localStorage(5-10MB)
  • 异步操作:不阻塞主线程,基于事件或 Promise
  • 支持索引:可以建立索引实现高效查询
  • 事务支持:保证数据一致性(ACID 特性)
  • 存储类型丰富:可以存储任意 JavaScript 对象、二进制数据(Blob、ArrayBuffer)

1.2 适用场景

场景 说明
离线应用(PWA) 缓存 API 数据,实现离线访问
大数据量存储 超过 localStorage 容量限制的场景
富客户端应用 在线文档编辑器、邮件客户端等
缓存层 作为远程数据的本地缓存,提升加载速度
用户生成内容 本地草稿、用户配置等

2. 核心概念与API概览

2.1 核心概念

复制代码
数据库(Database)
  └── 对象仓库(Object Store)  // 类似关系型数据库的"表"
        └── 索引(Index)       // 加速查询
              └── 记录(Record) // 键值对数据
概念 说明
数据库 每个域名可创建多个数据库,每个数据库有独立的版本号
对象仓库 存储数据的容器,类似"表"。每个对象仓库有一个主键(key)
索引 基于对象属性的查询加速器,类似 SQL 的索引
事务 对数据库的一组操作,保证原子性
游标 遍历数据的指针,用于范围查询和分页

2.2 主要 API

javascript 复制代码
// 核心对象
window.indexedDB           // 全局入口
IDBRequest                 // 所有异步操作返回的请求对象
IDBDatabase                // 数据库实例
IDBObjectStore             // 对象仓库
IDBIndex                   // 索引
IDBCursor                  // 游标
IDBTransaction             // 事务

3. 环境准备与兼容性

3.1 浏览器兼容性

IndexedDB 支持所有现代浏览器:

  • Chrome/Edge 24+
  • Firefox 16+
  • Safari 10+(iOS 10+)
  • Opera 15+

3.2 检测兼容性

javascript 复制代码
function checkIndexedDBSupport() {
  if (!window.indexedDB) {
    console.error('当前浏览器不支持 IndexedDB');
    return false;
  }
  return true;
}

// 使用示例
if (checkIndexedDBSupport()) {
  console.log('IndexedDB 可用');
}

3.3 辅助工具

  • Chrome DevTools:Application → IndexedDB,可查看和编辑数据
  • Firefox DevTools:Storage → IndexedDB
  • 封装库推荐
    • idb(轻量级 Promise 封装)
    • Dexie.js(功能完善的封装)

本教程使用原生 API,便于理解底层原理。


4. 基础操作:打开数据库

4.1 打开/创建数据库

javascript 复制代码
// 打开数据库(如果不存在则创建)
// 参数:数据库名称,版本号
const request = indexedDB.open('MyDatabase', 1);

// 成功回调
request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('数据库打开成功', db);
};

// 失败回调
request.onerror = (event) => {
  console.error('数据库打开失败', event.target.error);
};

// 版本更新回调(首次创建或版本号升级时触发)
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const oldVersion = event.oldVersion;
  const newVersion = event.newVersion;
  
  console.log(`数据库版本升级: ${oldVersion} → ${newVersion}`);
  
  // 在此处创建对象仓库和索引
};

4.2 数据库版本管理

IndexedDB 使用版本号来管理数据库结构变化(创建/删除对象仓库、索引等)。

javascript 复制代码
// 版本升级时创建对象仓库
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // 检查对象仓库是否已存在,避免重复创建
  if (!db.objectStoreNames.contains('users')) {
    // 创建对象仓库,指定主键
    const store = db.createObjectStore('users', { 
      keyPath: 'id',           // 使用对象的 id 属性作为主键
      autoIncrement: false     // 是否自动递增(设为 true 时自动生成主键)
    });
    console.log('users 表创建成功');
  }
  
  // 如果需要删除对象仓库(谨慎使用)
  // if (db.objectStoreNames.contains('oldStore')) {
  //   db.deleteObjectStore('oldStore');
  // }
};

4.3 封装数据库连接

javascript 复制代码
class DatabaseManager {
  constructor(dbName, version = 1) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
  }
  
  async connect() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };
      request.onupgradeneeded = (event) => {
        this.db = event.target.result;
        this.onUpgrade(this.db);
        resolve(this.db);
      };
    });
  }
  
  onUpgrade(db) {
    // 子类可覆盖此方法
    console.log('数据库升级回调');
  }
  
  getDB() {
    if (!this.db) {
      throw new Error('请先调用 connect() 方法');
    }
    return this.db;
  }
}

5. 对象仓库(Object Store)操作

5.1 主键的定义方式

javascript 复制代码
// 方式1:使用 keyPath(对象属性作为主键)
const store1 = db.createObjectStore('users', { keyPath: 'id' });
// 添加数据:需要对象包含 id 属性

// 方式2:使用 autoIncrement(自动生成递增主键)
const store2 = db.createObjectStore('logs', { autoIncrement: true });
// 添加数据时自动生成主键 1, 2, 3...

// 方式3:不指定 keyPath,手动设置主键
const store3 = db.createObjectStore('config');
// 添加时通过 add(data, key) 的第二个参数指定主键

5.2 创建带索引的对象仓库

javascript 复制代码
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // 创建 users 表
  const userStore = db.createObjectStore('users', { keyPath: 'id' });
  
  // 创建索引(加速查询)
  // 参数:索引名称,索引属性,配置项
  userStore.createIndex('email', 'email', { unique: true });
  userStore.createIndex('age', 'age', { unique: false });
  userStore.createIndex('name_age', ['name', 'age']); // 复合索引
  
  // 创建 posts 表
  const postStore = db.createObjectStore('posts', { keyPath: 'id', autoIncrement: true });
  postStore.createIndex('userId', 'userId', { unique: false });
  postStore.createIndex('createdAt', 'createdAt', { unique: false });
};

5.3 索引的配置选项

选项 类型 说明
unique boolean 索引值是否唯一(默认 false)
multiEntry boolean 当属性为数组时,是否对数组每个元素建立索引(默认 false)

6. CRUD 完整示例

6.1 添加数据(Create)

javascript 复制代码
// 添加单条数据
function addUser(db, user) {
  return new Promise((resolve, reject) => {
    // 创建事务(指定对象仓库和操作模式)
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    
    // 添加数据
    const request = store.add(user);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
    
    // 事务完成回调
    transaction.oncomplete = () => console.log('事务完成');
    transaction.onerror = () => reject(transaction.error);
  });
}

// 使用示例
const newUser = {
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  age: 25,
  createdAt: new Date()
};

addUser(db, newUser).then(() => {
  console.log('用户添加成功');
}).catch(err => {
  console.error('添加失败', err);
});

6.2 读取数据(Read)

javascript 复制代码
// 根据主键读取单条数据
function getUserById(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.get(id);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 使用示例
getUserById(db, 1).then(user => {
  if (user) {
    console.log('找到用户:', user);
  } else {
    console.log('用户不存在');
  }
});

6.3 更新数据(Update)

javascript 复制代码
// 方式1:使用 put(存在则更新,不存在则新增)
function updateUser(db, user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.put(user);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 方式2:先读取再修改(适用于部分更新)
async function partialUpdateUser(db, id, updates) {
  const user = await getUserById(db, id);
  if (!user) throw new Error('用户不存在');
  
  const updatedUser = { ...user, ...updates };
  return updateUser(db, updatedUser);
}

// 使用示例
await partialUpdateUser(db, 1, { age: 26, name: '张三丰' });

6.4 删除数据(Delete)

javascript 复制代码
// 根据主键删除
function deleteUserById(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.delete(id);
    
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

// 清空整个对象仓库
function clearStore(db, storeName) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);
    const request = store.clear();
    
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

6.5 完整 CRUD 封装

javascript 复制代码
class IndexedDBHelper {
  constructor(dbName, version, upgradeCallback) {
    this.dbName = dbName;
    this.version = version;
    this.upgradeCallback = upgradeCallback;
    this.db = null;
  }
  
  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };
      request.onupgradeneeded = (event) => {
        this.db = event.target.result;
        if (this.upgradeCallback) {
          this.upgradeCallback(this.db);
        }
        resolve(this.db);
      };
    });
  }
  
  async add(storeName, data) {
    return this._execute(storeName, 'readwrite', (store) => store.add(data));
  }
  
  async get(storeName, key) {
    return this._execute(storeName, 'readonly', (store) => store.get(key));
  }
  
  async put(storeName, data) {
    return this._execute(storeName, 'readwrite', (store) => store.put(data));
  }
  
  async delete(storeName, key) {
    return this._execute(storeName, 'readwrite', (store) => store.delete(key));
  }
  
  async getAll(storeName) {
    return this._execute(storeName, 'readonly', (store) => store.getAll());
  }
  
  _execute(storeName, mode, callback) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], mode);
      const store = transaction.objectStore(storeName);
      const request = callback(store);
      
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
      transaction.onerror = () => reject(transaction.error);
    });
  }
}

7. 索引与查询

7.1 通过索引查询单条数据

javascript 复制代码
// 通过 email 索引查询用户
function getUserByEmail(db, email) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('email');
    const request = index.get(email);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 使用示例
const user = await getUserByEmail(db, 'zhangsan@example.com');

7.2 通过索引查询多条数据

javascript 复制代码
// 查询所有年龄为 25 的用户
function getUsersByAge(db, age) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('age');
    const request = index.getAll(age);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

7.3 范围查询(使用 IDBKeyRange)

javascript 复制代码
// IDBKeyRange 常用方法
// 范围查询:age >= 18
const range1 = IDBKeyRange.lowerBound(18);

// 范围查询:age < 30
const range2 = IDBKeyRange.upperBound(30, true); // true 表示不包含上界

// 范围查询:age >= 18 且 age <= 30
const range3 = IDBKeyRange.bound(18, 30);

// 范围查询:age = 25
const range4 = IDBKeyRange.only(25);

// 使用示例:查询年龄在 20-30 之间的用户
function getUsersByAgeRange(db, minAge, maxAge) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('age');
    const range = IDBKeyRange.bound(minAge, maxAge);
    const request = index.getAll(range);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

7.4 复合索引查询

javascript 复制代码
// 创建复合索引(name + age)
// 在 onupgradeneeded 中创建
userStore.createIndex('name_age', ['name', 'age']);

// 查询 name = '张三' 且 age = 25 的用户
function getUserByNameAndAge(db, name, age) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('name_age');
    const request = index.get([name, age]);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

8. 游标与分页

8.1 使用游标遍历数据

javascript 复制代码
// 遍历所有用户
function getAllUsersWithCursor(db) {
  return new Promise((resolve, reject) => {
    const users = [];
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const cursorRequest = store.openCursor();
    
    cursorRequest.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        users.push(cursor.value);
        cursor.continue(); // 移动到下一条
      } else {
        resolve(users); // 遍历完成
      }
    };
    
    cursorRequest.onerror = () => reject(cursorRequest.error);
  });
}

8.2 带条件的游标查询

javascript 复制代码
// 查询年龄 >= 18 的用户,并限制返回数量
function getUsersWithCondition(db, minAge, limit = 10) {
  return new Promise((resolve, reject) => {
    const users = [];
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('age');
    const range = IDBKeyRange.lowerBound(minAge);
    const cursorRequest = index.openCursor(range);
    
    cursorRequest.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor && users.length < limit) {
        users.push(cursor.value);
        cursor.continue();
      } else {
        resolve(users);
      }
    };
    
    cursorRequest.onerror = () => reject(cursorRequest.error);
  });
}

8.3 分页实现

javascript 复制代码
// 分页查询
async function getUsersPaginated(db, pageSize = 10, pageIndex = 0) {
  return new Promise((resolve, reject) => {
    const users = [];
    let skipCount = pageSize * pageIndex;
    let currentIndex = 0;
    
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const cursorRequest = store.openCursor();
    
    cursorRequest.onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (!cursor) {
        resolve(users);
        return;
      }
      
      if (currentIndex >= skipCount && users.length < pageSize) {
        users.push(cursor.value);
      }
      
      currentIndex++;
      
      if (users.length < pageSize) {
        cursor.continue();
      } else {
        resolve(users);
      }
    };
    
    cursorRequest.onerror = () => reject(cursorRequest.error);
  });
}

// 使用示例
const page1 = await getUsersPaginated(db, 10, 0); // 第一页
const page2 = await getUsersPaginated(db, 10, 1); // 第二页

8.4 反向遍历(倒序)

javascript 复制代码
// 按创建时间倒序获取最新数据
function getLatestPosts(db, limit = 10) {
  return new Promise((resolve, reject) => {
    const posts = [];
    const transaction = db.transaction(['posts'], 'readonly');
    const store = transaction.objectStore('posts');
    const index = store.index('createdAt');
    
    // 打开反向游标
    const cursorRequest = index.openCursor(null, 'prev');
    
    cursorRequest.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor && posts.length < limit) {
        posts.push(cursor.value);
        cursor.continue();
      } else {
        resolve(posts);
      }
    };
    
    cursorRequest.onerror = () => reject(cursorRequest.error);
  });
}

9. 事务管理

9.1 事务的类型

模式 说明
readonly 只读,可并发执行,性能较好
readwrite 读写,独占操作,保证数据一致性

9.2 多对象仓库事务

javascript 复制代码
// 在一个事务中操作多个对象仓库
async function transferPost(db, userId, postId, newUserId) {
  return new Promise((resolve, reject) => {
    // 事务包含两个对象仓库
    const transaction = db.transaction(['users', 'posts'], 'readwrite');
    
    const userStore = transaction.objectStore('users');
    const postStore = transaction.objectStore('posts');
    
    let post = null;
    let newUser = null;
    
    // 步骤1:获取帖子
    const getPostRequest = postStore.get(postId);
    getPostRequest.onsuccess = () => {
      post = getPostRequest.result;
      if (!post) {
        transaction.abort();
        reject(new Error('帖子不存在'));
        return;
      }
      
      // 步骤2:获取目标用户
      const getUserRequest = userStore.get(newUserId);
      getUserRequest.onsuccess = () => {
        newUser = getUserRequest.result;
        if (!newUser) {
          transaction.abort();
          reject(new Error('目标用户不存在'));
          return;
        }
        
        // 步骤3:更新帖子所属用户
        post.userId = newUserId;
        const updateRequest = postStore.put(post);
        updateRequest.onsuccess = () => {
          console.log('帖子转移成功');
        };
      };
    };
    
    transaction.oncomplete = () => resolve({ post, newUser });
    transaction.onerror = () => reject(transaction.error);
    transaction.onabort = () => reject(new Error('事务已中止'));
  });
}

9.3 事务的生命周期

javascript 复制代码
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');

// 事务在以下情况自动提交:
// 1. 所有请求完成且没有新请求加入
// 2. 事件循环返回

// 手动中止事务
transaction.abort();

// 监听事务状态
transaction.oncomplete = () => console.log('事务提交成功');
transaction.onerror = () => console.log('事务失败');
transaction.onabort = () => console.log('事务被中止');

10. 实战案例:离线笔记应用

10.1 数据库设计

javascript 复制代码
// 数据库初始化
class NoteDatabase {
  constructor() {
    this.dbName = 'NoteApp';
    this.version = 1;
    this.db = null;
  }
  
  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        
        // 创建 notes 表
        if (!db.objectStoreNames.contains('notes')) {
          const noteStore = db.createObjectStore('notes', { 
            keyPath: 'id', 
            autoIncrement: true 
          });
          
          // 创建索引
          noteStore.createIndex('title', 'title');
          noteStore.createIndex('category', 'category');
          noteStore.createIndex('updatedAt', 'updatedAt');
          noteStore.createIndex('isDeleted', 'isDeleted');
          noteStore.createIndex('category_updated', ['category', 'updatedAt']);
        }
        
        // 创建 categories 表
        if (!db.objectStoreNames.contains('categories')) {
          const categoryStore = db.createObjectStore('categories', { 
            keyPath: 'id', 
            autoIncrement: true 
          });
          categoryStore.createIndex('name', 'name', { unique: true });
        }
        
        // 初始化默认分类
        const categoryStore = transaction.objectStore('categories');
        const defaultCategories = ['工作', '学习', '生活', '灵感'];
        defaultCategories.forEach(name => {
          categoryStore.add({ name });
        });
      };
    });
  }
}

10.2 笔记 CRUD 操作

javascript 复制代码
class NoteService {
  constructor(db) {
    this.db = db;
  }
  
  // 创建笔记
  async createNote(note) {
    const transaction = this.db.transaction(['notes'], 'readwrite');
    const store = transaction.objectStore('notes');
    
    const newNote = {
      ...note,
      createdAt: new Date(),
      updatedAt: new Date(),
      isDeleted: false
    };
    
    return new Promise((resolve, reject) => {
      const request = store.add(newNote);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  // 获取所有笔记(排除已删除)
  async getAllNotes() {
    const transaction = this.db.transaction(['notes'], 'readonly');
    const store = transaction.objectStore('notes');
    const index = store.index('isDeleted');
    const range = IDBKeyRange.only(false);
    
    return new Promise((resolve, reject) => {
      const request = index.getAll(range);
      request.onsuccess = () => {
        // 按更新时间倒序
        const notes = request.result.sort((a, b) => 
          new Date(b.updatedAt) - new Date(a.updatedAt)
        );
        resolve(notes);
      };
      request.onerror = () => reject(request.error);
    });
  }
  
  // 根据分类获取笔记
  async getNotesByCategory(category) {
    const transaction = this.db.transaction(['notes'], 'readonly');
    const store = transaction.objectStore('notes');
    const index = store.index('category_updated');
    const range = IDBKeyRange.bound([category, new Date(0)], [category, new Date()]);
    
    return new Promise((resolve, reject) => {
      const request = index.getAll(range);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  // 搜索笔记(标题或内容)
  async searchNotes(keyword) {
    const allNotes = await this.getAllNotes();
    const lowerKeyword = keyword.toLowerCase();
    
    return allNotes.filter(note => 
      note.title?.toLowerCase().includes(lowerKeyword) ||
      note.content?.toLowerCase().includes(lowerKeyword)
    );
  }
  
  // 更新笔记
  async updateNote(id, updates) {
    const transaction = this.db.transaction(['notes'], 'readwrite');
    const store = transaction.objectStore('notes');
    
    return new Promise(async (resolve, reject) => {
      // 先获取原笔记
      const getRequest = store.get(id);
      getRequest.onsuccess = () => {
        const oldNote = getRequest.result;
        if (!oldNote) {
          reject(new Error('笔记不存在'));
          return;
        }
        
        const updatedNote = {
          ...oldNote,
          ...updates,
          updatedAt: new Date()
        };
        
        const putRequest = store.put(updatedNote);
        putRequest.onsuccess = () => resolve(updatedNote);
        putRequest.onerror = () => reject(putRequest.error);
      };
      getRequest.onerror = () => reject(getRequest.error);
    });
  }
  
  // 软删除笔记
  async deleteNote(id) {
    return this.updateNote(id, { isDeleted: true });
  }
  
  // 永久删除笔记
  async permanentlyDeleteNote(id) {
    const transaction = this.db.transaction(['notes'], 'readwrite');
    const store = transaction.objectStore('notes');
    
    return new Promise((resolve, reject) => {
      const request = store.delete(id);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
  
  // 获取回收站笔记
  async getDeletedNotes() {
    const transaction = this.db.transaction(['notes'], 'readonly');
    const store = transaction.objectStore('notes');
    const index = store.index('isDeleted');
    const range = IDBKeyRange.only(true);
    
    return new Promise((resolve, reject) => {
      const request = index.getAll(range);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  // 清空回收站
  async clearTrash() {
    const deletedNotes = await this.getDeletedNotes();
    const promises = deletedNotes.map(note => this.permanentlyDeleteNote(note.id));
    await Promise.all(promises);
  }
}

10.3 分类管理

javascript 复制代码
class CategoryService {
  constructor(db) {
    this.db = db;
  }
  
  // 获取所有分类
  async getAllCategories() {
    const transaction = this.db.transaction(['categories'], 'readonly');
    const store = transaction.objectStore('categories');
    
    return new Promise((resolve, reject) => {
      const request = store.getAll();
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  // 添加分类
  async addCategory(name) {
    const transaction = this.db.transaction(['categories'], 'readwrite');
    const store = transaction.objectStore('categories');
    
    return new Promise((resolve, reject) => {
      const request = store.add({ name });
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  // 删除分类(同时将相关笔记移至默认分类)
  async deleteCategory(id) {
    const transaction = this.db.transaction(['categories', 'notes'], 'readwrite');
    const categoryStore = transaction.objectStore('categories');
    const noteStore = transaction.objectStore('notes');
    
    return new Promise(async (resolve, reject) => {
      // 获取要删除的分类名称
      const getRequest = categoryStore.get(id);
      getRequest.onsuccess = async () => {
        const category = getRequest.result;
        if (!category) {
          reject(new Error('分类不存在'));
          return;
        }
        
        // 将该分类下的笔记移到默认分类
        const notesIndex = noteStore.index('category');
        const range = IDBKeyRange.only(category.name);
        const cursorRequest = notesIndex.openCursor(range);
        
        cursorRequest.onsuccess = (event) => {
          const cursor = event.target.result;
          if (cursor) {
            const note = cursor.value;
            note.category = '未分类';
            noteStore.put(note);
            cursor.continue();
          } else {
            // 删除分类
            const deleteRequest = categoryStore.delete(id);
            deleteRequest.onsuccess = () => resolve();
            deleteRequest.onerror = () => reject(deleteRequest.error);
          }
        };
        
        cursorRequest.onerror = () => reject(cursorRequest.error);
      };
      getRequest.onerror = () => reject(getRequest.error);
    });
  }
}

10.4 React/Vue 集成示例

javascript 复制代码
// React Hook 示例
import { useState, useEffect, useCallback } from 'react';

function useNotes() {
  const [notes, setNotes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [noteService, setNoteService] = useState(null);
  
  useEffect(() => {
    initDB();
  }, []);
  
  async function initDB() {
    const dbManager = new NoteDatabase();
    await dbManager.init();
    setNoteService(new NoteService(dbManager.db));
    loadNotes();
  }
  
  async function loadNotes() {
    if (!noteService) return;
    setLoading(true);
    const allNotes = await noteService.getAllNotes();
    setNotes(allNotes);
    setLoading(false);
  }
  
  const createNote = useCallback(async (noteData) => {
    if (!noteService) return;
    const newId = await noteService.createNote(noteData);
    await loadNotes();
    return newId;
  }, [noteService]);
  
  const updateNote = useCallback(async (id, updates) => {
    if (!noteService) return;
    await noteService.updateNote(id, updates);
    await loadNotes();
  }, [noteService]);
  
  const deleteNote = useCallback(async (id) => {
    if (!noteService) return;
    await noteService.deleteNote(id);
    await loadNotes();
  }, [noteService]);
  
  return {
    notes,
    loading,
    createNote,
    updateNote,
    deleteNote,
    refreshNotes: loadNotes
  };
}

11. 性能优化与最佳实践

11.1 批量操作优化

javascript 复制代码
// ❌ 错误:循环中多次开启事务
async function addUsersBad(db, users) {
  for (const user of users) {
    await addUser(db, user); // 每个用户都开启新事务
  }
}

// ✅ 正确:使用单次事务批量添加
async function addUsersBatch(db, users) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    
    users.forEach(user => {
      store.add(user);
    });
    
    transaction.oncomplete = () => resolve();
    transaction.onerror = () => reject(transaction.error);
  });
}

11.2 使用 getAll() 代替游标遍历

javascript 复制代码
// ✅ 获取所有数据时优先使用 getAll()
const allUsers = await store.getAll();

// 仅在需要条件过滤或分页时使用游标

11.3 合理设置索引

javascript 复制代码
// 只为常用查询字段建立索引
// 索引会占用存储空间并影响写入性能

// ❌ 为每个字段都建立索引
userStore.createIndex('name', 'name');
userStore.createIndex('email', 'email');
userStore.createIndex('phone', 'phone');
userStore.createIndex('address', 'address');
userStore.createIndex('city', 'city');

// ✅ 只为核心查询字段建立索引
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('status', 'status');

11.4 使用 Promise 封装

javascript 复制代码
// 统一封装为 Promise,便于 async/await 使用
function promisify(request) {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 使用
const db = await promisify(indexedDB.open('mydb', 1));

11.5 错误处理与降级

javascript 复制代码
class SafeIndexedDB {
  constructor(dbName, version) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
    this.fallback = new Map(); // 降级方案使用 Map
  }
  
  async init() {
    if (!window.indexedDB) {
      console.warn('IndexedDB 不支持,使用内存存储');
      return false;
    }
    
    try {
      this.db = await this.openDB();
      return true;
    } catch (error) {
      console.error('IndexedDB 初始化失败', error);
      return false;
    }
  }
  
  async get(storeName, key) {
    if (!this.db) {
      return this.fallback.get(`${storeName}_${key}`);
    }
    
    const transaction = this.db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    return promisify(store.get(key));
  }
  
  // 其他方法类似...
}

11.6 存储空间管理

javascript 复制代码
// 检查存储使用情况
async function checkStorageQuota() {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    const estimate = await navigator.storage.estimate();
    const usageMB = estimate.usage / (1024 * 1024);
    const quotaMB = estimate.quota / (1024 * 1024);
    
    console.log(`已使用: ${usageMB.toFixed(2)} MB`);
    console.log(`总配额: ${quotaMB.toFixed(2)} MB`);
    console.log(`使用率: ${((usageMB / quotaMB) * 100).toFixed(2)}%`);
    
    // 当使用率超过 80% 时清理旧数据
    if (usageMB / quotaMB > 0.8) {
      await cleanupOldData();
    }
  }
}

// 清理过期数据
async function cleanupOldData() {
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
  
  const transaction = db.transaction(['notes'], 'readwrite');
  const store = transaction.objectStore('notes');
  const index = store.index('updatedAt');
  const range = IDBKeyRange.upperBound(thirtyDaysAgo);
  
  const cursorRequest = index.openCursor(range);
  cursorRequest.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      cursor.delete(); // 删除过期数据
      cursor.continue();
    }
  };
}

12. 常见问题与调试技巧

12.1 常见错误及解决方案

错误 原因 解决方案
DOMException: VersionError 版本号低于当前版本 使用更高的版本号
DOMException: ConstraintError 违反唯一约束 检查主键或唯一索引是否重复
DOMException: NotFoundError 对象仓库或索引不存在 检查名称拼写或在升级回调中创建
DOMException: TransactionInactiveError 事务已提交后操作 在事务生命周期内完成所有操作
DOMException: ReadOnlyError 只读事务中尝试写入 使用 readwrite 模式

12.2 调试技巧

javascript 复制代码
// 1. 在控制台查看数据库
// Chrome: Application → IndexedDB
// Firefox: Storage → IndexedDB

// 2. 监听所有请求
function debugRequest(request, operation) {
  request.onsuccess = () => {
    console.log(`[${operation}] 成功`, request.result);
  };
  request.onerror = () => {
    console.error(`[${operation}] 失败`, request.error);
  };
  return request;
}

// 3. 查看所有数据
async function debugPrintAllData(db) {
  const storeNames = Array.from(db.objectStoreNames);
  for (const name of storeNames) {
    const data = await db.transaction([name], 'readonly')
      .objectStore(name)
      .getAll();
    console.log(`${name}:`, data);
  }
}

12.3 数据迁移示例

javascript 复制代码
// 版本升级时迁移数据
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const oldVersion = event.oldVersion;
  
  if (oldVersion < 2) {
    // 升级到版本2:添加新字段
    const store = db.objectStore('users');
    const cursorRequest = store.openCursor();
    
    cursorRequest.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        const user = cursor.value;
        if (!user.createdAt) {
          user.createdAt = new Date();
          cursor.update(user);
        }
        cursor.continue();
      }
    };
  }
  
  if (oldVersion < 3) {
    // 升级到版本3:创建新索引
    const store = db.objectStore('users');
    if (!store.indexNames.contains('city')) {
      store.createIndex('city', 'city');
    }
  }
};

12.4 最佳实践总结

javascript 复制代码
// 完整的最佳实践示例
class BestPracticeDB {
  constructor(dbName) {
    this.dbName = dbName;
    this.db = null;
    this.version = 1;
  }
  
  async open() {
    if (this.db) return this.db;
    
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        
        // 监听数据库关闭
        this.db.onclose = () => {
          console.warn('数据库连接关闭');
          this.db = null;
        };
        
        // 监听数据库错误
        this.db.onerror = (event) => {
          console.error('数据库错误:', event.target.error);
        };
        
        resolve(this.db);
      };
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        this._createStores(db);
      };
    });
  }
  
  _createStores(db) {
    // 只在升级时创建结构
    if (!db.objectStoreNames.contains('data')) {
      const store = db.createObjectStore('data', { 
        keyPath: 'id', 
        autoIncrement: true 
      });
      
      // 只为常用查询创建索引
      store.createIndex('timestamp', 'timestamp');
    }
  }
  
  async transaction(storeName, mode, callback) {
    const db = await this.open();
    
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([storeName], mode);
      const store = transaction.objectStore(storeName);
      
      transaction.oncomplete = () => resolve();
      transaction.onerror = () => reject(transaction.error);
      
      callback(store, resolve, reject);
    });
  }
  
  async add(storeName, data) {
    return this.transaction(storeName, 'readwrite', (store, resolve, reject) => {
      const request = store.add(data);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  async get(storeName, key) {
    return this.transaction(storeName, 'readonly', (store, resolve, reject) => {
      const request = store.get(key);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  // 批量操作
  async bulkAdd(storeName, items) {
    return this.transaction(storeName, 'readwrite', (store, resolve, reject) => {
      let completed = 0;
      const total = items.length;
      
      items.forEach(item => {
        const request = store.add(item);
        request.onsuccess = () => {
          completed++;
          if (completed === total) resolve();
        };
        request.onerror = () => reject(request.error);
      });
    });
  }
}

结语

IndexedDB 是前端存储大规模数据的强大工具。虽然 API 设计相对复杂,但通过合理的封装和最佳实践,可以构建出高性能、可靠的离线应用。

学习建议

  1. 先掌握核心概念:数据库、对象仓库、索引、事务
  2. 使用 DevTools 实时查看数据,加深理解
  3. 从简单 CRUD 开始,逐步增加索引、游标等高级功能
  4. 在实际项目中尝试,比如构建离线待办事项或笔记应用

参考资料


本文档为 IndexedDB 完整教程,涵盖从入门到实战的全部内容。建议按章节顺序学习,并动手实践每个示例。

相关推荐
问道飞鱼2 小时前
【大模型学习】LangGraph 深度解析:定义、功能、原理与实践
数据库·学习·大模型·工作流
烤麻辣烫3 小时前
I/O流 基础流
java·开发语言·学习·intellij-idea
云边散步3 小时前
godot2D游戏教程系列二(22)
笔记·学习·游戏
jincheng_3 小时前
软件设计师上午题|9模块极速背诵版
学习
Schengshuo3 小时前
Spring学习——新建module模块
java·学习·spring
2401_865721334 小时前
WEB 学习框架搭建
网络·学习·web
lifewange4 小时前
删除学习“叶平”老师课的sc表记录
学习
健康人猿4 小时前
SuperGrok Lite 是啥?值不值得升级?与旗舰版的差距有多大?
人工智能·学习·ai
路小雨~4 小时前
Milvus 向量数据库的官方文档笔记
数据库·学习·milvus