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 物件存儲在**物件存儲(Object Store)**中。
  • 可以透過查詢或索引進行搜尋。
  • 擁有極大的儲存容量(數百 MB 或以上),可儲存的資料遠超 Cookie 或 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 事件。此時應建立資料表(object stores),並定義資料結構(schema)。

建立物件存儲(Object Store)

首先,在 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 屬性,作為每筆資料的主鍵(Primary Key)。如此一來,新增、搜尋或更新資料時可自動以 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() 進行刪除流程的範例

例如,刪除所有名稱為 Bob 的使用者流程如下:。

 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 更直觀地處理非同步流程。
  • 這可以避免回呼地獄(callback hell),讓程式碼更容易閱讀。

總結

當您希望在瀏覽器端進行進階資料管理時,IndexedDB 是非常強大的功能。一開始,基於事件的非同步處理可能會讓您感到困惑,但當您理解其結構之後,就能在客戶端執行完整的資料操作。

特別要注意以下幾點,能讓您使用上更加順利:。

  • 使用 onupgradeneeded 進行初始設置。
  • 注意交易的讀寫模式。
  • 索引能讓搜尋更有效率。
  • 資料可以以物件方式儲存,與 JSON 極為相容。

熟練掌握 IndexedDB 之後,PWA 及離線應用程式的資料管理就會簡單許多。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video