JavaScript 和 IndexedDB

JavaScript 和 IndexedDB

本文将介绍 JavaScript 和 IndexedDB。

本教程将逐步讲解 JavaScript 和 IndexedDB,每一步都提供实际示例代码,帮助您加深理解。

YouTube Video

javascript-indexed-db.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args
119                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
120                    .join(" ");
121                output.appendChild(message);
122            };
123
124            // Override console.error
125            const originalError = console.error;
126            console.error = function (...args) {
127                originalError.apply(console, args);
128                const message = document.createElement('div');
129                message.textContent = args
130                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
131                    .join(" ");
132                message.style.color = 'red'; // Color error messages red
133                output.appendChild(message);
134            };
135        })();
136
137        document.getElementById('executeBtn').addEventListener('click', () => {
138            // Prevent multiple loads
139            if (document.getElementById('externalScript')) return;
140
141            const script = document.createElement('script');
142            script.src = 'javascript-indexed-db.js';
143            script.id = 'externalScript';
144            //script.onload = () => console.log('javascript-indexed-db.js loaded and executed.');
145            //script.onerror = () => console.log('Failed to load javascript-indexed-db.js.');
146            document.body.appendChild(script);
147        });
148    </script>
149</body>
150</html>

JavaScript 和 IndexedDB

IndexedDB 是浏览器内置的异步键值存储数据库。它提供了类似于关系型数据库的功能,可以在客户端存储和查询大量结构化数据。它在支持离线的应用程序和 PWA(渐进式网页应用)中尤其有用。

IndexedDB 的特点

  • 异步和事件驱动的方式运行。
  • JavaScript 对象可以存储在对象存储中。
  • 可以通过查询或索引进行搜索。
  • 拥有较大的存储容量(数百 MB 甚至更多),可以存储比 cookies 或 localStorage 多得多的数据。

打开和创建数据库

要使用 IndexedDB,必须首先打开数据库。如果数据库不存在,则会自动创建。

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • open 方法会以异步方式打开数据库,并触发以下三个事件。

onsuccess

1// Fired when database is successfully opened
2request.onsuccess = (event) => {
3    const db = event.target.result; // Database instance
4    console.log('Database opened successfully:', db.name);
5};
  • 当数据库成功打开时会触发 onsuccess 事件。之后的操作应使用此时可用的 request.result

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • 当数据库打开失败时会触发 onerror 事件。在这里进行错误日志记录和错误处理。

onupgradeneeded

 1// Fired when database is newly created or upgraded
 2request.onupgradeneeded = (event) => {
 3    const db = event.target.result;
 4    console.log('Database upgrade needed (or newly created):', db.name);
 5
 6    // Example: Create an object store (like a table) if it doesn’t exist
 7    if (!db.objectStoreNames.contains('users')) {
 8        db.createObjectStore('users', { keyPath: 'id' });
 9        console.log('Object store "users" created');
10    }
11};
  • 当数据库新建或指定版本高于当前版本时,会触发 onupgradeneeded 事件。应在此时创建表(对象存储)和定义架构。

创建对象存储

首先,在 onupgradeneeded 事件中创建一个“对象存储”(相当于表)。

1request.onupgradeneeded = function (event) {
2    const db = event.target.result;
3    console.log("onupgradeneeded triggered. Database version:", db.version);
4    if (!db.objectStoreNames.contains('users')) {
5        console.log("Creating object store: users");
6        const store = db.createObjectStore('users', { keyPath: 'id' });
7        store.createIndex('name', 'name', { unique: false });
8    }
9};

在这里,应用以下设置:。

  • createObjectStore createObjectStore 是在数据库中创建新对象存储的方法。可以在对象存储中保存记录,并通过指定 keyPath 等选项来定义数据的管理方式。此操作必须在 onupgradeneeded 事件中进行。

  • keyPath: 'id'keyPath 设置为 id 属性,作为唯一标识每条记录的主键。这样在添加、查询或更新数据时会自动使用 id

  • createIndex 使用 createIndex 方法基于 name 属性创建索引便于搜索。通过设置 unique: false,允许有多个拥有相同 name 的记录。

处理数据库连接成功

将数据库连接成功后要执行的操作分配给 onsuccess 事件。在此过程中,获取数据库实例并为读写数据做准备。

1request.onsuccess = function (event) {
2    const db = event.target.result;
3    console.log('Database opened successfully');
4    // Use db for reading and writing in subsequent operations
5};
  • IndexedDB 数据库成功连接时,执行该操作。

  • event.target.result 包含已打开的数据库实例(IDBDatabase 对象),用于启动事务和访问对象存储。

  • 实际的数据增、查、改、删操作都是通过 db 对象来实现的。

此时数据库已准备就绪,可以安全地启动事务。

新增数据

下面介绍如何向 IndexedDB 添加新数据。

 1function addUser(db, user) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.add(user);
 5
 6    request.onsuccess = () => {
 7        console.log('User added:', user);
 8    };
 9
