Skip to content

HTML5 - 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 来更轻松地处理这些异步操作。

第一步是使用 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(错误);
});

所有数据操作(创建、读取、更新、删除)都必须在事务(transaction)内进行。

  1. 启动事务:指定涉及的对象存储以及模式('readonly' 或 'readwrite')。

  2. 获取对象存储:从事务对象访问所需的对象存储。

  3. 执行操作:使用 add()、put()、get()、getAll()、delete() 或 openCursor() 等方法。

  4. 处理结果:在操作返回的请求对象上使用 onsuccess 和 onerror,或者更好地使用 Promises。

使用 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(错误);
});

使用 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(错误);
});

使用 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(错误);
});

使用 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>
  • 异步操作:请始终记住 IndexedDB 是异步的。使用回调、Promises(async/await)或像 idb 这样的库来管理这一点。
  • 错误处理:为请求和事务实现 onerror 处理程序。错误可能因各种原因发生(例如,超出配额、违反约束等)。
  • 事务范围:保持事务简短且针对特定任务。长时间运行的读写事务可能会锁定数据库。
  • 模式版本控制:使用 onupgradeneeded 事件并递增数据库版本,仔细规划您的模式升级。
  • 浏览器兼容性:虽然得到广泛支持,但如果针对旧版浏览器,请务必检查特定功能或限制。caniuse.com 是一个很好的资源。
  • 存储限制:浏览器会施加存储限制,这些限制可能有所不同。优雅地处理潜在的配额错误。
  • 性能:使用索引在大数据集上进行高效查询。避免检索大量不必要的数据。

如需更深入的信息,请查阅官方文档和相关指南: