隨著前端應用程式規模越來越大,想在瀏覽器上儲存應用程式狀態的需求也就跟著複雜起來,這時就會想以資料庫操作的概念來操作。就現今而言,有多種方案可以選擇,我們可以試著從使用localStorage模擬、WebSQL到IndexedDB的探討,來協助釐清需求、選擇適當的方案。

使用localStorage模擬資料庫

我在先前專欄〈從Cookie到Storage〉中談到,Storage API提供了大小約5MB(因瀏覽器而異)的儲存空間,對於不用在每次請求中發送給伺服端的客戶端狀態,可以使用Storage儲存,Storage儲存的資料是鍵值成對,而且都只能是字串,Storage實例setItem、getItem等API本身也是同步的,這是因為,Storage本來就是為了簡單的資料而設計。

然而,應用程式的狀態,有時必須以更複雜的結構來表示,若要在localStorage儲存這類複雜結構,就必須實作物件與字串之間的轉換。

例如,localStorage值的部份若想儲存物件,我們可以透過JSON.stringify將物件序列化為JSON再儲存,要從localStorage取回物件,就是將儲存的字串透過JSON.parse剖析為物件(當然基於JSON格式的限制,這種方式直接儲存二進位等格式)。

既然可以用這種方式儲存複雜的結構,就有開發者想到,可不可以用localStorage模擬資料庫?將物件的序列化、反序列化細節隱藏起來,實現像是建立表格、查詢表格、查找欄位、修改資料等操作。

實際上,已經有些程式庫,實現了這類需求,例如,localStorageDB(https://bit.ly/2kgVOjP),就提供了createTable、insert、queryAll、update、deleteRows等,這些是從語義上貼近關聯式資料庫觀點的API。

有趣的事實是,瀏覽器在儲存localStorage的資料時,實際上就會使用資料庫。例如,Firefox實現這類作法時,會使用SQLite檔案來儲存,當然,基於Storage API來模擬資料庫的相關程式庫,本質上並非關聯式資料庫,比較像是模擬NoSQL資料庫,不能使用SQL查詢,也沒有索引、交易等機制。

曾經的Web SQL Database

有些專案試圖讓瀏覽器使用真正的資料庫,例如sql.js(https://bit.ly/2kgXkm1),它將SQLite的C語言程式碼搭配 Emscripten ,轉化為WebAssembly,可運行在記憶體之中,允許匯入SQLite檔案,或者是將資料庫匯出為JavaScript的Typed Array;甚至也有個Alasql.js(https://bit.ly/1PGZ007),使用純JavaScript實現了SQL資料庫。

客戶端資料庫的需求一直都在,而W3C曾經提出Web SQL Database草案(https://bit.ly/2kgYuxT),試圖規範一組資料庫API,以便瀏覽器廠商實現客戶端的資料庫方案。這組API是非同步的,結合JavaScript的回呼函式風格,並且支援交易。

而且,不少文件都簡稱此規範為Web SQL,不過這名稱應該是草案中的SQL語句規範,實現時,必須基於SQLite 3.6.19的SQL方言。目前,Chrome、Safari和Opera等瀏覽器,都曾實現了Web SQL Database的支援。

瀏覽器本身有一些內部的功能,實際上也會用到資料庫(例如之前談到的,使用SQLite作為localStorage的實現),但問題在於,支援Web SQL Database的瀏覽器,都採用了SQLite來實現,沒有廠商想要自行實作資料庫引擎,因此,Web SQL Database看來就只是個SQLite的包裹API罷了。

這也使得Web SQL Database規範的推進,陷入了僵局。畢竟SQLite是個產品,不是一個標準,而作為標準規範,不應依賴在特定產品;另一方面,就算有廠商打算實現Web SQL Database,也有可能出現不同的SQL方言,如同現今後端使用各家資料庫產品時,總要與不同的SQL方言為伍一樣。所以,在標準化的過程中,由於缺少多樣的實作品來推動規範,以及種種考量之下(像是使用SQL也有ORM方面的問題),W3C在2010年11月終止了Web SQL Database草案。

Indexed Database API

W3C在2015年1月發布了Indexed Database API(https://bit.ly/2jY9zDD),作為瀏覽器實現本地資料庫的規範,然而,訴求的對象並非關聯式資料庫,同時,也沒有SQL語句的支援,而像是NoSQL資料庫的規範,目前來說,主流的瀏覽器多有支援(IE11部份支援,可參考https://caniuse.com/#feat=indexeddb),2018年1月更進一步發布了Indexed Database API 2.0。

由於並非規範關聯式資料庫,Indexed Database API有自己的概念名詞,例如,不使用表格,而是使用物件倉庫(Object store)──顧名思義,倉庫用來儲存物件,必須使用createObjectStore建立倉庫,並以keyPath指定的名稱作為主鍵來識別各個物件,可儲存的資料類型不限於字串,也可儲存ArrayBuffer、Blob等二進位資料實例。

在容量方面,儘管因瀏覽器(與平臺)的儲存空間策略而有所不同,然而基本上會比localStorage大上許多,因為是針對大量資料儲存而設計,API設計上採用非同步風格,藉由不同的事件註冊來處理操作結果。例如查詢一筆資料的話:

let tx = db.transaction(['customers']);
let objectStore = tx.objectStore('customers');
let request = objectStore.get(1);
request.onsuccess = function(event) {
// 從 request.result 取得結果
};

IndexedDB的增、刪、查找等,都要在交易中進行,而交易有三種類型,讀取、讀寫,以及版本變更。如果發起交易後,沒有請求且回到事件循環中,交易就會自動結束。交易本身也提供error、abort和complete事件,若發生錯誤未處理,或者呼叫abort方法,交易將取消、復原。

而且,IndexedDB具有索引的概念,我們可以使用createIndex在倉庫中建立索引,用來在倉庫中指定索引查找資料,而不是只靠物件的主鍵進行查找,這就像是查找關聯式資料庫表格中某個欄位,建立索引時,還可以施加限制,例如設定值是否必須唯一(unique)。

關於IndexedDB的API使用,有更多的細節,我們在網路上可以找到不少文件,若想有個出發點,可以從MDN上的〈IndexedDB基礎概念〉(https://mzl.la/2ktpUjR),以及〈使用IndexedDB〉(https://mzl.la/2lCvAbr)開始。事實上,IndexedDB本身是個低階API,採用傳統的回呼模式,像idb(https://bit.ly/2jYl8L3)這類程式庫,以Promise封裝了IndexedDB,可以搭配async、await來使用,獲得更好的操作風格。

客戶端儲存的考量

談到「資料庫」的需求,其實是個籠統的概念,更多的時候,需要的只是個客戶端儲存方案,而當下要考量的點,可能是資料模型(特定結構、鍵值或者是位元組)、儲存的方式、瀏覽器支援度、API風格等。而在簡單的需求下,或許localStorageDB之類的模擬程式庫,就可以解決需求。

能夠考量的方案,也不僅有以上的介紹,有興趣的話,你可以看看〈How to choose the proper storage API〉,其中還包含了File System、Cache API的比較。在API的封裝上,有個〈Storage libraries〉(https://bit.ly/2lBdcje)列出了十幾個程式庫,必要時,可以逐一調查,看看在儲存方案上,這些程式庫各自的特色,如此也有助於釐清自身需求。

作者簡介


Advertisement

更多 iThome相關內容