Skip to content

IndexedDB 入门

IndexedDB 是浏览器提供的大型客户端数据库系统,支持存储结构化数据、文件和二进制对象。与 localStorage 相比,IndexedDB 支持索引查询、事务操作和游标遍历,存储容量几乎没有上限(通常可达数 GB),适合需要存储大量数据或进行复杂查询的 Web 应用。

前置知识

阅读本节前,建议先了解:Storage API 详解

基础概念

IndexedDB 核心概念

概念说明类比
数据库(Database)数据存储的最高层级数据库
对象仓库(Object Store)类似"表",存储结构化数据
索引(Index)加速数据查询索引
事务(Transaction)保证数据操作原子性事务
游标(Cursor)遍历查询结果集游标
请求(Request)所有异步操作返回 IDBRequestPromise

IndexedDB vs localStorage

特性IndexedDBlocalStorage
存储容量数百 MB 到数 GB约 5MB
数据类型对象、文件、Blob 等仅字符串
查询能力索引查询、游标遍历只能按键名
事务支持支持不支持
异步操作是(非阻塞)否(同步阻塞)
适用场景大量数据、复杂查询简单键值存储

语法与 API

打开数据库

javascript
// 打开或创建数据库
const request = indexedDB.open('myDatabase', 1);

// 数据库版本号变化时触发(创建或升级)
request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // 创建对象仓库(类似创建表)
  if (!db.objectStoreNames.contains('users')) {
    const store = db.createObjectStore('users', {
      keyPath: 'id',        // 主键
      autoIncrement: true    // 自动递增
    });

    // 创建索引
    store.createIndex('name', 'name', { unique: false });
    store.createIndex('email', 'email', { unique: true });
    store.createIndex('age', 'age', { unique: false });
  }

  // 创建第二个对象仓库
  if (!db.objectStoreNames.contains('products')) {
    const store = db.createObjectStore('products', {
      keyPath: 'id',
      autoIncrement: true
    });
    store.createIndex('name', 'name', { unique: false });
    store.createIndex('price', 'price', { unique: false });
    store.createIndex('category', 'category', { unique: false });
  }
};

// 成功打开
request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('数据库打开成功:', db.name, '版本:', db.version);
};

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

增删改查操作

javascript
// 获取数据库引用后执行操作
const db = null; // 假设已通过 open 获取

// ========== 新增数据(Create)==========
function addUser(user) {
  const transaction = db.transaction('users', 'readwrite');
  const store = transaction.objectStore('users');
  const request = store.add(user);

  request.onsuccess = () => {
    console.log('用户添加成功, ID:', request.result);
  };

  request.onerror = (event) => {
    console.error('添加失败:', event.target.error);
  };
}

// ========== 读取数据(Read)==========
function getUser(id) {
  const transaction = db.transaction('users', 'readonly');
  const store = transaction.objectStore('users');
  const request = store.get(id);

  request.onsuccess = () => {
    console.log('查询结果:', request.result);
  };
}

// ========== 更新数据(Update)==========
function updateUser(user) {
  const transaction = db.transaction('users', 'readwrite');
  const store = transaction.objectStore('users');
  const request = store.put(user); // put 可以新增或更新

  request.onsuccess = () => {
    console.log('用户更新成功');
  };
}

// ========== 删除数据(Delete)==========
function deleteUser(id) {
  const transaction = db.transaction('users', 'readwrite');
  const store = transaction.objectStore('users');
  const request = store.delete(id);

  request.onsuccess = () => {
    console.log('用户删除成功');
  };
}

// ========== 清空数据 ==========
function clearUsers() {
  const transaction = db.transaction('users', 'readwrite');
  const store = transaction.objectStore('users');
  const request = store.clear();

  request.onsuccess = () => {
    console.log('用户数据已清空');
  };
}

详细说明

事务模式

IndexedDB 的事务有三种模式:

模式说明操作
readonly只读(默认)读取数据
readwrite读写增删改
versionchange版本变更创建/删除对象仓库和索引
javascript
// 只读事务
const tx1 = db.transaction('users', 'readonly');

// 读写事务
const tx2 = db.transaction('users', 'readwrite');

// 跨多个对象仓库的事务
const tx3 = db.transaction(['users', 'products'], 'readwrite');

// 监听事务完成
tx3.oncomplete = () => {
  console.log('事务完成');
};

tx3.onerror = (event) => {
  console.error('事务失败:', event.target.error);
};

// 事务回滚
tx3.abort();

使用索引查询

