前端数据存储利器:IndexedDB

91 阅读6分钟

什么是 IndexedDB?

IndexedDB 是一个运行在浏览器中的事务型数据库系统,用于在客户端存储大量结构化数据。你可以把它想象成浏览器里的 "小型数据库",但它不是传统的关系型数据库,而是基于 JavaScript 对象的 NoSQL 数据库。

为什么需要 IndexedDB?

  • 存储大量数据:相比 localStorage(通常限制在 5MB),IndexedDB 可以存储数百 MB 甚至 GB 级的数据
  • 更复杂的数据结构:支持存储复杂对象,而不仅仅是字符串
  • 高性能查询:支持索引,可以快速检索数据
  • 异步操作:所有操作都是异步的,不会阻塞页面渲染
  • 事务支持:保证数据操作的完整性和一致性

核心概念

1. 数据库 (Database)

  • 每个源(协议+域名+端口)可以创建多个数据库
  • 数据库有版本概念,升级时需要处理版本变更

2. 对象仓库 (Object Store)

  • 相当于关系数据库中的"表"
  • 用于存储 JavaScript 对象

3. 索引 (Index)

  • 在对象仓库的特定属性上创建索引
  • 加速基于该属性的查询

4. 事务 (Transaction)

  • 所有数据操作都必须在事务中进行
  • 提供原子性:要么全部成功,要么全部失败

使用示例

1. 打开/创建数据库

// 打开或创建数据库
const request = indexedDB.open('MyDatabase', 1);
let db;  // 用于保存数据库连接的变量

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

request.onsuccess = (event) => {
  // 当数据库成功打开时触发
  db = event.target.result;  // 获取数据库实例
  console.log('数据库打开成功');
};

request.onupgradeneeded = (event) => {
  // 当数据库需要升级(首次创建或版本变更)时触发
  const db = event.target.result;
  
  // 创建对象仓库(类似于数据库表)
  // keyPath: 'id' 表示使用对象的 id 属性作为主键
  // autoIncrement: true 表示如果没有 id,会自动生成自增的 id
  const store = db.createObjectStore('users', { 
    keyPath: 'id', 
    autoIncrement: true 
  });
  
  // 在 name 属性上创建索引,允许重复
  store.createIndex('name', 'name', { unique: false });
  // 在 email 属性上创建唯一索引,不允许重复
  store.createIndex('email', 'email', { unique: true });
  
  console.log('对象仓库创建成功');
};

详细说明:

  • indexedDB.open('MyDatabase', 1)打开名为"MyDatabase"的数据库,版本号为1
  • 如果数据库不存在,会自动创建
  • onupgradeneeded只在数据库首次创建或版本升级时触发
  • 对象仓库(Object Store)类似于关系数据库中的表
  • 索引用于加速查询,唯一索引确保该字段值不重复

2. 添加数据

function addUser(user) {
  // 返回 Promise 以便使用 async/await
  return new Promise((resolve, reject) => {
    // 创建事务,指定要操作的对象仓库和事务模式
    // 'readwrite' 表示读写事务,'readonly' 表示只读事务
    const transaction = db.transaction(['users'], 'readwrite');
    // 获取对象仓库
    const store = transaction.objectStore('users');
    
    // 添加数据,返回的 request 对象代表这个异步操作
    const request = store.add(user);
    
    request.onsuccess = () => {
      // 添加成功,request.result 是自动生成的主键(id)
      console.log('用户添加成功,ID:', request.result);
      resolve(request.result);
    };
    
    request.onerror = (event) => {
      console.error('添加用户失败:', event.target.error);
      reject(event.target.error);
    };
  });
}

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

// 调用添加函数
addUser(user).then(id => {
  console.log('添加成功,用户ID:', id);
});

详细说明:

  • 所有数据操作都必须在事务中进行

  • 事务模式:

    • 'readonly':只读,性能更好
    • 'readwrite':可读写
  • store.add()添加数据,如果设置了 autoIncrement,返回自动生成的 id

  • 使用 Promise 包装,便于使用 async/await

3. 查询数据

根据主键查询

