昨天我們介紹了 IndexedDB 的基本操作:開啟資料庫、建立 Object Store、基本的新增/讀取/更新/刪除。今天要更深入,看看它更強大的功能,以及一些實務上會遇到的狀況。
IndexedDB 提供 createIndex
方法,讓你針對 Object Store 中的欄位建立索引,就像關聯式資料庫中的 secondary index。建立後,可以透過該欄位快速查詢,而不必只能依靠主鍵。
objectStore.createIndex(name, keyPath, options)
{ unique: true }
表示值不能重複。const request = indexedDB.open("MyDB", 2);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = event.target.transaction.objectStore("notes");
// 建立索引:名稱、對應欄位、是否唯一
if (!store.indexNames.contains("by_title")) {
store.createIndex("by_title", "title", { unique: false });
}
};
// 使用索引查詢
const tx = db.transaction("notes", "readonly");
const idx = tx.objectStore("notes").index("by_title");
const req = idx.getAll("會議紀錄");
req.onsuccess = () => console.log(req.result);
這樣就能快速找到所有 title = '會議紀錄'
的筆記。
Cursor 是 IndexedDB 常用的 API,用來逐筆讀取資料,而不是一次性全取出。常用於「分頁」、「篩選」、「批次處理」。相較於 getAll()
一次取出全部,Cursor 更省記憶體,也能控制方向與篩選。
objectStore.openCursor(range, direction)
IDBKeyRange
限制範圍。next
(預設):由小到大。prev
:由大到小。nextunique
/ prevunique
:只取不同 key。const tx = db.transaction("notes", "readonly");
const store = tx.objectStore("notes");
// 開啟游標(Cursor):逐筆讀取資料,而不是一次 getAll 全抓
// 好處:更省記憶體、可控制遍歷方向與範圍(可搭配 IDBKeyRange)
const req = store.openCursor();
req.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log("id:", cursor.key, "value:", cursor.value);
cursor.continue(); // 移到下一筆
} else {
console.log("遍歷完成");
}
};
Cursor 也能結合索引使用,例如透過 idx.openCursor(range, direction)
做範圍查詢與排序。
在 IndexedDB 中,如果想要做條件查詢,可以透過 IDBKeyRange
來建立範圍。
IDBKeyRange.lowerBound(lower, open);
IDBKeyRange.upperBound(upper, open);
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
IDBKeyRange.only(value);
// 查詢 key >= 10 且 <= 20
const range = IDBKeyRange.bound(10, 20);
const req = store.openCursor(range);
// 查詢 key >= 100
const range2 = IDBKeyRange.lowerBound(100);
這些功能讓 IndexedDB 查詢更像傳統資料庫,而不只是 key-value 存取。
IndexedDB 容量雖大,但若遇到數萬筆甚至更多資料,還是需要注意:
getAll()
一口氣拉出所有資料,建議用 Cursor 分頁載入,減少記憶體壓力。IndexedDB 的 schema(Object Store、Index)只能在 版本升級 (onupgradeneeded
) 時建立或修改。這裡有幾個常見陷阱:
indexedDB.open("MyDB", version)
若版本號沒改,就不會觸發升級流程。event.oldVersion
判斷要執行哪些升級,確保兼容舊版本。範例:
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
// 升級步驟設計成「遞增式」:依舊版本逐段補齊,避免跳版造成漏建
if (oldVersion < 1) {
db.createObjectStore("notes", { keyPath: "id", autoIncrement: true });
}
// v2:在既有的 'notes' store 上建立索引
// 注意:只能在 onupgradeneeded 的升級 transaction 內調整索引/結構
if (oldVersion < 2) {
const store = event.target.transaction.objectStore("notes");
store.createIndex("by_title", "title");
}
};
這樣就能安全地處理不同版本的 schema 升級。
原生 IndexedDB API 使用事件回呼,常常寫成巢狀函式,難以維護。改用 Promise 會更直觀:
function getAllNotes(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction("notes", "readonly");
const store = tx.objectStore("notes");
const req = store.getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
// 使用
const notes = await getAllNotes(db);
雖然 IndexedDB 很強大,但原生 API 較繁瑣。這時可以使用封裝好的工具,例如 Dexie.js。
import Dexie from "dexie";
const db = new Dexie("MyDB");
db.version(1).stores({
notes: "++id, title, createdAt"
});
// 新增
await db.notes.add({ title: "新筆記", createdAt: Date.now() });
// 查詢
const list = await db.notes.where("title").equals("新筆記").toArray();
// 範圍查詢
const recent = await db.notes.where("createdAt").above(Date.now() - 86400000).toArray();
相比原生 API,Dexie.js 語法更簡潔、直觀,還支援 transaction、hook、liveQuery 等進階功能。
今天我們進一步探索了 IndexedDB 的進階功能與實務應用:
到這裡,你已經能駕馭 IndexedDB,打造離線應用或快取系統。🚀
👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