10    request.onerror = () => {
11        console.error('Add failed:', request.error);
12    };
13}
14
15// Example: Add a user
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    addUser(db, { id: 1, name: 'Alice' });
19    addUser(db, { id: 2, name: 'Bob' });
20    addUser(db, { id: 3, name: 'John' });
21};
  • db.transaction() 创建事务,指定要操作的对象存储及模式(此处用 readwrite)。
  • store.add() 方法添加新数据。
  • 事务会自动提交,如果需要将多个操作分组,可以使用事务结束事件进行管理。

数据查询(主键搜索)

下面介绍如何利用主键检索特定数据。

 1function getUserById(db, id) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.get(id);
 5
 6    request.onsuccess = () => {
 7        if (request.result) {
 8            console.log('User found:', request.result);
 9        } else {
10            console.log('User not found');
11        }
12    };
13
14    request.onerror = () => {
15        console.error('Error retrieving user:', request.error);
16    };
17}
18
19// Example: Get a user by id
20request.onsuccess = function (event) {
21    const db = event.target.result;
22    getUserById(db, 1);
23};
  • 通过 db.transaction() 创建只读事务。
  • store.get(id) 获取与指定主键对应的数据。
  • 检索成功会调用 onsuccess,如有结果则显示。如果没有结果,则视为“无对应数据”。

使用索引搜索

如需按主键以外的属性进行搜索,可使用事先创建的索引。

 1function getUserByName(db, name) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.get(name);
 6
 7    request.onsuccess = () => {
 8        if (request.result) {
 9            console.log('User by name:', request.result);
10        } else {
11            console.log('No user found with that name');
12        }
13    };
14
15    request.onerror = () => {
16        console.error('Error retrieving user by name:', request.error);
17    };
18}
19
20// Example: Get a user by id
21request.onsuccess = function (event) {
22    const db = event.target.result;
23    getUserByName(db, 'Alice');
24};
  • 通过 store.index('name') 访问已创建的 name 索引。
  • index.get(value) 获取第一个值匹配的记录。 如有多个记录相同,可用 index.getAll(value) 一次性全部获取。

更新数据

要更新(覆盖)现有数据,可使用 put() 方法。

如有相同主键的记录则更新,没有则添加新记录。

 1function updateUser(db, updatedUser) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.put(updatedUser);
 5
 6    request.onsuccess = () => {
 7        console.log('User updated:', updatedUser);
 8    };
 9
10    request.onerror = () => {
11        console.error('Update failed:', request.error);
12    };
13}
14
15// Example: Update user
16request.onsuccess = async (event) => {
17    const db = event.target.result;
18
19    // Test : update existing user
20    updateUser(db, { id: 3, name: 'John Updated' });
21
22    // Test : insert new user
23    updateUser(db, { id: 4, name: 'Charlie' });
24};
  • put() 方法既支持更新也支持添加,非常方便。
  • 如已存在相同主键的数据,则会被覆盖。
  • 如更新前需判断是否已存在,可先用 get() 检查。

删除数据

要删除指定主键的数据,可使用 delete() 方法。

 1function deleteUser(db, id) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.delete(id);
 5
 6    request.onsuccess = () => {
 7        console.log(`User with id=${id} deleted successfully`);
 8    };
 9
10    request.onerror = () => {
11        console.error(`Failed to delete user with id=${id}:`, request.error);
12    };
13}
14
15// Example: Delete a user by id
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    deleteUser(db, 4);
19};
  • store.delete(id) 删除对应主键的数据。
  • 注意:即使数据不存在也不会报错,仍视作成功。
  • 实现错误处理可让代码更加健壮。

获取所有数据

getAll()

要获取对象存储中的所有记录,可通过 getAll() 方法实现。

 1function getAllUsers(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.getAll();
 5
 6    request.onsuccess = () => {
 7        console.log('All users:', request.result);
 8    };
 9
10    request.onerror = () => {
11        console.error('Failed to retrieve users:', request.error);
12    };
13}
14
15// Example: Get all users
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    getAllUsers(db);
19};
  • getAll() 会将指定对象存储中的所有记录作为数组获取。
  • 即使一次检索大量数据,也能高效处理。
  • 结果以数组形式存储在 request.result 中。
  • 务必添加错误处理以应对可能的失败。

openCursor()

openCursor() 是用于顺序遍历对象存储或索引记录的方法。当需要逐条处理数据而不是一次性获取大量数据时,非常实用。

 1function getAllUsersWithCursor(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.openCursor();
 5
 6    request.onsuccess = () => {
 7        const cursor = request.result;
 8        if (cursor) {
 9            console.log('User:', cursor.value); // Process the current record
10            cursor.continue(); // Move to the next record
11        } else {
12            console.log('All users have been processed.');
13        }
14    };
15
16    request.onerror = () => {
17        console.error('Failed to open cursor:', request.error);
18    };
19}
20
21// Example: Get all users
22request.onsuccess = function (event) {
23    const db = event.target.result;
24    getAllUsersWithCursor(db);
25};
  • 通过 openCursor(),可启动游标,一条条读取对象存储中的所有记录。
  • cursor.value 获取当前记录的数据对象。
  • cursor.continue() 将游标移动到下一条记录。
  • cursor === null 时,说明所有记录均已遍历完毕。

