使用索引資料庫的最佳做法

瞭解在 IndexedDB 為熱門狀態管理程式庫之間同步處理應用程式狀態的最佳做法。

使用者初次載入網站或應用程式時,建構用來轉譯 UI 的初始應用程式狀態通常需要大量工作。舉例來說,有時應用程式需要在用戶端驗證使用者,然後發出數個 API 要求,才能取得需要在頁面上顯示的所有資料。

將應用程式狀態儲存在 IndexedDB 中,可以有效提升重複造訪的載入時間。接著,應用程式便可在背景與任何 API 服務同步,並延遲更新 UI,採取過時的重新驗證策略。

IndexedDB 的另一個優點是儲存使用者產生的內容,在上傳到伺服器之前做為暫存儲存庫,或做為遠端資料的用戶端快取,當然或兩者都有。

不過,使用 IndexedDB 時有許多需要考量的重要事項,對於剛開始使用 API 的開發人員可能無法立即明顯瞭解。本文會解答常見問題,並探討在 IndexedDB 中保留資料時應留意的幾個重點。

讓您的應用程式容易預測

許多有關 IndexedDB 的複雜性,都是因為您 (開發人員) 無法控管的許多因素。本節將探討使用 IndexedDB 時必須留意的許多問題。

並非所有平台上的內容都能儲存在 IndexedDB 中

如果要儲存使用者產生的大型檔案 (例如圖片或影片),您可以嘗試將其儲存為 FileBlob 物件。這適用於部分平台,但在其他平台上無法運作。特別是 iOS 版 Safari 無法在 IndexedDB 中儲存 Blob

幸好,將 Blob 轉換為 ArrayBuffer 並不困難,反之亦然。非常支援在 IndexedDB 中儲存 ArrayBuffer

但請注意,Blob 具有 MIME 類型,而 ArrayBuffer 則沒有。您需要將類型連同緩衝區一起儲存,才能正確執行轉換作業。

如要將 ArrayBuffer 轉換為 Blob,只需使用 Blob 建構函式即可。

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

另一種方向比較複雜,並且為非同步程序。您可以使用 FileReader 物件,將 blob 讀取為 ArrayBuffer。讀取完成後,會在讀取器上觸發 loadend 事件。您可以將這個程序納入 Promise 中,如下所示:

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

寫入儲存空間可能失敗

寫入 IndexedDB 時發生錯誤的原因有很多,且在某些情況下,這些原因並非開發人員可控管。舉例來說,某些瀏覽器目前不允許在私密瀏覽模式下寫入 IndexedDB。使用者裝置的磁碟空間可能會即將用盡,瀏覽器會禁止您儲存任何內容。

因此,請務必一律在索引資料庫程式碼中採用適當的錯誤處理機制。這也表示,通常建議將應用程式狀態保留在記憶體中 (除了儲存狀態以外),這樣在私密瀏覽模式下執行或儲存空間無法使用時,UI 不會中斷 (即使其他需要儲存空間的應用程式功能無法運作)。

您可以在建立 IDBDatabaseIDBTransactionIDBRequest 物件時,為 error 事件新增事件處理常式,藉此擷取 IndexedDB 作業中的錯誤。

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

儲存的資料可能已遭使用者修改或刪除

有別於伺服器端資料庫,您可以限制未經授權的存取行為,但用戶端資料庫可以存取瀏覽器擴充功能和開發人員工具,而且使用者可以清除這些資料庫。

雖然使用者可能不常修改儲存在本機的資料,但使用者經常會清除資料。請務必讓您的應用程式能夠處理這兩種情況,而不會發生錯誤。

儲存的資料可能已過時

與上一節類似,即便使用者並未自行修改資料,也可能是舊版程式碼寫入儲存空間中的資料,而這可能是內含錯誤的版本。

IndexedDB 內建對結構定義版本的支援,並透過 IDBOpenDBRequest.onupgradeneeded() 方法升級;但您仍需撰寫升級程式碼,以便處理來自舊版的使用者 (包括含有錯誤的版本)。

單元測試在此處相當實用,因為通常無法手動測試所有可能的升級路徑和案例。

維持應用程式效能

IndexedDB 的其中一項主要功能是非同步 API,但別讓人誤以為自己使用時不會擔心效能問題。但在許多情況下,效能低落仍可能會阻斷主執行緒,進而導致卡頓和沒有回應。

一般來說,對 IndexedDB 的讀取和寫入作業不應大於存取資料的所需大小。

雖然 IndexedDB 可以將大型巢狀物件儲存為單一記錄 (從開發人員的角度而言是相當方便的做法),但應該避免採用這種做法。這是因為 IndexedDB 儲存物件時,必須先建立該物件的結構化本機副本,而結構化複製程序會在主執行緒上發生。物件越大,封鎖時間越長。

這在規劃如何將應用程式狀態保留至 IndexedDB 時帶來一些挑戰,因為大部分的常用的狀態管理程式庫 (例如 Redux) 都能透過將整個狀態樹狀結構做為單一 JavaScript 物件來管理。

雖然以這種方式管理狀態有許多好處 (例如讓程式碼更容易理解及偵錯),而只是在 IndexedDB 中將整個狀態樹狀結構儲存為單一記錄,可能很讓使用者覺得方便且方便,但在每次變更之後進行這項操作 (即使受到節流/延遲化),也會導致瀏覽器無法寫入主要執行緒,甚至導致無法寫入主要執行緒,甚至造成瀏覽器寫入錯誤的可能性。

請不要將整個狀態樹狀結構儲存在單一記錄中,而應拆分為個別記錄,並只更新實際變更的記錄。

同樣的,如果您在 IndexedDB 中儲存圖片、音樂或影片等大型項目,也是一樣。請將每個項目都有其專屬金鑰,而非存放在較大的物件中,這樣您就能擷取結構化資料,而不用另外支付擷取二進位檔案的費用。

如同大多數最佳做法,這並不是萬無一失的規則。如果無法分割狀態物件並只寫入最低的變更集,請將資料拆分為子樹狀結構,而只寫入整個狀態樹狀結構仍建議寫入整個狀態樹狀結構。有些微改進比完全沒有改善。

最後,您應一律應評估您編寫的程式碼對效能的影響。雖然對索引資料庫進行少量寫入作業的執行成效優於大型寫入作業,但只有在應用程式執行的 IndexedDB 寫入作業確實導致封鎖主執行緒並降級使用者體驗時,才需要留意這一點。請務必進行評估,才能瞭解自己要最佳化的項目

結論

開發人員可以利用 IndexedDB 等用戶端儲存機制,讓各個工作階段保持狀態,還能減少重複造訪時載入初始狀態所需的時間,進而改善應用程式的使用者體驗。

正確使用 IndexedDB 可以大幅提升使用者體驗,但若使用錯誤或無法處理錯誤情況,可能會導致應用程式異常終止,並讓使用者感到不滿。

由於用戶端儲存空間涉及許多無法控管的因素,因此請務必妥善測試程式碼,並妥善處理錯誤,即使一開始看似不太可能發生的錯誤也能妥善處理。