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 核心概念
Section titled “IndexedDB 核心概念”理解 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 1const request = indexedDB.open('myDatabase', 1);
let db;
// Error handlerrequest.onerror = (event) => { console.error(`Database error: ${event.target.errorCode}`);};
// Success handlerrequest.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: 如果打开数据库时发生错误(例如,磁盘配额超出,安全错误),则触发此事件。
在事务中操作数据
Section titled “在事务中操作数据”所有数据读写都必须在一个事务内发生。你在数据库连接对象上启动一个事务。
// Assume 'db' is the opened database connection from the previous example
// Start a read-write transaction on the 'users' object storeconst transaction = db.transaction(['users'], 'readwrite');
// Get the object store reference from the transactionconst userStore = transaction.objectStore('users');
// Now you can perform operations like add, put, get, delete on userStore
// Transaction completion handlerstransaction.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 文档中找到相关资源。
添加或更新数据 (Put/Add)
Section titled “添加或更新数据 (Put/Add)”要添加或更新数据,请在 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 existsconst 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对象,如果已存在具有相同键的对象,则更新该对象。
读取数据 (Get/Cursor)
Section titled “读取数据 (Get/Cursor)”要读取数据,通常在 readonly(或 readwrite)事务中使用 get() 获取单条记录,或使用 openCursor() 迭代多条记录。
示例:根据键读取单条记录:
// Assume 'db' is the opened database connection and user with ID 1 existsconst transaction = db.transaction(['users'], 'readonly');const userStore = transaction.objectStore('users');
const keyToGet = 1; // The primary key (id) we want to retrieveconst 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 connectionconst 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:
- MDN IndexedDB API 指南: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- IndexedDB 背后的基本概念: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Concepts_Behind_IndexedDB
- 使用 IndexedDB: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
虽然原生 API 功能强大,但一些开发者发现其基于事件的特性比较冗长。像 Dexie.js (https://dexie.org/) 这样的库提供了一个基于 Promise 的 wrapper,可以显著简化 IndexedDB 的使用,特别是对于复杂的应用程序。