function getUserById(id) {
  return new Promise((resolve, reject) => {
    // 创建只读事务
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    
    // 根据主键查询
    const request = store.get(id);
    
    request.onsuccess = () => {
      // request.result 是查询到的数据,如果没有则是 undefined
      resolve(request.result);
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

使用索引查询

function getUserByEmail(email) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    // 获取 email 索引
    const index = store.index('email');
    
    // 通过索引查询
    const request = index.get(email);
    
    request.onsuccess = () => {
      resolve(request.result);
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

获取所有数据

function getAllUsers() {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    
    // 获取所有数据
    const request = store.getAll();
    
    request.onsuccess = () => {
      // request.result 是包含所有数据的数组
      resolve(request.result);
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

详细说明:

  • store.get(id):通过主键查询
  • index.get(value):通过索引查询
  • store.getAll():获取所有数据
  • 查询操作应该使用只读事务以提高性能

4. 更新数据

function updateUser(user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    
    // 使用 put 方法更新数据
    // 如果主键已存在,则更新;不存在,则添加
    const request = store.put(user);
    
    request.onsuccess = () => {
      resolve(request.result);
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

详细说明:

  • store.put()方法:

    • 如果数据的主键已存在,更新该数据
    • 如果不存在,添加新数据
  • 这相当于"插入或更新"操作

5. 删除数据

function deleteUser(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 = (event) => {
      reject(event.target.error);
    };
  });
}

6. 完整示例解析

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>indexDB</title>
  </head>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
      background-color: #f5f5f5;
    }
    .container {
      background-color: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }
    .add-wrap {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-bottom: 20px;
      padding: 15px;
      background-color: #f9f9f9;
      border-radius: 5px;
    }
    .add-wrap input {
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    .add-wrap button {
      padding: 10px;
      background-color: #4caf50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .add-wrap button:hover {
      background-color: #45a049;
    }
    .user-list {
      margin-top: 20px;
    }
    .user-item {
      border: 1px solid #ddd;
      padding: 15px;
      margin-bottom: 10px;
      border-radius: 5px;
      background-color: #fff;
    }
    .user-item p {
      margin: 5px 0;
    }
    .delete-btn {
      background-color: #f44336;
      color: white;
      border: none;
      padding: 5px 10px;
      border-radius: 3px;
      cursor: pointer;
    }
    .delete-btn:hover {
      background-color: #d32f2f;
    }
  </style>
  <body>
    <div class="container">
      <div class="add-wrap">
        <input type="number" id="userId" placeholder="请输入用户ID" />
        <input type="text" id="name" placeholder="请输入用户名" />
        <input type="email" id="email" placeholder="请输入邮箱" />
        <button onclick="handleAddUser()">添加数据</button>
      </div>
      <br />
      <div>
        <button onclick="displayAllUser()">显示所有用户</button>
        <div id="userList" class="user-list"></div>
      </div>
    </div>
  </body>
  <script>
    let db;

    function initDb() {
      return new Promise((resolve, reject) => {
        const request = indexedDB.open("MyDataBase", 1);

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

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

        // 数据库首次创建或者变更时触发
        request.onupgradeneeded = (event) => {
          const db = event.target.result;

          const store = db.createObjectStore("users", {
            keyPath: "id",
            autoIncrement: true,
          });

          store.createIndex("name", "name", { unique: false });

          // 在email上唯一索引
          store.createIndex("userId", "userId", { unique: true });
        };
      });
    }

    function handleAddUser() {
      const idxNode = document.querySelector("#userId");
      const nameNode = document.querySelector("#name");
      const emailNode = document.querySelector("#email");

      const user = {
        userId: idxNode.value,
        name: nameNode.value,
        email: emailNode.value,
        createAt: new Date(),
      };

      addUser(user)
        .then((res) => {
          displayAllUser();
          console.log("添加用户成功");
        })
        .catch((error) => {
          console.log("添加用户失败", error);
        });
    }

    function addUser(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 = (event) => {
          reject(event.target.error);
        };
      });
    }

    function displayAllUser() {
      const userList = document.querySelector("#userList");

      let str = "";
      getAllUsers()
        .then((res) => {
          if (res.length === 0) {
            str = `<p>没有用户数据</p>`;
          } else {
            res.forEach((item) => {
              str += `<div class="user-item">
                    <p>ID:${item.userId}</p>
                    <p>姓名:${item.name}</p>
                    <p>邮箱:${item.email}</p>
                    <p>创建时间:${item.createAt}</p>
                    <button class="delete-btn" onclick="deleteUser(${item.id})">删除</button>
                    </div>`;
            });
          }
          userList.innerHTML = str;
        })
        .catch((error) => {
          console.log("获取用户失败", error);
        });
    }

    function getAllUsers() {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(["users"], "readonly");
        const store = transaction.objectStore("users");

        const request = store.getAll();

        request.onsuccess = () => {
          resolve(request.result);
        };

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

    function deleteUser(id) {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(["users"], "readwrite");
        const store = transaction.objectStore("users");
        const request = store.delete(id);

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

        request.onerror = (event) => {
          console.log("删除用户失败");
          reject(event.target.error);
        };
      });
    }

    initDb()
      .then(() => {
        console.log("数据库初始化成功");
      })
      .catch((error) => {
        console.log("数据库打开失败", error);
      });
  </script>
</html>

image.png

核心概念总结

1. 数据库连接生命周期

// 打开连接
const request = indexedDB.open(name, version);

request.onupgradeneeded; // 数据库结构变更
request.onsuccess;       // 连接成功
request.onerror;         // 连接失败

2. 事务类型

  • readonly: 只读,性能更好,可并行
  • readwrite: 读写,会锁定对象仓库

3. 数据操作方法

  • add() : 添加新数据
  • get() : 根据主键查询
  • put() : 更新或添加数据
  • delete() : 删除数据
  • getAll() : 获取所有数据

4. 错误处理模式

request.onsuccess = () => {
  // 操作成功
};

request.onerror = (event) => {
  // 操作失败,通过 event.target.error 获取错误信息
};

实际使用建议

  1. 封装工具函数: 将常用操作封装成 Promise 函数
  2. 错误处理: 所有操作都要有错误处理
  3. 版本管理: 数据库结构变更时要更新版本号
  4. 事务优化: 只读操作使用 readonly 事务
  5. 连接管理: 合理管理数据库连接的生命周期