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

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

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

將應用程式狀態儲存在 IndexedDB 中,是能加快重複造訪載入時間的好方法。然後應用程式可以在背景與任何 API 服務同步,並採用「過時時重新驗證」策略,延遲更新 UI 與新資料。

使用 IndexedDB 的另一個好處是儲存使用者原創內容,例如在檔案上傳到伺服器之前的暫存儲存庫,或做為遠端資料的用戶端快取,當然也可以兩者並用。

不過,使用 IndexedDB 時,有許多需要注意的事項,但對 API 新手開發人員可能無法立即察覺。本文將回答常見問題,並探討在 IndexedDB 中保存資料時,需要注意的幾個重要事項。

確保應用程式符合預期

IndexedDB 非常複雜,因為您 (開發人員) 無法控管這麼多因素。本節說明使用 IndexedDB 時必須留意的許多問題。

並非所有平台都能儲存在 IndexedDB 中

如果要儲存由使用者產生的大型檔案 (例如圖片或影片),可以試著將這些檔案儲存為 FileBlob 物件。這個方法可在部分平台上執行,但在其他平台上卻無效。特別是 iOS 上的 Safari,無法將 Blob 儲存在 IndexedDB 中。

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

但請注意,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 時可能會因為各種原因而發生錯誤,在某些情況下,原因可能會讓開發人員無法控管。舉例來說,部分瀏覽器目前不允許在私密瀏覽模式下寫入已建立索引的資料庫。此外,使用者也可能會安裝記憶體即將用盡的裝置,瀏覽器也會禁止您儲存任何內容。

因此,請務必一律在索引資料庫程式碼中導入正確的錯誤處理機制。這也表示除了儲存應用程式狀態外,也建議將應用程式狀態保留在記憶體中,因此在私密瀏覽模式下執行,或儲存空間無法使用時,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 儲存物件時,必須先為該物件建立結構化本機副本,而結構化複製程序是在主要執行緒上進行。物件越大,封鎖時間越長。

規劃如何將應用程式狀態保存至索引資料庫時可能會遇到一些挑戰,因為大部分的熱門狀態管理程式庫 (例如 Redux) 的做法是以單一 JavaScript 物件的形式管理整個狀態樹狀結構。

雖然採用這種方式管理狀態可帶來許多好處 (例如讓程式碼易於推斷及偵錯),雖然只是將整個狀態樹狀結構儲存為 IndexedDB 中的單一記錄,雖然相當方便,,但在每次變更之後執行此操作 (即使發生節流/去退彈的情形),可能會導致瀏覽器分頁遭到非必要封鎖,甚至導致瀏覽器無法回應。

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

如果在 IndexedDB 中儲存圖片、音樂或影片等大型項目,也是如此。請為每個項目儲存各自的金鑰,而非在較大的物件中儲存資料,這樣在擷取結構化資料時,就不必另外支付擷取二進位檔案的費用。

就跟大部分的最佳做法一樣,這不是孤注一擲的規則。如果無法拆解狀態物件,請只編寫最低限度的變更集,將資料拆分為子樹狀結構,且最好還是只寫入整個狀態樹狀結構。稍微改進就是沒有改進。

最後,請一律針對您撰寫的程式碼評估效能影響。儘管小規模寫入 IndexedDB 的效能確實優於大型寫入,但只有在應用程式正在執行的索引資料庫寫入作業實際上導致封鎖主執行緒並降低使用者體驗的長時間工作時,才有意義。請務必評估自己的最佳化目標

結論

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

雖然正確使用 IndexedDB 可以大幅改善使用者體驗,但若是不當使用或無法處理錯誤情況,可能會導致應用程式無法正常運作,使用者也會感到不滿。

由於用戶端儲存空間涉及無法控制的許多因素,因此程式碼必須經過充分測試,並妥善處理錯誤,即使是最初看似不可能發生的錯誤也一樣。