Skip to content

HTML5 - Web SQL 数据库

HTML5 - 使用 IndexedDB 进行客户端存储

Section titled “HTML5 - 使用 IndexedDB 进行客户端存储”

现代 Web 应用程序通常需要在用户计算机上直接存储数据。虽然 HTML5 引入了 LocalStorage 和 SessionStorage 等特性用于简单的键值对存储,但对于大量结构化数据、离线功能和复杂查询,则需要更健壮的解决方案。历史上,曾提出过一个名为 Web SQL Database 的 API,但它从未被正式标准化,由于缺乏跨浏览器的支持以及潜在的安全问题,目前已被废弃。现代开发中不应使用它。

当前强大的客户端存储标准是 IndexedDB API。它在浏览器内部提供一个事务性、异步、面向对象的数据库。与 Web SQL 不同,它不使用 SQL 查询。相反,你需要使用“对象仓库”(‘object stores’,类似于表)和“索引”(‘indexes’)来高效地检索数据。

IndexedDB 非常适合:存储大量用户数据、缓存应用程序数据以供离线使用(Progressive Web Apps - PWA)、以及管理需要查询或索引的数据。所有现代 Web 浏览器都支持它。

理解 IndexedDB 需要掌握以下关键概念:

  • 异步操作 (Asynchronous Operations): 几乎所有 IndexedDB 操作都是异步的。它们会立即返回,并在稍后通过事件或 Promises 通知你完成(成功或错误)。这可以防止阻塞浏览器主线程,确保响应灵敏的用户界面。
  • 数据库 (Databases): 你首先打开一个命名并带版本的数据库。
  • 对象仓库 (Object Stores): 在数据库内部,你创建对象仓库(类似于 SQL 中的表)来存放你的数据。每个仓库通常存放同一类型的数据对象(例如,‘users’、‘products’)。
  • 索引 (Indexes): 你可以在对象仓库中对象的特定属性上创建索引。索引允许基于该属性进行快速查找和迭代(例如,在 ‘users’ 仓库中,在 ‘email’ 属性上创建索引)。
  • 事务 (Transactions): 所有数据操作(读取、写入、删除)都必须在一个事务内发生。事务确保数据完整性——要么事务内的所有操作都成功,要么都不成功(原子性)。事务有不同的模式,通常是 readonly 或 readwrite。
  • 请求 (Requests): 异步操作会生成请求对象(IDBRequest 或 IDBOpenDBRequest)。你附加事件处理程序(onsuccess、onerror)或使用 Promises 来处理这些请求的结果。

第一步是使用 indexedDB.open() 请求打开一个数据库。此方法是异步的。

// Request opening 'myDatabase', version 1
const request = indexedDB.open('myDatabase', 1);
let db;
// Error handler
request.onerror = (event) => {
console.error(`Database error: ${event.target.errorCode}`);
};
// Success handler
request.onsuccess = (event) => {
db = event.target.result;
console.log('Database opened successfully.');
// Database is ready for transactions
};
// Upgrade handler (or initial setup)
request.onupgradeneeded = (event) => {
db = event.target.result;
console.log('Database upgrade needed or initial setup.');
// Create an object store 'users' if it doesn't exist
if (!db.objectStoreNames.contains('users')) {
// Use 'id' as the key path and auto-increment it
const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
// Create an index on the 'email' property for quick lookups
userStore.createIndex('email', 'email', { unique: true });
console.log('Object store "users" created.');
}
// Add other object stores or indexes here if needed for this version
};

关于 indexedDB.open(name, version) 的关键点:

  • name: 你的数据库名称。
  • version: 一个整数版本号。如果数据库不存在,或者提供的版本高于现有版本,则会触发 onupgradeneeded 事件。
  • onupgradeneeded: 这是唯一可以修改数据库结构(创建/删除对象仓库,创建/删除索引)的地方。如果在需要升级时,它会在 onsuccess 之前运行。
  • onsuccess: 在数据库成功以指定版本打开时触发。event.target.result 提供数据库连接对象(IDBDatabase)。
  • onerror: 如果打开数据库时发生错误(例如,磁盘配额超出,安全错误),则触发此事件。