javascript
// 通过索引查找
function getUserByName(name) {
  const transaction = db.transaction('users', 'readonly');
  const store = transaction.objectStore('users');
  const index = store.index('name');
  const request = index.get(name);

  request.onsuccess = () => {
    console.log('找到用户:', request.result);
  };
}

// 通过索引范围查询
function getUsersByAgeRange(min, max) {
  const transaction = db.transaction('users', 'readonly');
  const store = transaction.objectStore('users');
  const index = store.index('age');

  // IDBKeyRange 范围查询
  const range = IDBKeyRange.bound(min, max);
  const request = index.openCursor(range);

  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      console.log(`年龄 ${cursor.key}:`, cursor.value);
      cursor.continue();
    }
  };
}

// IDBKeyRange 方法
IDBKeyRange.only(25);              // 等于 25
IDBKeyRange.lowerBound(18);         // 大于等于 18
IDBKeyRange.upperBound(60);         // 小于等于 60
IDBKeyRange.bound(18, 60);         // 18 到 60
IDBKeyRange.bound(18, 60, true);   // 18 到 60(不含边界)

使用游标遍历

javascript
// 遍历所有数据
function getAllUsers() {
  const transaction = db.transaction('users', 'readonly');
  const store = transaction.objectStore('users');
  const request = store.openCursor();

  const users = [];

  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      users.push(cursor.value);
      cursor.continue(); // 继续下一条
    } else {
      console.log('所有用户:', users);
    }
  };
}

// 游标高级用法
function getUsersFiltered() {
  const transaction = db.transaction('users', 'readonly');
  const store = transaction.objectStore('users');
  const index = store.index('age');

  const range = IDBKeyRange.lowerBound(20);
  const request = index.openCursor(range, 'prev'); // prev: 降序

  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      console.log(cursor.key, cursor.value);
      // cursor.advance(5); // 跳过 5 条
      // cursor.delete();  // 删除当前记录
      // cursor.update(newValue); // 更新当前记录
      cursor.continue();
    }
  };
}

Promise 封装

javascript
// 将 IndexedDB 的回调风格封装为 Promise
class IDBHelper {
  constructor(dbName, version) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
  }

  // 打开数据库
  open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        this._onUpgrade(db);
      };

      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve(this.db);
      };

      request.onerror = (event) => {
        reject(event.target.error);
      };
    });
  }

  // 子类覆盖此方法定义结构
  _onUpgrade(db) {}

  // 通用事务操作
  _transaction(storeName, mode, callback) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(storeName, mode);
      const store = transaction.objectStore(storeName);
      const result = callback(store);

      if (result && result.onsuccess !== undefined) {
        result.onsuccess = () => resolve(result.result);
        result.onerror = () => reject(result.error);
      } else {
        transaction.oncomplete = () => resolve();
        transaction.onerror = () => reject(transaction.error);
      }
    });
  }

  // 添加
  add(storeName, data) {
    return this._transaction(storeName, 'readwrite', store => store.add(data));
  }

  // 获取
  get(storeName, key) {
    return this._transaction(storeName, 'readonly', store => store.get(key));
  }

  // 获取所有
  getAll(storeName) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(storeName, 'readonly');
      const store = transaction.objectStore(storeName);
      const request = store.getAll();

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  // 更新
  put(storeName, data) {
    return this._transaction(storeName, 'readwrite', store => store.put(data));
  }

  // 删除
  delete(storeName, key) {
    return this._transaction(storeName, 'readwrite', store => store.delete(key));
  }
}

// 使用示例
const db = new IDBHelper('myApp', 1);

// 自定义数据库结构
db._onUpgrade = (database) => {
  if (!database.objectStoreNames.contains('notes')) {
    const store = database.createObjectStore('notes', { keyPath: 'id' });
    store.createIndex('title', 'title', { unique: false });
    store.createIndex('updatedAt', 'updatedAt', { unique: false });
  }
};

// 初始化
db.open().then(() => {
  console.log('数据库就绪');

  // 添加数据
  return db.add('notes', {
    id: 1,
    title: '学习笔记',
    content: 'IndexedDB 学习内容',
    updatedAt: Date.now()
  });
}).then(() => {
  // 获取所有数据
  return db.getAll('notes');
}).then(notes => {
  console.log('笔记列表:', notes);
});

实战示例

