iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Modern Web

從 UI/UX 設計到前端實作:30 天打造專屬的任務學習計時器系列 第 23 篇

DAY 23|Local-First 架構(二):實作 Promise 版 IndexedDB 工具模組

  • 分享至 

  • xImage
  •  

因為 IndexedDB 的原生寫法太過古老繁瑣且難以維護,因此實作中會使用 Promise版的IndexedDB。


一、什麼是「Promise 版」?為什麼要這樣做?

1. 原生 IndexedDB 的問題:傳統的事件監聽(Callback)

IndexedDB 是很久以前就有的語法,採用 「發起請求 → 監聽事件」 的方式運作。
如果直接用原生語法寫,光是打開資料庫、讀取一筆資料,程式碼會是過深的巢狀,資料庫邏輯很難獨立拆分出來,因此接下來會使用Promise版。

2. 什麼是「Promise 版」?

在現代 JavaScript 中,習慣使用 Promise 與 async / await,讓非同步的程式碼讀起來就像一般的同步程式碼一樣直覺:

// Promise 版寫法
const tasks = await getAllTasksFromDB();
console.log("拿到的資料:", tasks);

所謂「封裝成 Promise 版」,就是寫一個包裝函式(Helper),把原生繁瑣的事件監聽(onsuccess / onerror)包在 Promise 的 resolve() 與 reject() 裡面。

這樣一來,只要簡單用 await 就能輕鬆跟資料庫溝通。


二、設計 TaskTimerDB 資料庫結構

在開始寫程式之前,先規劃好瀏覽器資料庫裡的藍圖:

  • 資料庫名稱(Database Name):TaskTimerDB

  • 版本號(Version):1

  • 資料表名稱(Object Store):tasks

  • 主鍵(Key Path):id(以每個主任務的 id 為唯一識別碼)


三、實作 db.js 封裝模組

為了保持程式碼結構清晰,另外建立一個獨立的 db.js 檔案,專門管理 IndexedDB 的連線與資料操作。

步驟 1:建立基礎設定與連線控制 (openDB)

首先設定資料庫名稱、版本與資料表(Object Store)名稱,並實作 openDB() 函式。這裡使用單例模式(Singleton)暫存連線,避免重複觸發開啟資料庫的操作。

// ==========================================
// TaskTimer - IndexedDB 封裝模組 (db.js)
// ==========================================

const DB_NAME = "TaskTimerDB";
const DB_VERSION = 1;
const STORE_NAME = "tasks";

let dbInstance = null; // 暫存開啟後的資料庫連線,避免重複開啟

/**
 * 1. 開啟資料庫 (Open Database)
 */
function openDB() {
  return new Promise((resolve, reject) => {
    // 如果已經連線過,直接回傳連線,不用重複開啟
    if (dbInstance) {
      resolve(dbInstance);
      return;
    }

    const request = indexedDB.open(DB_NAME, DB_VERSION);

    // 首次建立資料庫或升級版本時觸發(用來建表)
    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        // 建立名為 tasks 的資料表,並以 'id' 當作主鍵
        db.createObjectStore(STORE_NAME, { keyPath: "id" });
      }
    };

    // 成功開啟資料庫
    request.onsuccess = (event) => {
      dbInstance = event.target.result;
      resolve(dbInstance); // 代表成功完成,回傳 db 連線
    };

    // 開啟失敗
    request.onerror = (event) => {
      console.error("IndexedDB 開啟失敗:", event.target.error);
      reject(event.target.error);
    };
  });
}

重點解析:

  • onupgradeneeded:只有在資料庫首次建立,或 DB_VERSION 升級時才會觸發,是建立 Object Store 的唯一時機。

  • keyPath: "id":指定每筆任務物件中的 id 欄位作為唯一主鍵。


步驟 2:實作取得所有任務 (getAllTasksFromDB)

接著封裝讀取邏輯。因為 IndexedDB 是非同步操作,我們透過 await openDB() 拿到連線後,開啟唯讀交易(readonly)並回傳 Promise 物件。

/**
 * 2. 讀取所有任務資料 (Read)
 */
async function getAllTasksFromDB() {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(STORE_NAME, "readonly");
    const store = transaction.objectStore(STORE_NAME);
    const request = store.getAll();

    request.onsuccess = () => resolve(request.result || []);
    request.onerror = (e) => reject(e.target.error);
  });
}

重點解析:

  • store.getAll():直接撈出 tasks 資料表內的所有資料。如果資料庫還沒有任何資料,則回傳空陣列 [] 作為預設值。

步驟 3:實作覆寫與儲存任務 (saveAllTasksToDB)

最後是同步最新資料到資料庫的函式。這裡採用「全量覆寫」策略:先清空整個 Object Store,再將畫面上最新狀態的任務陣列批次寫入。

/**
 * 3. 儲存/更新所有任務資料 (Save / Update)
 * 當畫面的 tasks 陣列有變動時,呼叫此函式將最新資料寫入瀏覽器
 */
async function saveAllTasksToDB(tasksArray) {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(STORE_NAME, "readwrite");
    const store = transaction.objectStore(STORE_NAME);

    // 先清空舊資料,再寫入最新的整包 tasks 陣列
    const clearRequest = store.clear();

    clearRequest.onsuccess = () => {
      tasksArray.forEach((task) => {
        store.put(task); // put 會自動新增或覆蓋
      });
    };

    // 整筆交易成功完成時觸發
    transaction.oncomplete = () => resolve(true);
    transaction.onerror = (e) => reject(e.target.error);
  });
}

重點解析:

  • readwrite 交易類型:執行寫入、更新與刪除時必須宣告為 readwrite。

  • transaction.oncomplete:IndexedDB 的交易會在所有寫入操作完成後觸發 oncomplete,在這裡 resolve(true) 才能確保資料已真正寫入磁碟。


重點小結

  1. 使用簡潔的函式名稱:把所有底層複雜的交易(Transaction)與事件監聽通通藏在 db.js 內部,對外只看得見簡潔的函式名稱。

  2. 單例模式(Singleton):利用 dbInstance 記錄連線狀態,不用每次存資料都重新開啟一次資料庫,執行效能更好。

  3. 完美對接 TaskTimer 的全域 State:因為 TaskTimer 採用單一 tasks 陣列驅動畫面,所以提供 saveAllTasksToDB() 可以安心地將整包 State 直接同步存進瀏覽器。


(參考資料:Mdn - Using IndexedDB)


上一篇
DAY 22|Local-First 架構(一):什麼是 Local-First?為什麼選擇使用 IndexedDB?
下一篇
DAY 24|Local-First 架構(三):整合資料到 IndexedDB
系列文
從 UI/UX 設計到前端實作:30 天打造專屬的任務學習計時器 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言