IndexedDB 是 W3C 於 2015 年 1 月定案的前端大量資料暫存方案,其架構足以讓網頁應用程式在前端儲存整個跟當前使用者有關的所有資料表,以便應用程式可以利用前端資料快取來增加效率、減少網路傳輸量,或者甚至達到離線使用的目的。不過這個東西使用起來真的跟大部分工程師熟悉的 SQL 資料庫不太一樣;這篇文章來簡述幾個我的初學心得。

1. 它是一個 NoSQL 資料庫

因此,IndexedDB 沒有辦法下達像 SQL 資料庫那樣複雜的查詢語言來精確撈出資料;取而代之地,它很仰賴對資料庫建立索引、以便可以利用資料的不同欄位進行檢索。但是即使下達了一堆的索引,IndexedDB 在查找的時候最厲害也就只是針對鍵值的一個範圍來撈出資料而已。因此大部份比較複雜的資料篩選還是得在把資料讀到記憶體之後再來操作,撈資料只能粗略的撈。

2. 它所有的資料操作都是非同步的

不只如此,原生的 IndexedDB API 竟然走的是事件導向的非同步機制(非常不巧地,Promise 這個東西是 ECMAScript 2015 加入的,而其發布是在那年的六月,剛好比 IndexedDB 晚了一些,所以 IndexedDB 推出的時候還沒有 Promise 這回事),這導致要撰寫原生的 IndexedDB 程式是非常痛苦的一件事。幸好,在這幾年之間我們陸續有了一大堆 IndexedDB 的函式庫可以用,像是 idbDexie.js 等等,而這些函式庫大多都把 IndexedDB 包裝成了 Promise 導向的非同步了,寫起來輕鬆很多,但是仍舊必須記得要 await 一切的資料操作。

3. 它的交易是沒有辦法持續開啟的

這個設計真的有點詭異:IndexedDB 的交易(transaction)只要當前的工作佇列清空了就會自動認可(commit)並且關閉,所以沒有任何的辦法可以維持交易持續開啟、並且在中間執行一些需要等候的非同步操作。在撰寫程式流程的時候一定要考慮到這一點;只有中途出現了需要等候的非同步操作,那麼之前之後的資料庫操作就一定得拆成兩次交易來進行。這在某些情境之下還真的有點不方便,因為我們沒有便捷的方法可以在中間的非同步操作失敗的時候直接替之前的交易做恢復(rollback),所以要嘛自己想辦法實作這種恢復,要嘛就得在流程設計上避免這種需求產生。

在多數情境中,後者通常是做得到的,其概念大致上是把全部的操作分成三個步驟:

  1. 進行一次唯讀的交易,把一些要用到的資料讀取出來。
  2. 利用前一個步驟讀出來的資料進行需要等候的非同步操作。
  3. 進行一次讀寫的交易,開始根據前一個步驟的執行結果來更動資料;在交易的一開始可以先檢查步驟 1. 讀取出來的那些資料是否曾經在步驟 2. 的等候時間裡面發生變化,視需求而定可以在有發生變化的情況下重新來過或者捨棄交易。

另外也可以設法把資料庫鎖定住,使得進行步驟 2. 的時候別的程式不會同時也去寫入資料庫,以避免步驟 3. 中的麻煩。辦法大致是這樣的:

// 設置一個 Promise 來代表「等待上一次 Process 執行完畢」
// 它的初始值為 Promise.resolve(),亦即不用等
var lock = Promise.resolve();

// 要執行的時候執行這個
async function Run() {
    // 等待前一個 lock
    await lock;

    // 設置新的 lock
    let resolve;
    lock = new Promise(res => resolve = res);

    try {
        // 執行並傳回該傳回的東西
        retrun await Process();
    } finally {
        // 不管 Process 有沒有出錯都把 lock 正常 resolve,
        // 如此一來別的等候 lock 的程序才不會出錯
        resolve();
    }
}

async function Process() {
    // 上面所說的步驟 123
    return ...;
}

然後只要在其它任何會進行資料庫寫入的地方、都在前面加上 await lock 即可。如此一來,只要 Process 正在被執行的時候,其它所有的寫入操作都會等它執行完了才寫入。

4. 它的交易取消一定要明確呼叫

如果交易過程中發生任何錯誤,我們都必須明確地捕捉例外並且手動呼叫 IDBTransaction.abort() 方法(這是一個同步的方法,因為它不算資料操作)。這和前一點加起來真的和大部分的資料庫的邏輯不太一樣:多數資料庫都是除非明確呼叫認可、不然不會自己自動認可交易,更不用說是丟出例外的場合,但是 IndexedDB 卻是完全相反:如果沒有明確呼叫取消,那麼當工作佇列清空的時候,交易仍舊是會被自動認可下去,即使中途曾經丟出例外也一樣。

分享此頁至:
最後修改日期: 2020/06/15

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。