完整的 IndexedDB 笔记应用

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>IndexedDB 笔记应用</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; background: #f5f7fa; }

    .app { max-width: 700px; margin: 0 auto; }
    h1 { text-align: center; color: #1a1a2e; }

    .form {
      padding: 20px;
      background: white;
      border-radius: 12px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      margin-bottom: 20px;
    }

    .form input, .form textarea {
      width: 100%;
      padding: 10px;
      margin-bottom: 10px;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
      font-size: 14px;
    }

    .form textarea { height: 80px; resize: vertical; }
    .form button {
      padding: 10px 24px;
      background: #1a73e8;
      color: white;
      border: none;
      border-radius: 8px;
      cursor: pointer;
    }

    .note-item {
      padding: 16px;
      background: white;
      border-radius: 10px;
      margin-bottom: 10px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    }

    .note-title { font-weight: 600; color: #333; }
    .note-content { color: #666; margin-top: 6px; font-size: 14px; }
    .note-time { font-size: 12px; color: #aaa; margin-top: 8px; }

    .note-actions {
      margin-top: 8px;
      display: flex;
      gap: 8px;
    }

    .note-actions button {
      padding: 4px 12px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
    }

    .btn-delete { background: #fee2e2; color: #dc2626; }
    .stats { margin-top: 12px; font-size: 13px; color: #888; }
  </style>
</head>
<body>
  <div class="app">
    <h1>IndexedDB 笔记应用</h1>

    <div class="form">
      <input type="text" id="noteTitle" placeholder="标题">
      <textarea id="noteContent" placeholder="内容"></textarea>
      <button onclick="addNote()">添加笔记</button>
    </div>

    <div id="noteList"></div>
    <div class="stats" id="stats"></div>
  </div>

  <script>
    const DB_NAME = 'NotesApp';
    const DB_VERSION = 1;
    const STORE_NAME = 'notes';
    let db = null;

    // 初始化数据库
    function initDB() {
      return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onupgradeneeded = (event) => {
          const database = event.target.result;
          if (!database.objectStoreNames.contains(STORE_NAME)) {
            const store = database.createObjectStore(STORE_NAME, {
              keyPath: 'id',
              autoIncrement: true
            });
            store.createIndex('title', 'title', { unique: false });
            store.createIndex('createdAt', 'createdAt', { unique: false });
          }
        };

        request.onsuccess = (event) => {
          db = event.target.result;
          resolve(db);
          loadNotes();
        };

        request.onerror = (event) => reject(event.target.error);
      });
    }

    // 添加笔记
    function addNote() {
      const title = document.getElementById('noteTitle').value.trim();
      const content = document.getElementById('noteContent').value.trim();

      if (!title) { alert('请输入标题'); return; }

      const note = {
        title,
        content,
        createdAt: Date.now()
      };

      const transaction = db.transaction(STORE_NAME, 'readwrite');
      const store = transaction.objectStore(STORE_NAME);
      store.add(note);

      transaction.oncomplete = () => {
        document.getElementById('noteTitle').value = '';
        document.getElementById('noteContent').value = '';
        loadNotes();
      };
    }

    // 加载所有笔记
    function loadNotes() {
      const transaction = db.transaction(STORE_NAME, 'readonly');
      const store = transaction.objectStore(STORE_NAME);
      const request = store.getAll();

      request.onsuccess = () => {
        const notes = request.result.sort((a, b) => b.createdAt - a.createdAt);
        renderNotes(notes);
        document.getElementById('stats').textContent =
          `共 ${notes.length} 条笔记,存储在 IndexedDB 中`;
      };
    }

    // 渲染笔记
    function renderNotes(notes) {
      const container = document.getElementById('noteList');
      container.innerHTML = notes.map(note => `
        <div class="note-item">
          <div class="note-title">${note.title}</div>
          <div class="note-content">${note.content}</div>
          <div class="note-time">${new Date(note.createdAt).toLocaleString()}</div>
          <div class="note-actions">
            <button class="btn-delete" onclick="deleteNote(${note.id})">删除</button>
          </div>
        </div>
      `).join('');
    }

    // 删除笔记
    function deleteNote(id) {
      const transaction = db.transaction(STORE_NAME, 'readwrite');
      const store = transaction.objectStore(STORE_NAME);
      store.delete(id);
      transaction.oncomplete = () => loadNotes();
    }

    // 初始化
    initDB().catch(console.error);
  </script>
</body>
</html>

注意事项

  1. IndexedDB 是异步 API,不会阻塞主线程
  2. 数据库结构只能在 onupgradeneeded 中修改
  3. 版本号只能升高,不能降低
  4. 事务会在所有请求完成后自动提交
  5. 大量数据操作应使用 getAll() 而非逐条游标

最佳实践

  1. 封装 Promise 化的 IndexedDB 操作
  2. 合理设计索引以提升查询性能
  3. 批量操作使用事务减少开销
  4. 为每个应用使用独立的数据库和版本
  5. 处理 onblocked 事件处理多标签页升级冲突

下一节

继续学习:Cookie 对比

参考链接