HTML5 - IndexedDB
现代客户端存储:IndexedDB
Section titled “现代客户端存储:IndexedDB”本教程将探讨 IndexedDB,这是用于在客户端稳健地存储大量结构化数据的标准 API。它允许 Web 应用程序将数据持久地存储在用户的浏览器中。
注意:之前在此介绍的旧版 Web SQL Database API 现已弃用,不应在新项目中使用。它从未被正式采纳为 Web 标准,并且已被大多数现代浏览器移除或不再支持。IndexedDB 是推荐的现代替代方案。
IndexedDB 提供了一个异步的、事务性的、面向对象的数据库。与 SQL 数据库中使用行和列表的表格不同,它使用包含键值对的对象存储(object stores)。它受到所有现代 Web 浏览器的支持。
理解这些核心概念是使用 IndexedDB 的关键:
- Database(数据库):数据的容器。您通过名称和版本号打开与特定数据库的连接。
- Object Store(对象存储):类似于 SQL 数据库中的表。每个对象存储都包含通过键标识的记录(JavaScript 对象)。
- Index(索引):允许基于主键以外的属性高效查找记录。类似于 SQL 中的索引。
- Transaction(事务):操作的包装器,确保数据完整性(原子性)。所有读取和写入操作都在事务内发生。
- Request(请求):IndexedDB 操作是异步的。它们返回带有
onsuccess和onerror事件处理程序的请求对象,用于管理结果或错误。现代开发通常使用 Promises 来更轻松地处理这些异步操作。
打开数据库连接
Section titled “打开数据库连接”第一步是使用 indexedDB.open() 请求打开与数据库的连接。该方法接受数据库名称和可选的版本号。
// 请求打开名为 'myAppDB'、版本为 1 的数据库const request = indexedDB.open('myAppDB', 1);打开数据库是一个异步操作。您需要事件处理程序来知晓操作何时成功、失败或需要进行模式升级:
onerror: 处理打开请求期间发生的错误。onsuccess: 连接成功建立时触发。数据库连接对象可在event.target.result中获取。onupgradeneeded: 如果数据库尚不存在,或请求的版本号高于现有版本号时触发。这是创建或修改数据库模式(对象存储和索引)的唯一位置。
let db;
const request = indexedDB.open('myNotesAppDB', 1);
request.onerror = (event) => { console.error(`数据库错误: ${event.target.errorCode}`);};
request.onsuccess = (event) => { db = event.target.result; console.log(`数据库成功打开。`); // 您可以在此处开始使用数据库连接 (db)};
request.onupgradeneeded = (event) => { db = event.target.result; console.log(`需要数据库升级或进行初始设置。`);
// 创建一个名为 'notes' 的对象存储 // 'id' 将是键,它将自动递增 const objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
// 在 'title' 属性上创建一个名为 'title_idx' 的索引 objectStore.createIndex('title_idx', 'title', { unique: false });
// 在 'created' 属性上创建一个名为 'created_idx' 的索引 objectStore.createIndex('created_idx', 'created', { unique: false });
console.log('对象存储和索引已创建。');};使用 Promises 可以显著简化异步操作的处理。像 idb(由 Jake Archibald 开发)这样的库提供了围绕 IndexedDB 的轻量级 Promise 封装,但这里演示了如何手动封装打开请求:
function openNotesDatabase(dbName, version) { return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, version);
request.onerror = (event) => { reject(`Database error: ${event.target.errorCode}`); };
request.onsuccess = (event) => { resolve(event.target.result); };
request.onupgradeneeded = (event) => { console.log(`数据库升级版本 ${version} 所需。`); const db = event.target.result;
if (!db.objectStoreNames.contains('notes')) { const objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }); objectStore.createIndex('title_idx', 'title', { unique: false }); objectStore.createIndex('created_idx', 'created', { unique: false }); console.log('对象存储 \'notes\' 已创建。'); } // 如有需要,在此处添加未来版本的升级逻辑 }; });}
// 使用方法:openNotesDatabase('myNotesAppDB', 1) .then(dbInstance => { console.log('通过 Promise 成功打开数据库'); db = dbInstance; // 如有需要,赋值给全局或模块级别变量 // 开始使用 dbInstance }) .catch(error => { console.error(错误); });执行操作 (CRUD)
Section titled “执行操作 (CRUD)”所有数据操作(创建、读取、更新、删除)都必须在事务(transaction)内进行。
-
启动事务:指定涉及的对象存储以及模式(
'readonly'或'readwrite')。 -
获取对象存储:从事务对象访问所需的对象存储。
-
执行操作:使用
add()、put()、get()、getAll()、delete()或openCursor()等方法。 -
处理结果:在操作返回的请求对象上使用
onsuccess和onerror,或者更好地使用 Promises。
添加数据(创建)
Section titled “添加数据(创建)”使用 add() 方法向存储中添加新对象。如果具有相同键的对象已存在,则 add() 会失败。使用 put() 方法添加或更新(覆盖)对象。
function addNote(db, note) { return new Promise((resolve, reject) => { // 启动一个读写事务 const transaction = db.transaction(['notes'], 'readwrite'); const objectStore = transaction.objectStore('notes');
// 添加 note 对象 const request = objectStore.add(note);
request.onsuccess = (event) => { // event.target.result 包含添加项的键 resolve(event.target.result); };
request.onerror = (event) => { reject(`Error adding note: ${event.target.errorCode}`); };
// 可选:处理事务完成/中止 transaction.oncomplete = () => { console.log('添加事务完成。'); }; transaction.onerror = (event) => { console.error(`事务错误: ${event.target.errorCode}`); // 注意:请求错误通常也会冒泡到事务错误 }; });}
// 使用方法(假设 'db' 是已打开的数据库连接):const newNote = { title: 'My First Note', body: 'This is the content of the note.', created: new Date()};
addNote(db, newNote) .then(addedKey => { console.log(`已添加 note,键为: ${addedKey}`); }) .catch(error => { console.error(错误); });读取数据(读取)
Section titled “读取数据(读取)”使用 get(key) 按其键检索单个对象,或使用 getAll() 检索存储中的所有对象。您还可以使用索引和游标进行更复杂的查询。
function getAllNotes(db) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readonly'); const objectStore = transaction.objectStore('notes');
// 获取所有 note const request = objectStore.getAll();
request.onsuccess = (event) => { // event.target.result 包含所有 note 对象的数组 resolve(event.target.result); };
request.onerror = (event) => { reject(`Error getting notes: ${event.target.errorCode}`); }; });}
// 使用方法:getAllNotes(db) .then(notes => { console.log('已检索到 note:', notes); // 使用 note 更新 UI const statusDiv = document.getElementById('status'); // 假设有一个 id='status' 的元素 statusDiv.innerHTML = `<p>找到 ${notes.length} 个 note。</p>`; notes.forEach(note => { statusDiv.innerHTML += `<p>**${note.title}**: ${note.body}</p>`; }); }) .catch(error => { console.error(错误); });更新数据(更新)
Section titled “更新数据(更新)”使用 put() 方法。如果指定键的对象存在,它将被更新;否则,将添加一个新对象。
function updateNote(db, note) { // note 对象必须包含 'id' (keyPath) return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readwrite'); const objectStore = transaction.objectStore('notes'); const request = objectStore.put(note); // 使用 put 进行更新
request.onsuccess = (event) => { // event.target.result 包含更新/添加项的键 resolve(event.target.result); };
request.onerror = (event) => { reject(`Error updating note: ${event.target.errorCode}`); }; });}
// 使用方法(假设您之前已检索到一个 id=1 的 note):const noteToUpdate = { id: 1, // 必须与现有键匹配 title: '我的更新 Note 标题', body: '内容已更新。', created: new Date() // 或者保留原始日期};
updateNote(db, noteToUpdate) .then(updatedKey => { console.log(`已更新 note,键为: ${updatedKey}`); }) .catch(error => { console.error(错误); });删除数据(删除)
Section titled “删除数据(删除)”使用 delete(key) 方法按其键移除对象。
function deleteNote(db, key) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readwrite'); const objectStore = transaction.objectStore('notes'); const request = objectStore.delete(key);
request.onsuccess = (event) => { // 成功表示删除操作已排队 resolve(`Note with key ${key} delete request successful.`); };
request.onerror = (event) => { reject(`Error deleting note: ${event.target.errorCode}`); };
// 通常最好在事务完成后 resolve/reject transaction.oncomplete = () => { console.log(`键为 ${key} 的删除事务完成。`); resolve(`键为 ${key} 的 note 已成功删除。`); // 在这里 resolve 以确保成功 }; transaction.onerror = (event) => { reject(`删除事务错误: ${event.target.errorCode}`); };
});}
// 使用方法(删除 id=2 的 note):deleteNote(db, 2) .then(message => { console.log(消息); }) .catch(error => { console.error(错误); });这是一个基本的 HTML 页面,演示了如何打开数据库、添加 note,然后读取所有 note,并使用 Promises 实现更清晰的异步处理。
<!DOCTYPE HTML><html><head> <title>IndexedDB Notes App</title> <meta charset="UTF-8"></head><body>
<h1>My Notes</h1> <div id="status" name="status"> 正在加载数据库... </div>
<div id="notesList"> <!-- note 将在此处显示 --> </div>
<script> let db; // 用于保存数据库连接的变量 const dbName = 'myNotesAppDB'; const dbVersion = 1;
// --- 基于 Promise 的辅助函数 ---
function openNotesDatabase(name, version) { return new Promise((resolve, reject) => { const request = indexedDB.open(name, version); request.onerror = event => reject(`Database error: ${event.target.errorCode}`); request.onsuccess = event => resolve(event.target.result); request.onupgradeneeded = event => { console.log(`正在升级数据库到版本 ${version}...`); const tempDb = event.target.result; if (!tempDb.objectStoreNames.contains('notes')) { const store = tempDb.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }); store.createIndex('title_idx', 'title', { unique: false }); console.log('对象存储 \'notes\' 已创建。'); } }; }); }
function performTransaction(db, storeName, mode, action) { return new Promise((resolve, reject) => { const transaction = db.transaction([storeName], mode); const objectStore = transaction.objectStore(storeName);
transaction.oncomplete = () => resolve(); // 事务成功 transaction.onerror = event => reject(`Transaction error: ${event.target.errorCode}`);
try { action(objectStore, resolve, reject); // 将 store 和 Promise 控制传递给 action } catch (err) { reject(err); transaction.abort(); // 在 action 错误时中止事务 } }); }
function addNote(db, note) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readwrite'); const store = transaction.objectStore('notes'); const request = store.add(note); request.onsuccess = event => resolve(event.target.result); // 使用新键 resolve request.onerror = event => reject(`Add error: ${event.target.error}`); transaction.onerror = event => reject(`Transaction error: ${event.target.error}`); }); }
function getAllNotes(db) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readonly'); const store = transaction.objectStore('notes'); const request = store.getAll(); request.onsuccess = event => resolve(event.target.result); // 使用 note 数组 resolve request.onerror = event => reject(`GetAll error: ${event.target.error}`); transaction.onerror = event => reject(`Transaction error: ${event.target.error}`); }); }
function displayNotes(notes) { const listDiv = document.getElementById('notesList'); const statusDiv = document.getElementById('status'); listDiv.innerHTML = ''; // 清除先前的列表 if (notes.length === 0) { statusDiv.textContent = '尚未存储 note。'; return; } statusDiv.textContent = `正在显示 ${notes.length} 个 note:`; const ul = document.createElement('ul'); notes.forEach(note => { const li = document.createElement('li'); li.textContent = `ID ${note.id}: ${note.title} - ${note.body} (创建于: ${new Date(note.created).toLocaleString()})`; ul.appendChild(li); }); listDiv.appendChild(ul); }
// --- 主逻辑 --- async function initializeApp() { try { db = await openNotesDatabase(dbName, dbVersion); document.getElementById('status').textContent = '数据库成功打开。';
// 示例:如果不存在 note,则添加初始 note(简单检查) const currentNotes = await getAllNotes(db); if (currentNotes.length === 0) { console.log('正在添加初始 note...'); const key1 = await addNote(db, { title: '欢迎', body: '这是您的第一个 note!', created: new Date() }); const key2 = await addNote(db, { title: '提醒', body: '学习 IndexedDB', created: new Date() }); console.log(`已添加 note,键为: ${key1}, ${key2}`); }
// 加载并显示所有 note const notes = await getAllNotes(db); displayNotes(notes);
} catch (error) { console.error('初始化失败:', 错误); document.getElementById('status').textContent = `错误: ${error}`; } }
// 启动应用程序 initializeApp();
</script>
</body></html>关键注意事项与最佳实践
Section titled “关键注意事项与最佳实践”- 异步操作:请始终记住 IndexedDB 是异步的。使用回调、Promises(
async/await)或像idb这样的库来管理这一点。 - 错误处理:为请求和事务实现
onerror处理程序。错误可能因各种原因发生(例如,超出配额、违反约束等)。 - 事务范围:保持事务简短且针对特定任务。长时间运行的读写事务可能会锁定数据库。
- 模式版本控制:使用
onupgradeneeded事件并递增数据库版本,仔细规划您的模式升级。 - 浏览器兼容性:虽然得到广泛支持,但如果针对旧版浏览器,请务必检查特定功能或限制。caniuse.com 是一个很好的资源。
- 存储限制:浏览器会施加存储限制,这些限制可能有所不同。优雅地处理潜在的配额错误。
- 性能:使用索引在大数据集上进行高效查询。避免检索大量不必要的数据。
如需更深入的信息,请查阅官方文档和相关指南:
- MDN Web Docs - IndexedDB API:https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- MDN Web Docs - 使用 IndexedDB:https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
- web.dev - IndexedDB:https://web.dev/articles/indexeddb
- Google Developers - IndexedDB Codelab:https://developers.google.com/web/ilt/pwa/working-with-indexeddb