什么是 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>
核心概念总结
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 获取错误信息
};
实际使用建议
- 封装工具函数: 将常用操作封装成 Promise 函数
- 错误处理: 所有操作都要有错误处理
- 版本管理: 数据库结构变更时要更新版本号
- 事务优化: 只读操作使用 readonly 事务
- 连接管理: 合理管理数据库连接的生命周期