被前端存储坑到崩溃?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函数用于渲染待办事项列表。
- 表单提交事件监听器用于添加新的待办事项。
 
- 使用