使用 openCursor() 进行更新操作的示例

例如,将名称为 Alice 的用户名称更改为 Alicia 的过程如下:。

 1function updateUserName(db, oldName, newName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name'); // Use the 'name' index
 5    const request = index.openCursor(IDBKeyRange.only(oldName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const user = cursor.value;
11            user.name = newName; // Update the name
12            const updateRequest = cursor.update(user);
13
14            updateRequest.onsuccess = () => {
15                console.log('Updated user:', user);
16            };
17
18            updateRequest.onerror = () => {
19                console.error('Failed to update user:', updateRequest.error);
20            };
21
22            cursor.continue();
23        } else {
24            console.log('All matching users have been updated.');
25        }
26    };
27
28    request.onerror = () => {
29        console.error('Cursor error:', request.error);
30    };
31}
32
33// Example: Update user name
34request.onsuccess = function (event) {
35    const db = event.target.result;
36    updateUserName(db, 'Alice', 'Alicia');
37};
  • IDBKeyRange.only(oldName) 通过使用 IDBKeyRange.only,可以仅定位键与 oldName 完全匹配的记录。当你希望直接访问某个特定值时,这非常有用。

  • cursor.update() 更新 cursor.value 后,调用 update() 会覆盖相应的记录。

  • 处理多条匹配记录 通过调用 cursor.continue(),可以将指针移动到下一个匹配的记录。这样可以依次处理所有符合相同键或条件的多条记录。

  • 错误处理 当操作失败时,在 onerror 输出日志可以更方便地排查原因和在实际运行中进行问题处理。

使用 openCursor() 进行删除操作的示例

例如,删除所有 nameBob 的用户的过程如下:。

 1function deleteUsersByName(db, targetName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.openCursor(IDBKeyRange.only(targetName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const deleteRequest = cursor.delete();
11
12            deleteRequest.onsuccess = () => {
13                console.log('Deleted user:', cursor.value);
14            };
15
16            deleteRequest.onerror = () => {
17                console.error('Failed to delete user:', deleteRequest.error);
18            };
19
20            cursor.continue();
21        } else {
22            console.log('All matching users have been deleted.');
23        }
24    };
25
26    request.onerror = () => {
27        console.error('Cursor error:', request.error);
28    };
29}
30
31// Example: Delete user by name
32request.onsuccess = function (event) {
33    const db = event.target.result;
34    deleteUsersByName(db, 'Bob');
35};
  • cursor.delete() 使用 cursor.delete() 可以删除指针当前位置的记录。由于结果是异步返回的,你可以在 onsuccess 中确认删除过程。

关于事务和异步处理的注意事项

IndexedDB 是异步且基于事件驱动的。所有操作都必须通过 onsuccessonerror 事件处理。当你希望将多个操作组合在一起时,把它们包装到 Promise 中会很方便。

 1function openDatabase() {
 2    return new Promise((resolve, reject) => {
 3        const request = indexedDB.open('MyDatabase', 1);
 4
 5        request.onupgradeneeded = function (event) {
 6            const db = event.target.result;
 7
 8            // Initialization process (creating object stores and indexes, etc.)
 9            const store = db.createObjectStore('users', { keyPath: 'id' });
10            store.createIndex('name', 'name', { unique: false });
11        };
12
13        request.onsuccess = function (event) {
14            const db = event.target.result;
15            resolve(db);
16        };
17
18        request.onerror = function () {
19            reject(request.error);
20        };
21    });
22}
23
24function addUserAsync(db, user) {
25    return new Promise((resolve, reject) => {
26        const transaction = db.transaction('users', 'readwrite');
27        const store = transaction.objectStore('users');
28        const request = store.add(user);
29
30        request.onsuccess = () => resolve();
31        request.onerror = () => reject(request.error);
32    });
33}
34
35async function main() {
36    try {
37        const db = await openDatabase();
38        await addUserAsync(db, { id: 1, name: 'Alice' });
39        console.log('User added successfully');
40    } catch (error) {
41        console.error('Error:', error);
42    }
43}
  • 将诸如 openDatabaseaddUserAsync 等函数用 Promise 包装后,就可以通过 async/await 直观地处理异步流程。
  • 这样可以避免回调地狱,使代码更加易读。

总结

在需要在浏览器端进行高级数据管理时,IndexedDB 是一个非常强大的功能。最初你可能会被基于事件的异步处理方式搞糊涂,但一旦理解其结构,就可以在客户端进行完整的数据操作。

尤其是注意以下几点,会让你使用起来更加流畅:。

  • 使用 onupgradeneeded 进行初始设置。
  • 注意事务的读写模式。
  • 索引能够实现高效检索。
  • 数据可以作为对象存储,与 JSON 高度兼容。

掌握了 IndexedDB,PWA 和离线应用的数据管理就会变得简单很多。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video