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 对比

参考链接 ​