iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Modern Web

從 Canvas 到各式各樣的 Web API 之旅系列 第 23

Day 23 - IndexedDB 進階篇:索引建立、Cursor 遍歷、範圍查詢

  • 分享至 

  • xImage
  •  

昨天我們介紹了 IndexedDB 的基本操作:開啟資料庫、建立 Object Store、基本的新增/讀取/更新/刪除。今天要更深入,看看它更強大的功能,以及一些實務上會遇到的狀況。


索引(Index)

IndexedDB 提供 createIndex 方法,讓你針對 Object Store 中的欄位建立索引,就像關聯式資料庫中的 secondary index。建立後,可以透過該欄位快速查詢,而不必只能依靠主鍵。

objectStore.createIndex(name, keyPath, options)

  • name:索引名稱,之後存取要用。
  • keyPath:對應物件中的屬性,例如 "title"。
  • 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)

Cursor 是 IndexedDB 常用的 API,用來逐筆讀取資料,而不是一次性全取出。常用於「分頁」、「篩選」、「批次處理」。相較於 getAll() 一次取出全部,Cursor 更省記憶體,也能控制方向與篩選。

objectStore.openCursor(range, direction)

  • range:可用 IDBKeyRange 限制範圍。
  • direction
    • 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) 做範圍查詢與排序。


範圍查詢(IDBKeyRange)

在 IndexedDB 中,如果想要做條件查詢,可以透過 IDBKeyRange 來建立範圍。

IDBKeyRange.lowerBound(lower, open);
IDBKeyRange.upperBound(upper, open);
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
IDBKeyRange.only(value);
  • lowerBound:大於等於指定值。
  • upperBound:小於等於指定值。
  • bound:落在範圍內。
  • only:等於某個值。
// 查詢 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 容量雖大,但若遇到數萬筆甚至更多資料,還是需要注意:

  1. 批次操作:一次 transaction 包含多筆寫入,效能遠比逐筆建立 transaction 佳。
  2. Cursor 分頁:避免 getAll() 一口氣拉出所有資料,建議用 Cursor 分頁載入,減少記憶體壓力。
  3. Web Worker:大筆序列化或處理時,建議丟進 Worker,避免主執行緒卡住 UI。

版本升級陷阱

IndexedDB 的 schema(Object Store、Index)只能在 版本升級 (onupgradeneeded) 時建立或修改。這裡有幾個常見陷阱:

  • 版本號必須遞增indexedDB.open("MyDB", version) 若版本號沒改,就不會觸發升級流程。
  • 升級失敗風險:若 migration 流程寫錯,使用者資料可能無法正確存取。
  • 最佳實務:透過 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 升級。


封裝 API:避免 Callback Hell

原生 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);

Dexie.js:讓 IndexedDB 更好用

雖然 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 的進階功能與實務應用:

  • 索引(Index):快速查詢欄位。
  • Cursor(游標):逐筆遍歷、分頁與排序。
  • 範圍查詢(IDBKeyRange):靈活條件查詢。
  • 大量資料處理:批次操作、Cursor 分頁、Web Worker。
  • 版本升級策略:版本號設計與 migration 策略。
  • Promise 封裝:改善程式碼可讀性。
  • Dexie.js:第三方工具能省去繁瑣細節,專注在商業邏輯。

到這裡,你已經能駕馭 IndexedDB,打造離線應用或快取系統。🚀


👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯


上一篇
Day 22 - IndexedDB 初探:瀏覽器裡的資料庫
系列文
從 Canvas 到各式各樣的 Web API 之旅23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言