所有数据读写都必须在一个事务内发生。你在数据库连接对象上启动一个事务。

// Assume 'db' is the opened database connection from the previous example
// Start a read-write transaction on the 'users' object store
const transaction = db.transaction(['users'], 'readwrite');
// Get the object store reference from the transaction
const userStore = transaction.objectStore('users');
// Now you can perform operations like add, put, get, delete on userStore
// Transaction completion handlers
transaction.oncomplete = () => {
console.log('Transaction completed successfully.');
};
transaction.onerror = (event) => {
console.error(`Transaction error: ${event.target.error}`);
};
// Example: Add data (see next section)

db.transaction() 方法接受两个参数:

  • stores: 参与此事务的对象仓库名称数组(例如,['users'] 或 ['users', 'products'])。
  • mode: 事务模式:'readonly'(默认值,用于读取数据)或 'readwrite'(用于添加、更新或删除数据)。

现代 JavaScript 通常使用 Promises 或 async/await 来管理 IndexedDB 的异步性,使代码更简洁。虽然这里的示例为了清晰展示底层机制使用了事件处理程序,但在实际项目中,wrapper libraries 或 careful Promise wrapping 是常见的做法。你可以在 MDN 文档中找到相关资源。

要添加或更新数据,请在 readwrite 事务中,对对象仓库使用 put() 或 add() 方法。

// Assume 'db' is the opened database connection
const newUser = { name: 'John Doe', email: 'john.doe@example.com' }; // 'id' will be auto-generated
const transaction = db.transaction(['users'], 'readwrite');
const userStore = transaction.objectStore('users');
// Use add() to insert - fails if a user with the same key already exists
// const request = userStore.add(newUser);
// Use put() to insert or update - overwrites if key exists
const request = userStore.put(newUser);
request.onsuccess = (event) => {
// event.target.result contains the key of the added/updated record (the auto-generated id)
console.log(`User added/updated with ID: ${event.target.result}`);
};
request.onerror = (event) => {
console.error(`Error adding/updating user: ${event.target.error}`);
// If using add() and the email index is unique, this might fire on duplicate email
};
// Transaction handlers (oncomplete, onerror) are important too!

主要区别:

  • add(data): 添加 data 对象。如果已存在具有相同键的对象,则请求失败(触发 onerror)。
  • put(data): 添加 data 对象,如果已存在具有相同键的对象,则更新该对象。

要读取数据,通常在 readonly(或 readwrite)事务中使用 get() 获取单条记录,或使用 openCursor() 迭代多条记录。

示例:根据键读取单条记录:

// Assume 'db' is the opened database connection and user with ID 1 exists
const transaction = db.transaction(['users'], 'readonly');
const userStore = transaction.objectStore('users');
const keyToGet = 1; // The primary key (id) we want to retrieve
const request = userStore.get(keyToGet);
request.onsuccess = (event) => {
const user = event.target.result;
if (user) {
console.log('User found:', user); // { id: 1, name: 'John Doe', email: '...' }
// Update UI, etc.
} else {
console.log(`User with key ${keyToGet} not found.`);
}
};
request.onerror = (event) => {
console.error(`Error getting user: ${event.target.error}`);
};

示例:使用 cursor 读取多条记录:

// Assume 'db' is the opened database connection
const transaction = db.transaction(['users'], 'readonly');
const userStore = transaction.objectStore('users');
const users = []; // Array to hold the results
const cursorRequest = userStore.openCursor();
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// cursor.key contains the key
// cursor.value contains the object
console.log(`Found user: ID=${cursor.key}, Name=${cursor.value.name}`);
users.push(cursor.value);
cursor.continue(); // Move to the next record
} else {
// No more entries
console.log('Finished iterating through users.');
console.log('All users:', users);
// Update UI with the 'users' array
}
};
cursorRequest.onerror = (event) => {
console.error(`Error opening cursor: ${event.target.error}`);
};

你还可以在索引上使用 openCursor(),根据索引属性的顺序或在特定范围内迭代记录。

这是一个演示如何打开数据库、添加数据,然后读取并显示数据的基本 HTML 页面:

<!DOCTYPE HTML>
<html>
<head>
<title>IndexedDB Example</title>
<style>
body { font-family: sans-serif; }
#status, #output { margin-top: 10px; padding: 10px; border: 1px solid #ccc; background-color: #f9f9f9; }
#output p { margin: 5px 0; }
</style>
</head>
<body>
<h1>IndexedDB Simple Demo</h1>
<div id="status">Connecting to database...</div>
<div id="output">
<h2>Stored Users:</h2>
<p>Loading...</p>
</div>
<script>
const dbName = 'myDemoDB';
const dbVersion = 1;
let db;
const statusDiv = document.getElementById('status');
const outputDiv = document.getElementById('output');
function logStatus(message) {
console.log(message);
statusDiv.textContent = message;
}
function displayError(message, error) {
console.error(message, error);
statusDiv.textContent = `${message} - See console for details.`;
statusDiv.style.color = 'red';
}
// 1. Open Database Request
const request = indexedDB.open(dbName, dbVersion);
request.onerror = (event) => {
displayError(`Database error: ${event.target.errorCode}`);
};
request.onupgradeneeded = (event) => {
logStatus('Database upgrade needed or initial setup.');
db = event.target.result;
if (!db.objectStoreNames.contains('contacts')) {
const store = db.createObjectStore('contacts', { keyPath: 'id', autoIncrement: true });
store.createIndex('emailIdx', 'email', { unique: true });
logStatus('Object store "contacts" created.');
} else {
logStatus('Object store "contacts" already exists.');
}
};
request.onsuccess = (event) => {
db = event.target.result;
logStatus('Database opened successfully.');
// Add some initial data if needed, then display
addInitialData();
};
function addInitialData() {
const transaction = db.transaction(['contacts'], 'readwrite');
const store = transaction.objectStore('contacts');
let itemsAdded = 0;
const initialContacts = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
];
initialContacts.forEach(contact => {
// Using put - won't error if they already exist from a previous run
const req = store.put(contact);
req.onsuccess = () => { itemsAdded++; };
req.onerror = (e) => { console.warn(`Could not add ${contact.email}: ${e.target.error}`); };
});
transaction.oncomplete = () => {
logStatus(`Initial data check/addition complete (${itemsAdded} potentially new).`);
displayContacts(); // Now display the data
};
transaction.onerror = (event) => {
displayError('Error during initial data transaction.', event.target.error);
};
}
function displayContacts() {
const transaction = db.transaction(['contacts'], 'readonly');
const store = transaction.objectStore('contacts');
const cursorRequest = store.openCursor();
outputDiv.innerHTML = '<h2>Stored Users:</h2>'; // Clear previous content
let foundData = false;
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
foundData = true;
const contact = cursor.value;
const p = document.createElement('p');
p.textContent = `ID: ${contact.id}, Name: ${contact.name}, Email: ${contact.email}`;
outputDiv.appendChild(p);
cursor.continue();
} else {
logStatus('Finished loading contacts.');
if (!foundData) {
const p = document.createElement('p');
p.textContent = 'No contacts found in the database.';
outputDiv.appendChild(p);
}
}
};
cursorRequest.onerror = (event) => {
displayError('Error reading contacts.', event.target.error);
outputDiv.innerHTML += '<p style="color: red;">Error loading contacts.</p>';
};
}
</script>
</body>
</html>

此示例演示了基本异步流程:打开 -> 升级/创建仓库(如果需要)-> 成功 -> 执行事务(添加/更新)-> 执行事务(读取/迭代)-> 显示结果。请记住始终为所有请求和事务添加健壮的错误处理 (onerror)。

IndexedDB 是一个功能强大的 API,具有更多特性,例如使用索引进行高效查询、删除数据、优雅处理版本更改以及理解存储限制。有关更深入的信息,请查阅 MDN Web Docs:

虽然原生 API 功能强大,但一些开发者发现其基于事件的特性比较冗长。像 Dexie.js (https://dexie.org/) 这样的库提供了一个基于 Promise 的 wrapper,可以显著简化 IndexedDB 的使用,特别是对于复杂的应用程序。