在 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。
原本在 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();
現在已經可以在開網頁時讀取資料庫了,接下來我們要在每一次資料變動時,將最新的 tasks 狀態非同步寫回 IndexedDB。
不需要在每個 CRUD 函式裡重寫 IndexedDB 語法,只要在更新記憶體陣列後,補上一行 await saveAllTasksToDB(tasks) 即可!
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;
}
// 快速切換狀態
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();
}
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();
}
在上方程式碼都寫完之後,可以開啟瀏覽器的開發者工具(F12)進行驗證:
切換至 Application 頁籤 → 點擊左側 IndexedDB → TaskTimerDB → tasks。
在網頁畫面上新增一個主任務,或是進行一次計時並儲存。
點擊 DevTools 內部的 Refresh (重新整理圖示),會看到資料庫即時出現剛才新增的資料內容。
按下 F5 重新整理網頁,甚至切換成離線模式(Offline),所有任務與歷史計時紀錄依然完好呈現。
