iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Modern Web

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

DAY 24|Local-First 架構(三):整合資料到 IndexedDB

  • 分享至 

  • xImage
  •  

在 Day 22 介紹了 Local-First 的核心理念,並在 Day 23 成功用 Promise 包裝出獨立的 db.js 資料庫工具模組。

今天要來進行整合實作,將原本存在記憶體(Memory)中的 tasks 全域變數,與 IndexedDB 進行連線,實現 「重新整理頁面資料不遺失、無網路環境也能完好運作」 的本地化體驗。


整合邏輯規劃:資料是如何流動的?

[網頁首次載入]
       │
       ▼
 呼叫 initDB() ──► 嘗試從 IndexedDB 讀取資料
       │
       ├──► 若 DB 有資料 ──► 覆蓋記憶體中的 tasks 陣列 ──► 繪製畫面 renderAll()
       │
       └──► 若 DB 為空   ──► 寫入預設 Demo 資料到 DB   ──► 繪製畫面 renderAll()

[使用者操作 CRUD / 儲存計時]
       │
       ▼
更新記憶體 tasks ──► 呼叫 saveAllTasksToDB(tasks) ──► 畫面重新渲染 renderAll()

這樣的設計讓前端保持 「單一資料源(Single Source of Truth)」:畫面的渲染依然依賴記憶體中的 tasks,但只要資料一有變動,就會同步(Sync)寫入 IndexedDB。


步驟 1:重構網頁初始化流程

原本在 all.js 的最後一行,我們是直接呼叫 renderAll()。現在改為非同步的初始化函式 initApp():

// all.js

// 應用程式非同步初始化
async function initApp() {
  try {
    // 1. 從 IndexedDB 讀取舊資料
    const savedTasks = await getAllTasksFromDB();

    if (savedTasks && savedTasks.length > 0) {
      // 2. 如果資料庫裡有紀錄,用資料庫的內容覆蓋全域 State
      tasks = savedTasks;
    } else {
      // 3. 若為首次使用(DB 為空),將預設的 Demo 資料寫入 IndexedDB
      await saveAllTasksToDB(tasks);
    }

    // 4. 校正選取的預設任務 ID (避免舊 ID 找不到)
    currentMainTaskId = tasks[0]?.id || null;
    const currentTask = tasks.find((t) => t.id === currentMainTaskId);
    currentSubtaskId = currentTask?.subtasks.find((s) => s.status !== "completed")?.id || currentTask?.subtasks[0]?.id || null;
    modalSelectedParentId = currentMainTaskId;

  } catch (error) {
    console.error("初始化資料庫失敗:", error);
    showToast("本地資料載入失敗,將使用暫存模式", true);
  } finally {
    // 5. 不管成功或失敗,最後都進行畫面繪製
    renderAll();
  }
}

// 將原本的 renderAll() 替換為初始化函式
initApp();

步驟 2:在 CRUD 動作中加入資料庫同步

現在已經可以在開網頁時讀取資料庫了,接下來我們要在每一次資料變動時,將最新的 tasks 狀態非同步寫回 IndexedDB。

不需要在每個 CRUD 函式裡重寫 IndexedDB 語法,只要在更新記憶體陣列後,補上一行 await saveAllTasksToDB(tasks) 即可!

1. 新增主任務 / 子任務時同步

async function addMainTask(title) {
  const cleanTitle = title.trim();
  if (!cleanTitle) return false;

  // ... (檢查重複名稱邏輯保持不變) ...

  tasks.push({
    id: `task-${Date.now()}`,
    title: cleanTitle,
    subtasks: [],
  });

  // 同步存入 IndexedDB
  await saveAllTasksToDB(tasks);

  showToast(`已成功新增主任務「${cleanTitle}」`);
  renderAll();
  return true;
}

2. 切換任務狀態與刪除時同步

// 快速切換狀態
async function changeSubtaskStatus(parentTaskId, subtaskId, newStatus) {
  // ... (尋找 task 與 subtask 邏輯保持不變) ...

  sub.status = newStatus;

  // 同步存入 IndexedDB
  await saveAllTasksToDB(tasks);

  showToast(`「${sub.title}」狀態已更新為:${STATUS_MAP[sub.status].label}`);
  renderAll();
}

// 刪除子任務
async function deleteSubtask(parentTaskId, subtaskId) {
  // ... (二次確認提示 confirm 邏輯保持不變) ...

  parentTask.subtasks = parentTask.subtasks.filter((s) => s.id !== subtaskId);

  // 同步存入 IndexedDB
  await saveAllTasksToDB(tasks);

  showToast("已刪除子任務", true);
  renderAll();
}

3. 計時結束儲存紀錄時同步

async function saveSession() {
  const currentTask = tasks.find((t) => t.id === currentMainTaskId);
  const currentSub = currentTask?.subtasks.find((s) => s.id === currentSubtaskId);

  if (currentSub) {
    // ... (新增 record 與更新狀態邏輯保持不變) ...

    // 同步存入 IndexedDB
    await saveAllTasksToDB(tasks);
  }

  if (saveTimerModal) saveTimerModal.hide();
  resetTimerUI();
  showToast("已成功儲存本次計時!");
  renderAll();
}

成果驗證:利用 Chrome DevTools 檢查

在上方程式碼都寫完之後,可以開啟瀏覽器的開發者工具(F12)進行驗證:

  1. 切換至 Application 頁籤 → 點擊左側 IndexedDB → TaskTimerDB → tasks。

  2. 在網頁畫面上新增一個主任務,或是進行一次計時並儲存。

  3. 點擊 DevTools 內部的 Refresh (重新整理圖示),會看到資料庫即時出現剛才新增的資料內容。

  4. 按下 F5 重新整理網頁,甚至切換成離線模式(Offline),所有任務與歷史計時紀錄依然完好呈現。


DevTools 檢查


上一篇
DAY 23|Local-First 架構(二):實作 Promise 版 IndexedDB 工具模組
系列文
從 UI/UX 設計到前端實作:30 天打造專屬的任務學習計時器 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言