打造離線優先 (Offline-First) 應用:IndexedDB 與資料同步機制的設計
探討 DueWise 如何在網路不穩定的環境下依然保持數據可用。深入 IndexedDB 實作、衝突處理解決策略以及雲端同步邏輯。
打造離線優先 (Offline-First) 應用:IndexedDB 與資料同步機制的設計
「如果您在沒有訊號的超市地下室,還能查到家裡的牛奶是否過期嗎?」對於 DueWise 來說,這個答案必須是「是的」。這就是 Offline-First (離線優先) 架構的價值所在。
在 DueWise 中,雲端不是唯一的數據源,它更像是一個備份與多裝置同步的終端。這篇文章將介紹我們如何利用 IndexedDB 構建強大的本地持久層。
1. 核心觀念:為什麼不是 LocalStorage?
雖然 localStorage 使用簡單,但對於 DueWise 而言它有三大缺陷:
- 容量限制: 通常只有 5MB,難以存放大量的物品圖片快取或歷史紀錄。
- 效能瓶頸: 它是同步的,大量讀寫會阻塞 UI 主執行緒。
- 結構化查詢: 難以進行「查詢所有下週過期的物品」這類複雜檢索。
因此,我們選擇了 IndexedDB。它是瀏覽器內建的、支持事務 (Transactions) 與索引的異步資料庫。
2. 實作細節:Zustand 與 IndexedDB 的連動
我們並未直接操作低階的 IndexedDB API,而是封裝了一個簡易的 Store。在 DueWise 中,數據流如下:
- 讀取: App 啟動時,從 IndexedDB 載入數據到 Zustand
ItemsStore。 - 寫入: 用戶操作後,同時更新 Zustand(更新 UI)與 IndexedDB(確保持久化)。
寫入範例:
const saveItemLocal = async (item) => {
const db = await openDB("DueWiseDB", 1);
const tx = db.transaction("items", "readwrite");
await tx.objectStore("items").put(item);
await tx.done;
};
3. 同步機制:解決雲端與在地的衝突
這是離線架構中最具技術含量的地方。當用戶在離線時刪除了一個項目,而在連線後另一台裝置又修改了同一個項目,該聽誰的?
DueWise 採用了 「最後寫入者勝 (Last Write Wins)」 策略,輔以 updated_at 時間戳。
同步邏輯流程:
- 標記變更: 在本地 IndexedDB 中,我們有一個
_status欄位。新增或修改的項目會被標記為pending。 - 背景同步: 當瀏覽器的
online事件觸發時,App 會自動掃描所有pending項目。 - 批次上傳: 呼叫我們之前提到的 Cloudflare D1 Batch API。
- 下發更新: 同步完成後,後端返回伺服器端的最新數據,App 刷新本地數據。
4. 針對大數據量的優化:分頁與懶加載
隨著用戶累積的物品變多(例如超過 500 件),全量同步會消耗不必要的流量與效能。
我們實作了差異同步 (Delta Sync):
- App 會記錄本地最後一次同步的時間。
- 請求 API 時帶上
last_sync_time。 - D1 僅返回該時間點之後發生變更的數據。
這讓 DueWise 的啟動速度即使在數據量龐大時也能維持在 1 秒以內。
5. 無網路時的 UI 提示:樂觀 UI (Optimistic UI)
離線優先不僅僅是技術實現,更是一種 UX 設計。
在 DueWise 中,我們不會因為沒網路就轉圈圈。用戶可以流利地切換分頁、新增項目。我們會透過一個小小的狀態圖示(例如:雲朵斜線)提醒用戶:「目前為離線模式,數據將於連線後同步」。
這種「不打斷」的體驗,讓用戶覺得 App 極其穩定可靠。
個人心得:離線能力是 PWA 的護城河
現在的 Web 開發者往往過於依賴強大的網路。但真實的世界充斥著訊號死角。具備離線能力的應用,不僅能在極端環境工作,它更是提升「感知速度」的絕佳手段。
當你的 App 不再需要等待網路請求就能顯示內容時,那種「 instant 」的感覺,是任何優化 API 效能的努力都換不來的。
結論
透過 IndexedDB 實現的本地持久層,結合 D1 的雲端備份,DueWise 成功達成了一個平衡:既有 Web 的分發便利,又有原生 App 的韌性。
下一篇文章,我們將探討前端視覺的演進:「Tailwind CSS 4.0 現代化排版:從變數系統到高性能動畫的視覺實踐」。