被前端存储坑到崩溃?IndexedDB 高效用法帮你少走 90% 弯路
IndexedDB 是一种在浏览器中提供事务性的键值对存储的低级 API。它允许你在用户的浏览器中存储大量结构化数据,并且可以对其进行高效的搜索、更新和删除操作。IndexedDB 适用于需要离线存储和快速访问大量数据的应用程序,如 Progressive Web Apps (PWAs) 和单页应用程序 (SPAs)。本文将详细介绍如何在前端项目中高效使用 IndexedDB。
1. IndexedDB 基本概念
1.1 数据库
数据库是存储对象集合的地方。每个数据库都有一个名称和版本号。
1.2 对象存储空间(Object Store)
对象存储空间类似于关系型数据库中的表。每个对象存储空间可以包含一组对象,并且每个对象都有一个唯一的键。
1.3 索引(Index)
索引用于快速查找对象存储空间中的对象。索引可以基于对象的属性创建,从而加速查询操作。
1.4 事务(Transaction)
事务是一组操作的集合,这些操作要么全部成功,要么全部失败。事务可以保证数据的一致性和完整性。
1.5 请求(Request)
请求用于执行数据库操作,如添加、删除或获取数据。请求是异步的,可以通过事件监听器处理结果。
2. IndexedDB 基本操作
2.1 打开数据库
const request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = function(event) {
const db = event.target.result;
// 创建对象存储空间
if (!db.objectStoreNames.contains('myStore')) {
const objectStore = db.createObjectStore('myStore', { keyPath: 'id', autoIncrement: true });
// 创建索引
objectStore.createIndex('nameIndex', 'name', { unique: false });
}
};
request.onsuccess = function(event) {
const db = event.target.result;
console.log('Database opened successfully');
};
request.onerror = function(event) {
console.error('Database error:', event.target.errorCode);
};
2.2 添加数据
function addData(db, data) {
const transaction = db.transaction(['myStore'], 'readwrite');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.add(data);
request.onsuccess = function(event) {
console.log('Data added successfully');
};
request.onerror = function(event) {
console.error('Error adding data:', event.target.errorCode);
};
}
// 使用示例
const db = request.result;
addData(db, { name: 'John Doe', age: 30 });
2.3 获取数据
function getData(db, id) {
const transaction = db.transaction(['myStore'], 'readonly');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.get(id);
request.onsuccess = function(event) {
const data = event.target.result;
console.log('Data retrieved:', data);
};
request.onerror = function(event) {
console.error('Error retrieving data:', event.target.errorCode);
};
}
// 使用示例
getData(db, 1);
2.4 更新数据
function updateData(db, id, newData) {
const transaction = db.transaction(['myStore'], 'readwrite');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.get(id);
request.onsuccess = function(event) {
const data = event.target.result;
if (data) {
Object.assign(data, newData);
const updateRequest = objectStore.put(data);
updateRequest.onsuccess = function() {
console.log('Data updated successfully');
};
updateRequest.onerror = function(event) {
console.error('Error updating data:', event.target.errorCode);
};
} else {
console.error('Data not found');
}
};
request.onerror = function(event) {
console.error('Error retrieving data:', event.target.errorCode);
};
}
// 使用示例
updateData(db, 1, { age: 31 });
2.5 删除数据
function deleteData(db, id) {
const transaction = db.transaction(['myStore'], 'readwrite');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.delete(id);
request.onsuccess = function(event) {
console.log('Data deleted successfully');
};
request.onerror = function(event) {
console.error('Error deleting data:', event.target.errorCode);
};
}
// 使用示例
deleteData(db, 1);
3. 使用 Promise 封装 IndexedDB 操作
为了简化异步操作,可以使用 Promise 封装 IndexedDB 的基本操作。
function openDatabase(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = function(event) {
const db = event.target.result;
if (!db.objectStoreNames.contains('myStore')) {
const objectStore = db.createObjectStore('myStore', { keyPath: 'id', autoIncrement: true });
objectStore.createIndex('nameIndex', 'name', { unique: false });
}
};
request.onsuccess = function(event) {
resolve(event.target.result);
};
request.onerror = function(event) {
reject(event.target.error);
};
});
}
function addData(db, data) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['myStore'], 'readwrite');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.add(data);
request.onsuccess = function(event) {
resolve(event.target.result);
};
request.onerror = function(event) {
reject(event.target.error);
};
});
}
function getData(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['myStore'], 'readonly');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.get(id);
request.onsuccess = function(event) {
resolve(event.target.result);
};
request.onerror = function(event) {
reject(event.target.error);
};
});
}
function updateData(db, id, newData) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['myStore'], 'readwrite');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.get(id);
request.onsuccess = function(event) {
const data = event.target.result;
if (data) {
Object.assign(data, newData);
const updateRequest = objectStore.put(data);
updateRequest.onsuccess = function() {
resolve();
};
updateRequest.onerror = function(event) {
reject(event.target.error);
};
} else {
reject(new Error('Data not found'));
}
};
request.onerror = function(event) {
reject(event.target.error);
};
});
}
function deleteData(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['myStore'], 'readwrite');
const objectStore = transaction.objectStore('myStore');
const request = objectStore.delete(id);
request.onsuccess = function(event) {
resolve();
};
request.onerror = function(event) {
reject(event.target.error);
};
});
}
3.1 使用示例
openDatabase('myDatabase', 1)
.then(db => {
return addData(db, { name: 'John Doe', age: 30 });
})
.then(id => {
console.log('Data added with ID:', id);
return getData(db, id);
})
.then(data => {
console.log('Data retrieved:', data);
return updateData(db, data.id, { age: 31 });
})
.then(() => {
console.log('Data updated successfully');
return deleteData(db, data.id);
})
.then(() => {
console.log('Data deleted successfully');
})
.catch(error => {
console.error('Error:', error);
});
4. 使用 idb 库简化操作
idb 是一个用于简化 IndexedDB 操作的库,提供了更简洁的 API。
4.1 安装 idb
npm install idb
4.2 使用 idb 进行操作
import { openDB } from 'idb';
async function openDatabase() {
const db = await openDB('myDatabase', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('myStore')) {
const objectStore = db.createObjectStore('myStore', { keyPath: 'id', autoIncrement: true });
objectStore.createIndex('nameIndex', 'name', { unique: false });
}
},
});
return db;
}
async function addData(db, data) {
const tx = db.transaction('myStore', 'readwrite');
const store = tx.objectStore('myStore');
await store.add(data);
await tx.done;
}
async function getData(db, id) {
const tx = db.transaction('myStore', 'readonly');
const store = tx.objectStore('myStore');
return store.get(id);
}
async function updateData(db, id, newData) {
const tx = db.transaction('myStore', 'readwrite');
const store = tx.objectStore('myStore');
const data = await store.get(id);
if (data) {
Object.assign(data, newData);
await store.put(data);
} else {
throw new Error('Data not found');
}
await tx.done;
}
async function deleteData(db, id) {
const tx = db.transaction('myStore', 'readwrite');
const store = tx.objectStore('myStore');
await store.delete(id);
await tx.done;
}
// 使用示例
(async () => {
try {
const db = await openDatabase();
const id = await addData(db, { name: 'John Doe', age: 30 });
console.log('Data added with ID:', id);
const data = await getData(db, id);
console.log('Data retrieved:', data);
await updateData(db, data.id, { age: 31 });
console.log('Data updated successfully');
await deleteData(db, data.id);
console.log('Data deleted successfully');
} catch (error) {
console.error('Error:', error);
}
})();
5. 高效使用 IndexedDB 的最佳实践
5.1 使用事务
尽量将多个操作放在同一个事务中,以减少事务的开销并提高性能。
5.2 创建索引
为经常查询的字段创建索引,可以显著提高查询速度。
5.3 数据分片
如果需要存储大量数据,可以考虑将数据分片存储在不同的对象存储空间中。
5.4 异步操作
IndexedDB 是异步的,充分利用异步操作可以避免阻塞主线程,提高应用的响应速度。
5.5 错误处理
始终为每个请求添加错误处理逻辑,以便在出现问题时能够及时捕获并处理。
5.6 数据版本管理
使用版本号来管理数据库的升级和降级,确保数据的一致性和完整性。
6. 示例:构建一个简单的待办事项应用
下面是一个使用 IndexedDB 构建的简单待办事项应用的示例。
6.1 HTML 结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#todo-form {
margin-bottom: 20px;
}
#todo-list {
list-style-type: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.todo-item button {
margin-left: 10px;
}
</style>
</head>
<body>
<h1>Todo List</h1>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="Add a new todo" required>
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
<script src="app.js"></script>
</body>
</html>
6.2 JavaScript 代码 (app.js)
import { openDB } from 'idb';
let db;
async function openDatabase() {
db = await openDB('todoDatabase', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('todos')) {
db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true });
}
},
});
}
async function addTodo(todo) {
const tx = db.transaction('todos', 'readwrite');
const store = tx.objectStore('todos');
await store.add(todo);
await tx.done;
renderTodos();
}
async function getTodos() {
const tx = db.transaction('todos', 'readonly');
const store = tx.objectStore('todos');
return store.getAll();
}
async function deleteTodo(id) {
const tx = db.transaction('todos', 'readwrite');
const store = tx.objectStore('todos');
await store.delete(id);
await tx.done;
renderTodos();
}
async function renderTodos() {
const todos = await getTodos();
const todoList = document.getElementById('todo-list');
todoList.innerHTML = '';
todos.forEach(todo => {
const li = document.createElement('li');
li.className = 'todo-item';
li.textContent = todo.text;
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', () => deleteTodo(todo.id));
li.appendChild(deleteButton);
todoList.appendChild(li);
});
}
document.getElementById('todo-form').addEventListener('submit', async (event) => {
event.preventDefault();
const input = document.getElementById('todo-input');
const text = input.value.trim();
if (text) {
await addTodo({ text });
input.value = '';
}
});
(async () => {
await openDatabase();
renderTodos();
})();
6.3 解释
-
HTML 结构:
- 包含一个表单用于添加新的待办事项。
- 包含一个无序列表用于显示所有待办事项。
-
JavaScript 代码:
- 使用
idb库简化 IndexedDB 的操作。 - 打开数据库并创建
todos对象存储空间。 - 提供
addTodo、getTodos和deleteTodo函数来操作数据。 -
renderTodos函数用于渲染待办事项列表。 - 表单提交事件监听器用于添加新的待办事项。
- 使用