IndexedDB 打造靠譜 Web 離線資料庫
在知乎和我在平常工作中,常常會看到一個問題:
前端現在還火嗎?
這個我只想說:
隔岸觀火的人永遠無法明白起火的原因,只有置身風暴,才能找到風眼之所在 ——『秦時明月』
你 TM 看都不看前端現在的發展,怎麼去評判前端火不火,我該不該嘗試一下其他方面的內容呢?本人為啥為這麼熱衷於新的技術呢?主要原因在於,生怕會被某一項顛覆性的內容淘汰掉,從前沿領域掉隊下來。說句人話就是: 。所以本文會從頭剖析一下 在前端裡面的應用的發展。
indexedDB 目前在前端慢慢得到普及和應用。它正朝著前端離線資料庫技術的步伐前進。以前一開始是 manifest、localStorage、cookie 再到 webSQL,現在 indexedDB 逐漸被各大瀏覽器認可。我們也可以針對它來進行技術上創新的開發。比如,現在小視頻非常流行,那麼我們可以在用戶觀看時,通過 cacheStorage 緩存,然後利用 WebRTC 技術實現 P2P 分發的控制,不過需要注意,一定要合理利用大小,不然後果真的很嚴重。
indexedDB 的整體架構,是由一系列單獨的概念串聯而成,全部概念如下列表。一眼看去會發現沒有任何邏輯,不過,這裡我順手畫了一幅邏輯圖,中間會根據 函數 的調用而相互串聯起來。
IDBRequest
IDBFactory
IDBDatabase
IDBObjectStore
IDBIndex
IDBKeyRange
IDBCursor
IDBTransaction
整體邏輯圖如下:
TL;DR
下文主要介紹了 indexedDB 的基本概念,以及在實際應用中的實操代碼。
indexedDB 基礎概念。在 indexedDB 裡面會根據索引 index 來進行整體數據結構的劃分。
indexedDB 資料庫的更新是一個非常蛋疼的事情,因為,Web 的靈活性,你既需要做好向上版本的更新,也需要完善向下版本的容錯性。
indexedDB 高效索引機制,在內部,indexedDB 已經提供了 、 等高效的索引機制,推薦不要直接將所有數據都取回來,再進行篩選,而是直接利用 進行。
最後推薦幾個常用庫
離線存儲
IndexedDB 可以存儲非常多的數據,比如 Object,files,blobs 等,裡面的存儲結構是根據 Database 來進行存儲的。每個 DB 裡面可以有不同的 object stores。具體結構如下圖:
並且,我們可以給 設定相關特定的值,然後在索引的時候,可以直接通過 key 得到具體的內容。使用 IndexDB 需要注意,其遵循的是同域原則。
indexDB 基本概念
在 indexDB 中,有幾個基本的操作對象:
Database: 通過 方法直接打開,可以得到一個實例的 DB。每個頁面可以創建多個 DB,不過一般都是一個。
Object store: 這個就是 DB 裡面具體存儲的對象。這個可以對應於 SQL 裡面的 table 內容。其存儲的結構為:
index: 有點類似於外鏈,它本身是一種 Object store,主要是用來在本體的 store 中,索引另外 object store 裡面的數據。需要區別的是,key 和 index 是不一樣的。可以參考: index DEMO,mdn index。如下圖表示:
如下 code 為:
transaction: 事務其實就是一系列 CRUD 的集合內容。如果其中一個環節失敗了,那麼整個事務的處理都會被取消。例如:
cursor: 主要是用來遍歷 DB 裡面的數據內容。主要是通過 來進行控制。
如何使用 IndexDB
上面說了幾個基本的概念。那接下來我們實踐一下 IndexDB。實際上入門 IndexDB 就是做幾個基本的內容
打開資料庫表
設置指定的 primary Key
定義好索引的 index
前期搭建一個 IndexedDB 很簡單的代碼如下:
上面主要做了 3 件事:
打開資料庫表
新建 Store,並設置 primary Key
設置 index
打開資料庫表主要就是版本號和名字,沒有太多講的,我們直接從創建 store 開始吧。
創建 Object Store
使用的方法就是 IDBDatabase 上的 方法。
基本函數構造為:
keyPath: 用來設置主鍵的 key,具體區別可以參考下面的 keyPath 和 generator 的區別。
autoIncrement: 是否使用自增 key 的特性。
創建的 key 主要是為了保證,在數據插入時唯一性的標識。
不過,往往一個主鍵(key),是沒辦法很好的完成索引,在具體實踐時,就還需要輔鍵 (aid-key) 來完成輔助索引工作,這個在 IndexDB 就映射為 。
設置索引 index
在完成 PK(Primary key) 創建完畢後,為了更好的搜索性能我們還需要額外創建 。這裡可以直接使用:
indexName: 設置當前 index 的名字
property: 從存儲數據中,指明 index 所指的屬性。
其中,options 有三個選項:
unique: 當前 key 是否能重複 (最常用)
multiEntry: 設置當前的 property 為數組時,會給數組裡面每個元素都設置一個 index 值。
具體可以參考:MDN createIndex Prop 和 googleDeveloper Index。
增刪數據
在 IndexedDB 裡面進行數據的增刪,都需要在 中完成。而這個增刪數據,大家可以理解為一次 ,相當於在一個 裡面管理所有當前邏輯操作的 。所以,在正式開始進行數據操作之前,還需要給大家簡單介紹一些如果創建一個事務。
事務的創建
API,如下 [代碼1]。在創建時,你需要手動指定當前 transaction 是那種類型的操作,基本的內容有:
"readonly":只讀
"readwrite":讀寫
"versionchange":這個不能手動指定,會在 回調事件裡面自動創建。它可以用來修改現有 object store 的結構數據,比如 index 等。
你可以通過在資料庫打開之後,通過 上的 方法創建,如 [代碼2]。
事務在創建的時候不僅僅可以制定執行的模式,還可以指定本次事務能夠影響的 ObjectStore 範圍,具體細節就是在第一個 參數裡面傳入的是一個數據,然後通過 方法打開多個 OS 進行操作,如下 [代碼3]。
操作數據
完成了事務的創建之後,我們就可以正式的開始進行數據的交互操作了,也就是寫我們具體的業務邏輯。如下 [代碼1],一個完整數據事務的操作。
通過 回調得到的 IDBObjectStore 對象,我們就可以進行一些列的增刪查改操作了。可以參考 [代碼2]。詳細的可以參考文末的 。
索引數據
索引數據是所有資料庫裡面最重要的一個。這裡,我們可以使用游標,index 來做。例如,通過 index 來快速索引 key 值,參考 [代碼1]。
更詳細的內容,可以參考下文 數據索引方式。
keyPath 和 key Generator
何謂 keyPath 和 keyGenerator 應該算是 IndexedDB 裡面比較難以理解的概念。簡單來說,IndexedDB 在創建 Store 的時候,必須保證裡面的數據是唯一的,那麼得需要像其它資料庫一樣設置一個 來區分不同數據。而 keyPath 和 Generator 就是兩種不同的設置 key 的方式。
設置 keyPath
因為 ssn 在該數據集是唯一的,所以,我們可以利用它來作為 保證 的特性。或者,可以設置為自增的鍵值,比如 類似的。
使用 generator
generator 會每次在添加數據時,自動創建一個 unique value。這個 unique value 是和你的實際數據是分開的。裡面直接通過 來設置即可。
indexDB 打開注意事項
檢查是否支持 indexDB
版本更新: indexDB
在生成一個 indexDB 實例時,需要手動指定一個版本號。而最常用的
這樣會造成一個問題,比如上線過程中,用戶A第一次請求返回了新版本的網頁,連接了版本2。之後又刷新網頁命中了另一台未上線的機器,連接了舊版本1 出錯。主要原因是:
indexedDB API 中不允許資料庫中的數據倉庫在同一版本中發生變化. 並且當前 DB 版本不能和低版本的 version 連接。
比如,你一開始定義的 DB 版本內容為:
如果此時,用戶先打開了 version(1),但是後面,又得到的是 version(2) 版本的 HTML,這時就會出現 error 的錯誤。
參考:
版本更替
版本更新
這個在 IndexDB 是一個很重要的問題。主要原因在於
indexedDB API 中不允許資料庫中的數據倉庫在同一版本中發生變化. 並且當前 DB 版本不能和低版本的 version 連接。
上面就可以抽象為一個問題:
你什麼情況下需要更新 IndexDB 的版本呢?
該表資料庫裡面的 時。
你需要重新設計資料庫表結構時,比如新增 index
不過,如果直接修改版本號,會出現這樣一個 case:
由於原始 HTML 更新問題,用戶首先訪問的是版本 1 的 A 頁面,然後,訪問更新過後的 B 頁面。這時,IndexDB 成功更新為高版本。但是,用戶下次又命中了老版本的 A 頁面,此時 A 中還是連接低版本的 IndexDB ,就會報錯,導致你訪問失敗。
解決辦法就是,設置過濾,在 的時候,手動傳入版本號:
不過,這樣又會造成另外一個問題,即,數據遷移(老版本數據,不可能不要吧)。這裡,IndexDB 會有一個 updateCallback 給你觸發,你可以直接在裡面做相關的數據遷移處理。
在使用的時候,一定要注意 DB 版本的升級處理,比如有這樣一個 case,你的版本已經是 3,不過,你需要處理版本二的數據:
對於存在版本 2 資料庫的用戶來說是 OK 的,但是對於某些還沒有訪問過你資料庫的用戶來說,這無疑就報錯了。解決辦法有:
保留每個版本時,創建的欄位和 stores
在更新 callback 裡面,對處理的數據判斷是否存在即可。
在 Dexie.js DB 資料庫中,需要你保留每次 DB 創建的方法,實際上是通過 添加 swtich case ,來完成每個版本的更新:
如果遇到一個頁面打開,但是另外一個頁面拉取到新的代碼進行更新時,這個時候還需要將低版本 indexedDB 進行顯式的關閉。具體操作辦法就是監聽 事件,當版本升級時,通知當前 DB 進行關閉,然後在新的頁面進行更新操作。
最後,更新是還有幾個注意事項:
版本更新不能改變 primary key
回退代碼時,千萬注意版本是否已經更新。否則,只能增量更新,重新修改版本號來修復。
存儲加密特性
有時候,我們存儲時,想得到一個由一串 String 生成的 hash key,那在 Web 上應該如何實現呢?
這裡可以直接利用 Web 上已經實現的 WebCrypto,為了實現上述需求,我們可以直接利用裡面的 方法即可。這裡 MDN 上,已經有現成的辦法,我們直接使用即可。
參考:
WebCrypto 加密手段
存儲上限值
基本限制為:
逐出策略為:
參考:
存儲上限值瀏覽器內核存儲上限值處理
數據索引方式
在資料庫中除了基本的 CRUD 外,一個高效的索引架構,則是裡面的重中之重。在 indexedDB 中,我們一共可以通過三種方式來索引數據:
固定的 key 值
索引外鍵(index)
游標(cursor)
固定 key 索引
IDBObjectStore 提供給了我們直接通過 來索引數據,參考 [代碼1],這種方式需要我們一開始就知道目標的 內容。當然,也可以通過 全部索引數據。
比如,我們通過 primaryKey 得到一條具體的數據:
也可以 fetch 整個 Object Store 的數據。這些場景用處比較少,這裡就不過多講解。我們主要來了解一下 index 的索引方式。
index 索引
如果想要查詢某個數據,直接通過整個對象來進行遍歷的話,這樣做性能耗時是非常大的。如果我們結合 來將 key 加以分類,就可以很快速的實現指定數據的索引。這裡,我們可以直接利用 IDBObjectStore 上面的 方法來獲取指定 index 的值,具體方法可以參考 [代碼1]。
該方法會直接返回一個 IDBIndex 對象。這你也可以理解為一個類似 ObjectStore 的微型 index 數據內容。接著,我們可以使用 方法來獲得指定 index 的數據,參考[代碼2]。
使用 方法不管你的 index 是否是 的都會只會返回第一個數據。如果想得到多個數據的話,可以使用 來做。通過 得到的回調函數,直接通過 可以得到對應的 value 內容。
除了通過 得到所有數據外,還可以採用更高效的 方法遍歷得到的數據。
參考:
getAll() 和 openCursor 實例
游標索引
所謂的游標,大家心裡應該可以有一個初步的印象,就像我們物理尺子上的那個東西,可以自由的移動,來標識指向的對象內容。cursor 裡面有兩個核心的方法:
advance(count): 將當前游標位置向前移動 count 位置
continue(key): 將當前游標位置移動到指定 key 的位置,如果沒提供 key 則代表的移動下一個位置。
比如,我們使用 cursor 來遍歷 Object Store 的具體數據。
通常,游標可以用來遍歷兩個類型的數據,一個是 ObjectStore、一個是 Index。
Object.store: 如果在該對象上使用游標,那麼會根據 遍歷整個數據,注意,這裡不會存在重複的情況,因為 是唯一的。
index: 在 index 上使用游標的話,會以當前的 index 來進行遍歷,其中可能會存在重複的現象。
在 IDBObjectStore 對象上有兩種方法來打開游標:
openCursor: 遍歷的對象是 具體的數據值,最常用的方法
openKeyCursor: 遍歷的對象是 數據 key 值
這裡,我們通過 來直接打開一個 index 數據集,然後進行遍歷。
在游標中,還提供給了一個 和 方法,我們可以用它來進行數據的更新操作,否則的話就直接使用 ObjectStore 提供的 方法。
游標裡面我們還可以限定其遍歷的範圍和方向。這個設置是我們直接在 方法裡面傳參完成的,該方法的構造函數參考 [代碼1]。他裡面可以傳入兩個參數,第一個用來指定範圍,第二個用來指定 移動的方向。
如果需要對 cursor 設置範圍的話,就需要使用到 這個對象,使用樣板可以參考 [代碼2]。IDBKeyRange 裡面 key 參考的對象 因使用者的不同而不同。如果是針對 ObjectStore 的話,則是針對 primaryKey,如果是針對 Index 的話,則是針對當前的 indexKey
比如,我們這裡對 PersonIndex 設置一個 index 範圍,即,索引 在 和 之間的數據集合。
如果你還想設置遍歷的方向和是否排除重複數據,還可以根據 [代碼2] 的枚舉類型來設置。比如,在 [代碼3] 中,我們改變默認的 cursor 遍曆數據的方向為 ,從末尾開始。
事務讀取性能
在 indexDB 裡面的讀寫全部是基於 模式來的。也就是 IDBDataBase 裡面的 方法,如下 [代碼1]。所有的讀寫都可以比作在 作用域下的請求,只有當所有請求完成之後,該次 才會生效,否則就會拋出異常或者錯誤。 會根據監聽 error,abort,以及 complete 三個事件來完成整個事務的流程管理,參考[代碼2]。
例如:
你可以在 方法裡面手動傳入 或者其他表示事務的 參數,來表示本次事務你會進行如何的操作。IndexedDB 在初始設計時,就已經決定了它的性能問題。
只含有 readonly 模式的 transaction 可以並發進行執行含有 write 模式的 transaction 必須按照隊列 來 執行
這就意味著,如果你使用了 模式的話,那麼後續不管是不是 都必須等待該次 transaction 完成才行。
常用技巧
生成 id++ 的主鍵
指定 primaryKey 生成時,是通過 方法來操作的。有時候,我們會遇到想直接得到一個 key,並且存在於當前數據集中,可以在 options 中同時加上 和 屬性。該 key 的範圍是 [1- $ 2^ $],參考 keygenerator key 的大小
推薦
閱讀推薦
indexedDB W3C 文檔 indexedDB 入門MDN indexedDB 入門
好用庫推薦
idb: 一個 promise 的 DB 庫
Indexed Appendix
IndexedDB 資料庫使用key-value鍵值對儲存數據.你可以對對象的某個屬性創建索引(index)以實現快速查詢和列舉排序。.key可以使二進位對象
IndexedDB 是事務模式的資料庫. IndexedDB API提供了索引(indexes), 表(tables), 指針(cursors)等等, 但是所有這些必須是依賴於某種事務的。
The IndexedDB API 基本上是非同步的.
IndexedDB 資料庫的請求都會包含 onsuccess和onerror事件屬性。
IndexedDB 在結果準備好之後通過DOM事件通知用戶
IndexedDB是面向對象的。indexedDB不是用二維表來表示集合的關係型資料庫。這一點非常重要,將影響你設計和建立你的應用程序。
indexedDB不使用結構化查詢語言(SQL)。它通過索引(index)所產生的指針(cursor)來完成查詢操作,從而使你可以迭代遍歷到結果集合。
IndexedDB遵循同源(same-origin)策略
局限和移除 case
全球多種語言混合存儲。國際化支持不好。需要自己處理。
和伺服器端資料庫同步。你得自己寫同步代碼。
全文搜索。
在以下情況下,資料庫可能被清除:
用戶請求清除數據。
瀏覽器處於隱私模式。最後退出瀏覽器的時候,數據會被清除。
硬碟等存儲設備的容量到限。
不正確的
不完整的改變.
常規概念
資料庫
資料庫: 通常包含一個或多個 object stores. 每個資料庫必須包含以下內容:
名字(Name): 它標識了一個特定源中的資料庫,並且在資料庫的整個生命周期內保持不變. 此名字可以為任意字元串值(包括空字元串).
當前版本(version). 當一個資料庫首次創建時,它的 version 為1,除非另外指定. 每個資料庫在任意時刻只能有一個 version
對象存儲(object store): 用來承載數據的一個分區.數據以鍵值對形式被對象存儲永久持有。在 OS 中,創建一個 key 可以使用 和 。
key generator: 簡單來說就是在存儲數據時,主動生成一個 id++ 來區分每條記錄。這種情況下 存儲數據的 key 是和 value 分開進行存儲的,也就是 (out of line)。
key path: 需要用戶主動來設置儲存數據的 key 內容,
request: 每次讀寫操作,可以當做一次 request.
transaction: 一系列讀寫請求的集合。
index: 一個特殊的 Object Store,用來索引另外一個 Store 的數據。
具體數據 key/value
key generator: 相當於以一種 的形式來生成一個 key 值。
key path: 當前指定的 key 可以根據 value 裡面的內容來指定。裡面可以為一些分隔符。
指定的 key:這個就是需要用戶手動來指定生成。
key: 這個 key 的值,可以通過三種方式生成。 a key generator, a key path, 用戶指定的值。並且,這個 key 在當前的 Object Store 是唯一的。一個 key 類型可以是 string, date, float, and array 類型。不過,在老版本的時候,一般只支持 string or integer。(現在,版本應該都 OK 了)
value: 可以存儲 boolean, number, string, date, object, array, regexp, undefined, and null。現在還可以存儲 files and blob 對象。
操作作用域
scope:這可以比作 transaction 的作用域,即,一系列 transaction 執行的順序。該規定,多個 reading transaction 能夠同時執行。但是 writing 則只能排隊進行。
key range: 用來設置取出數據的 key 的範圍內容。
參考:
原生概念 IndexedDB
IDBFactory
這其實就是 上面掛載的對象。主要 API 如下:
你可以直接通過 來打開一個資料庫。通過 返回一個 Request 對象,來進行結果監聽的回調:
參考:
IndexDB Factory API
IDBRequest
當你通過 方法處理過後,就會得到一個 Request 回調對象。這個就是 IDBRequest 的實例。
你可以通過 得到當前資料庫操作的結果。如果你打開更新後的版本號的話,還需要監聽 事件來實現。最常通過 indexedDB.open 遇見的錯誤就是 版本錯誤。這表明存儲在磁碟上的資料庫的版本高於你試圖打開的版本。
所以,一般在創建 IndexDB 時,還需要管理它版本的更新操作,這裡就需要監聽 onupgradeneeded 來是實現。
或者我們可以直接使用 微型庫來實現讀取操作。
其中通過 回調得到的 event.result 就是 的實例,常常用來設置 index 和插入數據。參考下面內容。
參考:
IDBRequest API
IDBDatabase
該對象常常用來做 Object Store 和 transaction 的創建和刪除。該部分是 事件獲得的 對象:
具體 API 內容如下:
如果它通過 createObjectStore 方法,那麼得到的就是一個 實例對象。如果是 transaction 方法,那麼就是 對象。
IDBObjectStore
該對象一般是用來創建 index 和插入數據使用。
可以參考:
IDBIndex
該對象是用來進行 Index 索引的操作對象,裡面也會存在 和 等方法。詳細內容如下:
參考:
idb 開源庫,微型代碼庫treo 開源庫dexie.js 開源庫indexeddb原生概念 IndexedDB
TAG:前端小吉米 |