深入客戶端存儲(chǔ)

前言

從最早期的 Cookie,到后來(lái)的 AppCache蜡娶、localStorage 和 sessionStorage宁改,再到現(xiàn)在的 indexedDB,客戶端存儲(chǔ)的概念由來(lái)已久愉棱。

技術(shù)不斷往前迭代唆铐,瀏覽器開(kāi)放的能力越來(lái)越多,前端開(kāi)發(fā)者能夠做到的事情也越來(lái)越多樣奔滑。純粹的靜態(tài)網(wǎng)頁(yè)到有豐富交互的動(dòng)態(tài)網(wǎng)頁(yè)花了幾十年時(shí)間艾岂,簡(jiǎn)單的 H5 小游戲往復(fù)雜的中大型游戲過(guò)渡的速度卻在逐漸加快。

Cookie 支持的操作非常有限朋其,localStorage 有關(guān)的 API 精巧而簡(jiǎn)便王浴,到了 indexedDB,我們?cè)诒г蛊鋸?fù)雜性的同時(shí)梅猿,也不得不承認(rèn)祟牲,它的功能更加強(qiáng)大了言沐,賦予我們更多可能。

曾經(jīng)一度,瀏覽器廠商對(duì)前端的定義是精巧运杭、是簡(jiǎn)潔、是靈便枕赵,是一棵新生的樹(shù)遵堵,卓卓成長(zhǎng),擁有無(wú)限可能颖低,而隨著一樁樁要求被提出絮吵,一件件功能被滿足,樹(shù)木的根須越扎越深忱屑,樹(shù)干愈發(fā)粗壯蹬敲,它開(kāi)始能夠承載更多暇昂,那些可能性逐一抽枝,向天生長(zhǎng)伴嗡,如今的前端早今非昔比急波。

Cookie 面世幾十載,localStorage 出現(xiàn)的時(shí)間也不短闹究,網(wǎng)絡(luò)上關(guān)于這兩者的文章已經(jīng)太多幔崖,本文不再過(guò)多贅述,本文的重點(diǎn)將集中在最后出現(xiàn)的這家伙身上渣淤,——indexedDB赏寇,討論它出現(xiàn)的背景,介紹它具體的用法价认,再稍稍展望一下未來(lái)嗅定。

好了,廢話不說(shuō)話用踩,我們直接開(kāi)始吧渠退。

什么是 indexedDB?

在深入使用之前脐彩,我們需要先知道碎乃,什么是 indexedDB,以及惠奸,有了 localStorage梅誓,我們?yōu)槭裁催€需要 indexedDB?

這個(gè)問(wèn)題可以用 Cookie 和 localStorage 的關(guān)系來(lái)類比佛南,有了 Cookie梗掰,我們?yōu)槭裁催€需要 localStorage?

答案很簡(jiǎn)單嗅回,存儲(chǔ)空間不夠用及穗。

最早的 Cookie 只能存儲(chǔ) 4KB 的數(shù)據(jù),這個(gè)數(shù)據(jù)量對(duì)上世紀(jì)末的前端頁(yè)面來(lái)說(shuō)綽綽有余绵载,那時(shí)候的網(wǎng)速只有幾 KB埂陆,4KB 的存儲(chǔ)足夠 Cookie 橫行絕大多數(shù)網(wǎng)頁(yè)。

而隨著寬帶速率的提升娃豹,更復(fù)雜的網(wǎng)頁(yè)成為可能猜惋,精美的圖片、漂亮的字體培愁,視頻、音頻缓窜,前端的交互變得復(fù)雜定续,所涉及到的動(dòng)態(tài)數(shù)據(jù)也越來(lái)越多谍咆,前后端分離的愿景尚未完全實(shí)現(xiàn),被越來(lái)越嚴(yán)重的前后端耦合拉得開(kāi)起了倒車私股。

這種情況下摹察,HTML5 發(fā)布了,localStorage 應(yīng)運(yùn)而生倡鲸。

localStorage 支持的數(shù)據(jù)量達(dá)到 2.5MB 到 10MB供嚎,較前者提升了千倍不止,這在一定程度上解決了前后端耦合的問(wèn)題峭状,賦能了前端同學(xué)克滴,也降低了后端同學(xué)的工作量,大家都很開(kāi)心优床。

但前端同學(xué)的野心不止于此劝赔,瀏覽器同樣。

做過(guò) 3D WebGL 開(kāi)發(fā)的同學(xué)都知道胆敞,資源文件的加載十分令人頭疼着帽,這些文件的體積往往相當(dāng)可觀,localStorage 容納不下移层,網(wǎng)絡(luò)側(cè)加載又十分耗時(shí)仍翰,indexedDB 的出現(xiàn)為這一現(xiàn)象提供了有效的解決方案。

嘗試給 indexedDB 下一個(gè)定義之前观话,我們需要先了解兩個(gè)概念予借。

關(guān)系型數(shù)據(jù)庫(kù)非關(guān)系型數(shù)據(jù)庫(kù)

不熟悉后端的同學(xué)可能對(duì)這兩個(gè)概念都陌生匪燕,但是提及一些名詞蕾羊,你們大概就會(huì)模模糊糊的有概念。

MySql帽驯、Access龟再、Oracle。
MongoDB尼变、Redis利凑、HBase。

這么多單詞嫌术,總有一兩個(gè)你熟悉哀澈。

是的,后端同學(xué)經(jīng)常掛在嘴上的 MySql度气、Access割按、Oracle……這些可以用 SQL 語(yǔ)句進(jìn)行查詢的數(shù)據(jù)庫(kù),屬于關(guān)系型數(shù)據(jù)庫(kù)磷籍,它們擁有具體的行和列适荣,一系列行和列組成一張表现柠,若干張表構(gòu)成一個(gè)數(shù)據(jù)庫(kù)。

其他那些則統(tǒng)統(tǒng)屬于非關(guān)系型數(shù)據(jù)庫(kù)弛矛。

indexedDB 是一種運(yùn)行在客戶端的數(shù)據(jù)庫(kù)够吩,它以鍵值對(duì)(key-value)的形式存儲(chǔ)數(shù)據(jù),屬于非關(guān)系型數(shù)據(jù)庫(kù)丈氓。

indexedDB 遵循同源協(xié)議周循,只有同域的 js 代碼能夠訪問(wèn)它,其他都不可以万俗。

有了這個(gè)基礎(chǔ)概念之后湾笛,我們就可以進(jìn)一步去了解 indexedDB 了。

首先该编,關(guān)于存儲(chǔ)空間

我們知道迄本,indexedDB 之所以提出,是因?yàn)?localStorage 提供的 5MB 存儲(chǔ)空間實(shí)在不夠用课竣,那么嘉赎,indexedDB 可以讓我們?cè)诳蛻舳舜鎯?chǔ)多少數(shù)據(jù)呢?

答案是于樟,大體上比 localStorage 多公条,具體到不同瀏覽器,有不同的限制迂曲。

拿 Chrome 來(lái)說(shuō)靶橱,Chrome67 之前的版本規(guī)定 indexedDB 最多可使用 50% 的硬盤(pán)空間,從 Chrome 67 開(kāi)始路捧,這一規(guī)定發(fā)生變化关霸。

在 Chrome 正常模式下

該模式下的站點(diǎn)可用空間可通過(guò)如下公式計(jì)算:

(硬盤(pán)總空間 - 保留空間 - 已使用空間) * 20%

這里保留空間指得是,瀏覽器需要留存出來(lái)的空間(should remain avaliable)杰扫,它的定義是 2 GB 和硬盤(pán)總空間的 10% 中的較低值队寇,也就是:

min(2GB, 硬盤(pán)總空間*10%)

也就是說(shuō),當(dāng) 硬盤(pán)總空間 - 已使用空間 <= 保留空間 時(shí)章姓,站點(diǎn)的數(shù)據(jù)將無(wú)法存儲(chǔ)佳遣。

舉例來(lái)說(shuō),一塊存儲(chǔ)空間為 256GB 的硬盤(pán)凡伊,它的保留空間為 2 GB 和硬盤(pán)總空間的 10% 中的較低值零渐,也就是 2GB,這時(shí)瀏覽器可使用的臨時(shí)存儲(chǔ)空間大小就是 256GB - 2GB = 254GB系忙。
假設(shè)我們的站點(diǎn)上線時(shí)诵盼,用戶的 254GB 存儲(chǔ)空間已經(jīng)被占用 4GB,那么我們的站點(diǎn)可以分配到的存儲(chǔ)空間就是 (254 - 4) * 20%,也就是 50 GB拦耐。

在 Chrome 隱身模式下

新站點(diǎn)的配額為固定的 100MB耕腾。

其次,關(guān)于存儲(chǔ)格式

除了存儲(chǔ)空間問(wèn)題杀糯,indexedDB 也解決了 localStorage 存儲(chǔ)格式單一的問(wèn)題,它支持的存儲(chǔ)數(shù)據(jù)格式更加豐富苍苞,除了基本的字符串外固翰,它還支持二進(jìn)制數(shù)據(jù)的存儲(chǔ),比如 ArrayBuffer羹呵,比如 Blob骂际。

于是我們之前的提到的 3D 模型文件、各類圖片文件冈欢,就可以被轉(zhuǎn)換成 Blob 格式歉铝,存儲(chǔ)在 indexedDB 中,免去網(wǎng)絡(luò)請(qǐng)求的消耗凑耻。

講了那么多太示,indexedDB 那么強(qiáng)大,那么香浩,我們?cè)撊绾稳ナ褂盟兀?/p>

開(kāi)始了类缤,該怎么使用 indexedDB?

indexedDB 相較 localStorage 來(lái)說(shuō)邻吭,擁有更多的新概念餐弱、更復(fù)雜 API,直接引入那些概念囱晴,對(duì)沒(méi)接觸過(guò)后端的同學(xué)來(lái)說(shuō)膏蚓,存在一定門檻,所以本文通過(guò)一個(gè)實(shí)例來(lái)講解畸写。

這個(gè)例子假設(shè)我們現(xiàn)在需要開(kāi)發(fā)一款軟件驮瞧,寫(xiě)作軟件,面向文字工作者艺糜。

開(kāi)發(fā)軟件的時(shí)候剧董,我們需要新建項(xiàng)目、設(shè)置項(xiàng)目信息破停,再在項(xiàng)目?jī)?nèi)部編寫(xiě)代碼翅楼,對(duì)于這些文字工作者來(lái)說(shuō),他們也有新建書(shū)籍真慢、設(shè)置書(shū)籍基礎(chǔ)信息毅臊,以及在書(shū)籍內(nèi)部編寫(xiě)章節(jié)內(nèi)容的需要。

有了這一點(diǎn)基礎(chǔ)認(rèn)知黑界,我們就可以嘗試分解軟件功能管嬉≡砹郑總體來(lái)說(shuō),它應(yīng)該包含兩部分內(nèi)容:

  1. 新建書(shū)籍蚯撩。
  2. 編輯章節(jié)內(nèi)容础倍。

進(jìn)一步對(duì)這兩個(gè)需求進(jìn)行分析,我們還可以得出以下更詳細(xì)的功能需求胎挎。

  1. 書(shū)籍相關(guān)的:

    1. 書(shū)籍列表:一個(gè)展示書(shū)籍列表的頁(yè)面沟启,一個(gè)獲取書(shū)籍信息的方法。
    2. 新建書(shū)籍:一個(gè)新建書(shū)籍的對(duì)話框犹菇,一個(gè)新建書(shū)籍的方法德迹。
    3. 編輯書(shū)籍:一個(gè)編輯書(shū)籍的對(duì)話框,一個(gè)編輯書(shū)籍的方法揭芍。
    4. 刪除書(shū)籍:一個(gè)刪除書(shū)籍的對(duì)話框胳搞,一個(gè)刪除書(shū)籍的方法。
  2. 章節(jié)相關(guān)的:

    1. 章節(jié)列表:一個(gè)展示章節(jié)列表的頁(yè)面称杨,一個(gè)獲取章節(jié)信息的方法肌毅。
    2. 章節(jié)內(nèi)容:一個(gè)展示章節(jié)內(nèi)容的頁(yè)面,一個(gè)獲取章節(jié)內(nèi)容的方法列另。
    3. 新增章節(jié):……
    4. 修改章節(jié)標(biāo)題:……
    5. 刪除章節(jié):……
    6. 編輯章節(jié)內(nèi)容:……

有了項(xiàng)目文檔芽腾,我們就可以著手去開(kāi)發(fā)了。

前端大體是一個(gè)單頁(yè)應(yīng)用页衙,使用熟悉的技術(shù)棧去開(kāi)發(fā)就好摊滔,本文不再贅述。

數(shù)據(jù)存儲(chǔ)方面店乐,考慮到一個(gè)用戶不止編寫(xiě)一本書(shū)籍艰躺,一本書(shū)籍包含若干章節(jié),每個(gè)章節(jié)的編輯歷史也很復(fù)雜眨八,localStorage 的存儲(chǔ)空間不出意外百分百不夠用腺兴,在不借助后端的情況下,我們只能求助于 indexedDB廉侧。

那么就來(lái)使用 indexedDB 吧页响。

新建一個(gè)數(shù)據(jù)庫(kù)

使用 window.indexedDB.open 可以打開(kāi)或者新建一個(gè)數(shù)據(jù)庫(kù),像這樣:

const request = window.indexedDB.open(dbName, dbVersion);

dbName 指代數(shù)據(jù)庫(kù)的名字段誊,dbVersion 指代數(shù)據(jù)庫(kù)的版本闰蚕,該方法返回一個(gè)異步請(qǐng)求,通過(guò)設(shè)置它成功(success)连舍、失斆欢浮(error)和升級(jí)(upgradeneeded)回調(diào),我們可以獲得一個(gè)數(shù)據(jù)庫(kù)對(duì)象,進(jìn)行對(duì)應(yīng)的處理盼玄,像這樣:

let db = null;

const dbName = 'coolb-writer';
const dbVersion = 1;

const request = window.indexedDB.open(dbName, dbVersion);

request.onupgradeneeded = (ev) => {
    db = ev.target.result;
};

request.onsuccess = (ev) => {
    db = ev.target.result;
};

request.onerror = (error)=>{
    console.log('新建或打開(kāi)數(shù)據(jù)庫(kù)出錯(cuò)', error);
};

這里可能有一些內(nèi)容令人費(fèi)解贴彼,比如,新建數(shù)據(jù)庫(kù)就新建數(shù)據(jù)庫(kù)埃儿,為什么要指定 dbName器仗?比如,有 dbName 就算了蝌箍,為什么還要 dbVersion青灼?再比如,success 好理解妓盲,error 也不難接受,upgradeneeded 回調(diào)是什么专普?我們?yōu)槭裁葱枰O(jiān)聽(tīng)它悯衬?

下面來(lái)逐一解答。

  1. 新建數(shù)據(jù)庫(kù)就新建數(shù)據(jù)庫(kù)檀夹,為什么要指定 dbName筋粗?

    首先,我們知道 indexedDB 遵循同源協(xié)議炸渡,只有同域的 js 代碼能夠訪問(wèn)它娜亿,其他都不可以。

    在我們舉例的場(chǎng)景下蚌堵,一個(gè)頁(yè)面只需要一個(gè)數(shù)據(jù)庫(kù)买决,取什么名字都無(wú)所謂。

    但在稍微復(fù)雜一點(diǎn)的場(chǎng)景下吼畏,比如公司內(nèi)部的中臺(tái)系統(tǒng)督赤,——一個(gè)涉及到多人協(xié)作的公司內(nèi)部,為了提高工作效率泻蚊、降低出錯(cuò)概率躲舌,重復(fù)性的勞動(dòng)往往被流程化的內(nèi)部系統(tǒng)所取代,比如 OA 系統(tǒng)性雄、文檔中心没卸,比如發(fā)布系統(tǒng)、監(jiān)控系統(tǒng)秒旋,等等等等约计。

    這些系統(tǒng)互相之間保持高度獨(dú)立,但在某些子功能模塊上滩褥,它們也可能互相關(guān)聯(lián)病蛉。出于方便,我們將它們托管在同一個(gè)域名下,以不同的路徑區(qū)分铺然。

    因?yàn)橥蛩仔ⅲ运鼈兛梢栽L問(wèn)彼此的數(shù)據(jù)庫(kù),這種情況下魄健,給你的數(shù)據(jù)庫(kù)取一個(gè)識(shí)別度高的名字赋铝,就很有必要了。

  2. dbName 就算了沽瘦,為什么還要 dbVersion革骨?

    因?yàn)槟愕捻?xiàng)目大概率需要迭代,數(shù)據(jù)庫(kù)也存在一定概率析恋,需要跟著升級(jí)良哲。

    怎么理解這句話呢?

    簡(jiǎn)單來(lái)說(shuō)助隧,復(fù)雜的項(xiàng)目出于各種原因筑凫,可能被抽絲剝繭,進(jìn)行分期開(kāi)發(fā):一期只包含最核心功能并村,二三期根據(jù)市場(chǎng)反饋和決策者的抉擇巍实,進(jìn)行適當(dāng)?shù)凸δ茉鰷p。

    這個(gè)過(guò)程中哩牍,你的數(shù)據(jù)庫(kù)很難保持一成不變棚潦。

    就拿我們舉的例子來(lái)說(shuō),最開(kāi)始我們只包含核心功能膝昆,但是軟件上線后丸边,我們可能很快收到一個(gè)反饋:【能不能支持一下編輯歷史啊外潜?一個(gè)章節(jié)大幾千小幾萬(wàn)字的原环,個(gè)把月才能寫(xiě)完,有時(shí)候我寫(xiě)了一段內(nèi)容处窥,覺(jué)得不好嘱吗,刪了,回頭還想找回來(lái)就沒(méi)了滔驾,你們能不能支持一下回看編輯內(nèi)容摆寺蟆?我看別的軟件都有哆致,應(yīng)該不難做吧绕德?拜托支持一下,這對(duì)我真的很重要摊阀!】

    你當(dāng)然能夠理解這個(gè)功能的重要性耻蛇,畢竟踪蹬,你自己寫(xiě)代碼的時(shí)候,也用到了版本控制臣咖。

    想要支持這個(gè)功能跃捣,你就需要新增一個(gè)Object Store(類似關(guān)系型數(shù)據(jù)庫(kù)的表,具體后面會(huì)介紹)夺蛇,同時(shí)對(duì)已有的 Object Store 進(jìn)行適當(dāng)調(diào)整疚漆,這個(gè)操作我們通過(guò)修改數(shù)據(jù)庫(kù)版本號(hào)的方式來(lái)實(shí)現(xiàn)。

    數(shù)據(jù)庫(kù)的版本號(hào)修改后刁赦,upgradeneeded 事件便會(huì)被觸發(fā)娶聘,一切你需要做的事情,都可以在那里面進(jìn)行甚脉。這就是 dbVersion 的存在作用丸升。

  3. success 好理解,error 也不難接受牺氨,upgradeneeded 回調(diào)是什么发钝?我們?yōu)槭裁葱枰O(jiān)聽(tīng)它?

    upgradeneeded 回調(diào)只在新增升級(jí)數(shù)據(jù)庫(kù)時(shí)波闹,被觸發(fā)。

    新增涛碑,也就是 window.indexedDB.open 第一次被調(diào)用時(shí)精堕;升級(jí),也就是 dbVersion 調(diào)整后的第一次蒲障。只有這兩種情況下歹篓,upgradeneeded 回調(diào)被觸發(fā)。

    同時(shí)揉阎,新增庄撮、調(diào)整、刪除 Object Store 的操作毙籽,也只能在這里面進(jìn)行洞斯,success 里面不行,error 里面也不可以坑赡。

    也就是說(shuō)烙如,upgradeneeded 回調(diào)是用來(lái),讓我們?cè)谶m當(dāng)?shù)臅r(shí)候毅否,進(jìn)行數(shù)據(jù)庫(kù)本身的維護(hù)工作的亚铁,相對(duì)應(yīng)的,success 回調(diào)只處理跟數(shù)據(jù)有關(guān)的內(nèi)容螟加。

看完以上內(nèi)容徘溢,相信大家對(duì)創(chuàng)建數(shù)據(jù)庫(kù)的部分內(nèi)容已經(jīng)了解清楚吞琐。

至此,我們便有了一個(gè)可以用來(lái)存儲(chǔ)數(shù)據(jù)的數(shù)據(jù)庫(kù)然爆,下面來(lái)看看怎么把數(shù)據(jù)存進(jìn)去站粟,以及,怎么根據(jù)需求處理數(shù)據(jù)吧施蜜。

新建數(shù)據(jù)倉(cāng)庫(kù)

上一步我們新建了一個(gè)名為 coolb-writer 數(shù)據(jù)庫(kù) 卒蘸,把數(shù)據(jù)庫(kù)想象成一個(gè)空曠的倉(cāng)庫(kù),為了有序存放數(shù)據(jù)翻默,除了遮風(fēng)避雨的倉(cāng)庫(kù)本身缸沃,我們還需要一只只整齊擺放的貨架。

在關(guān)系型數(shù)據(jù)庫(kù)里修械,這一只只貨架趾牧,被我們稱為,在非關(guān)系型數(shù)據(jù)庫(kù) indexedDB 這兒肯污,我們稱之為 Object Store翘单,也就是數(shù)據(jù)倉(cāng)庫(kù)

創(chuàng)建數(shù)據(jù)倉(cāng)庫(kù)(Object Store)需要使用數(shù)據(jù)庫(kù)(db)對(duì)象的 createObjectStore 方法蹦渣,像這樣:

request.onupgradeneeded = (ev) => {
    db = ev.target.result;
    const bookStore = db.createObjectStore('books', { keyPath:'bid' });
    const chapterStore = db.createObjectStore('chapters', { keyPath:'cid' });
};

通過(guò)這兩次調(diào)用哄芜,我們創(chuàng)建了兩個(gè)名為 books 和 chapters 的數(shù)據(jù)倉(cāng)庫(kù),并為它們指定主鍵 bid 和 cid柬唯。

考慮到 upgradeneeded 回調(diào)可能不止一次被調(diào)用认臊,所以上面的寫(xiě)法最好還是改進(jìn)一下:

function createObjectStoreIfNotExist(name, config) {
    if (!db.objectStoreNames.contains(name)) {
        db.createObjectStore(name, config);
    }
}
request.onupgradeneeded = (ev) => {
    db = ev.target.result;

    createObjectStoreIfNotExist('books', { keyPath: 'bid' });
    createObjectStoreIfNotExist('chapters', { keyPath: 'cid' });
};

bid 和 cid 是 books 和 chapters 中所存儲(chǔ)數(shù)據(jù)的一個(gè)屬性,books 所存儲(chǔ)的數(shù)據(jù)大體是如下格式:

{
    'bid': 1678680731689,
    'name': ...,
    'thumb': ...,
    'process': ...,
    'tags': ...
}

除了 bid锄奢、name失晴、thumb 等,我們也可以指定 books 的主鍵為 tags 下面的屬性拘央,比如 tags.id涂屁,此外,如果數(shù)據(jù)記錄內(nèi)沒(méi)有適合作為主鍵的屬性灰伟,也可以讓 indexedDB 自動(dòng)生成主鍵拆又。

const objectStore = db.createObjectStore(objectStoreName, { autoIncrement: true });

數(shù)據(jù)倉(cāng)庫(kù)新建完成之后,我們可以嘗試為其添加索引袱箱。

新建索引

在新建索引之前遏乔,我們需要先知道,什么是索引发笔?

純純的前端同學(xué)可能對(duì)這個(gè)概念很陌生盟萨,所以這里還是通過(guò)一個(gè)例子來(lái)介紹,這也是 indexedDB 相較 localStotage 的強(qiáng)大之處了讨。

我們可以先回憶一下捻激,使用 localStotage 存儲(chǔ)數(shù)據(jù)時(shí)制轰,我們是怎么做的。

localStorage.setItem(key, value);
localStorage.getItem(key);

通過(guò)一個(gè)關(guān)鍵字 key胞谭,我們可以完成對(duì)數(shù)據(jù)的讀取和存儲(chǔ)功能垃杖。

假設(shè)當(dāng)前項(xiàng)目中,我們通過(guò) localStorage 存取數(shù)據(jù)丈屹,那么在存儲(chǔ)一本書(shū)時(shí)调俘,我們可以這樣做:

const bid = generateUniqueBid();
const bdata = {
    bid,
    name: 'JavaScript 高級(jí)程序設(shè)計(jì)',
    thumb: File,
    process: 0, // 0 新建,1 編寫(xiě)中旺垒,2 已完結(jié)彩库,3 坑
    ...
};

localStorage.setItem(bid, JSON.stringify(bdata));

在讀取一本書(shū)的詳細(xì)數(shù)據(jù)時(shí),我們可以這樣做:

localStorage.getItem(bid);

可如果我們想要讀取所有狀態(tài)為“已完結(jié)”的書(shū)籍呢先蒋?做不到骇钦,只能先讀取所有書(shū)籍的詳細(xì)信息,再在前端遍歷獲取竞漾。

索引的出現(xiàn)眯搭,為這一需求,提供了解決方案业岁。

以上案例中的 bid 可以類比到 indexedDB 中的主鍵鳞仙,name、process 一類笔时,可能作為查詢條件的字段則可以類比到 indexedDB 中的索引繁扎。

創(chuàng)建索引(Index)需要使用數(shù)據(jù)倉(cāng)庫(kù)(Object Store)對(duì)象的 createIndex 方法,像這樣:

function createObjectStoreIfNotExist(name, config, indexes = {}) {
    if (db.objectStoreNames.contains(name)) {
        const objectStore = db.createObjectStore(name, config);
        // 通過(guò)數(shù)據(jù)倉(cāng)庫(kù)的 createIndex 方法來(lái)創(chuàng)建索引糊闽。
        for (let name in indexes) {
            objectStore.createIndex(name, name, indexes[name]);
        }
    }
}
request.onupgradeneeded = (ev) => {
    db = ev.target.result;

    createObjectStoreIfNotExist('books', { keyPath: 'bid' }, {
        name: { unique: false },
        process: { unique: false }
    });
    createObjectStoreIfNotExist('chapters', { keyPath: 'cid' }, {
        name: { unique: false }
    });
};

查詢數(shù)據(jù)

在 indexedDB 中,我們可以通過(guò)主鍵查詢數(shù)據(jù)爹梁,也可以通過(guò)索引查詢數(shù)據(jù)右犹,具體落實(shí)查詢時(shí),我們會(huì)需要涉及一個(gè)叫做事務(wù)(transaction)的概念姚垃。

事務(wù)(transaction)保證了所有操作的按順序進(jìn)行念链,避免了寫(xiě)入時(shí)讀取,或者同時(shí)寫(xiě)入的問(wèn)題积糯。此外掂墓,事務(wù)作為一個(gè)完整的工作單位,存在不可分割的特性看成,也就是說(shuō)君编,一系列操作步驟之中,只要有一步失敗川慌,整個(gè)事務(wù)就都取消吃嘿,數(shù)據(jù)庫(kù)回滾到事務(wù)發(fā)生之前的狀態(tài)祠乃,不存在只改寫(xiě)一部分?jǐn)?shù)據(jù)的情況。

下面來(lái)看看具體的操作兑燥。

1. 按照主鍵讀取單條數(shù)據(jù):

function getData(objectStoreName, id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readonly');
        const objectStore = transaction.objectStore(objectStoreName);
        const request = objectStore.get(id);

        request.onsuccess = (ev) => resolve(ev.target.result);
        request.onerror = (err) => reject(err);
    });
}

getData('books', bid);

2. 按照索引讀取單條數(shù)據(jù):

function getOneByIndex(objectStoreName, indexName, indexValue) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readonly');
        const objectStore = transaction.objectStore(objectStoreName);
        const index = objectStore.index(indexName);
        const request = index.get(indexValue);

        request.onsuccess = (ev) => resolve(ev.target.result);
        request.onerror = (err) => reject(err);
    });
}

getOneByIndex('books', 'process', 2);

除了單條讀取數(shù)據(jù)外亮瓷,我們還可以批量讀取數(shù)據(jù),這時(shí)會(huì)涉及到一個(gè)叫做游標(biāo)(Cusor)的概念降瞳。

3. 借助游標(biāo)嘱支,讀取數(shù)據(jù)倉(cāng)庫(kù)中的所有數(shù)據(jù):

function getAll(objectStoreName) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readonly');
        const objectStore = transaction.objectStore(objectStoreName);
        const request = objectStore.openCursor();

        let results = [];

        request.onsuccess = (ev) => {
            const cursor = ev.target.result;
            if (cursor) {
                results.push(cursor.value);
                cursor.continue();
            } else {
                resolve(results);
            }
        };
        request.onerror = (err) => reject(err);
    });
}

4. 借助游標(biāo)和索引,讀取數(shù)據(jù)倉(cāng)庫(kù)中挣饥,滿足條件的部分?jǐn)?shù)據(jù):

function getDataByIndex(objectStoreName, indexName, indexValue) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readonly');
        const store = transaction.objectStore(objectStoreName);

        const index = store.index(indexName);
        const range = IDBKeyRange.only(indexValue);

        const request = index.openCursor(range);

        let result = [];

        request.onsuccess = (ev) => {
            const cursor = ev.target.result;
            if (cursor) {
                result.push(cursor.value);
                cursor.continue();
            } else {
                resolve(result);
            }
        };
        request.onerror = (err) => reject(err);
    });
}

以上示例中除师,我們通過(guò) IDBKeyRange.only 創(chuàng)建一個(gè)唯一匹配的 range 對(duì)象,表示只匹配值為 indexValue 的所有數(shù)據(jù)亮靴,除此之外 IDBKeyRange 還支持如下類型的匹配:

匹配值域 方法
所有 <= x 的值 IDBKeyRange.upperBound(x)
所有 < x 的值 IDBKeyRange.upperBound(x, true)
所有 >= y 的值 IDBKeyRange.lowerBound(y)
所有 > y 的值 IDBKeyRange.lowerBound(y, true)
所有 >= x && <= y 的值 IDBKeyRange.bound(x, y)
所有 > x && < y 的值 IDBKeyRange.bound(x, y, true, true)
所有 > x && <= y 的值 IDBKeyRange.bound(x, y, true, false)
所有 >= x && < y 的值 IDBKeyRange.bound(x, y, false, true)
=== x 的值 IDBKeyRange.only(x)

插入數(shù)據(jù)

插入數(shù)據(jù)同樣通過(guò)事務(wù)進(jìn)行馍盟。

function insertData(objectStoreName, data) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readwrite');
        const objectStore = transaction.objectStore(objectStoreName);
        const request = objectStore.add(data);

        request.onsuccess = (ev) => resolve(ev);
        request.onerror = (err) => reject(err);
    });
}

更新數(shù)據(jù)

更新數(shù)據(jù)通過(guò)一個(gè)叫做 put 的方法,put 方法也可以用于新增數(shù)據(jù)茧吊,它和 add 方法的區(qū)別是:

  1. 如果 objectStore 中已有對(duì)應(yīng) id贞岭,則表示更新,否則表示添加搓侄。
  2. 在設(shè)置了自增瞄桨,也就是 autoIncrement 為 true 的情況下,put 方法必須傳第二個(gè)參數(shù)讶踪,第二個(gè)參數(shù)指定被更新的主鍵的值芯侥,傳錯(cuò)或不傳,都表示新增乳讥。
function editData(objectStoreName, data) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readwrite');
        const objectStore = transaction.objectStore(objectStoreName);
        const request = objectStore.put(data);

        request.onsuccess = (ev) => resolve(ev);
        request.onerror = (err) => reject(err);
    });
}

刪除數(shù)據(jù)

1. 按照主鍵柱查,刪除單條數(shù)據(jù)

function deleteData(objectStoreName, id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readwrite');
        const objectStore = transaction.objectStore(objectStoreName);
        const request = objectStore.delete(id);

        request.onsuccess = (ev) => resolve(ev);
        request.onerror = (err) => reject(err);
    });
}

2. 按照游標(biāo)和索引,批量刪除數(shù)據(jù)

function deleteDataByIndex(objectStoreName, indexName, indexValue) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([objectStoreName], 'readwrite');
        const store = transaction.objectStore(objectStoreName);

        const index = store.index(indexName);
        const range = IDBKeyRange.only(indexValue);

        const request = index.openCursor(range);

        request.onsuccess = (ev) => {
            const cursor = ev.target.result;
            if (cursor) {
                cursor.delete();
                cursor.continue();
            } else {
                resolve();
            }
        };
        request.onerror = (err) => reject(err);
    });
}

以上便是 indexedDB 中最常用的一些 API云石,通過(guò)組合這些函數(shù)唉工,我們可以實(shí)現(xiàn)對(duì)寫(xiě)作軟件中所涉及所有數(shù)據(jù)的操作。

一點(diǎn)點(diǎn)展望

目前來(lái)看汹忠,indexedDB 的使用還不算廣泛淋硝,同時(shí),它的 API 使用起來(lái)也較為麻煩宽菜,市面上已經(jīng)存在部分工具包谣膳,幫助我們更加便捷地使用 indexedDB,同時(shí)也可以優(yōu)雅降級(jí)铅乡,在不支持 indexedDB 的瀏覽器上继谚,使用更加古早的 API。

這種狀況有點(diǎn)類似 es6阵幸、7犬庇、8 之前的 underscore 和 lodash僧界,我們現(xiàn)在也一定程度上依賴這些工具包,但 JS 本身的規(guī)范是朝著便捷和可用性的方向上靠的臭挽,所以捂襟,未來(lái),隨著 indexedDB 的使用愈發(fā)廣泛欢峰,我們也許也可以期待一下它的功能變得更強(qiáng)大葬荷,同時(shí) API 變得更簡(jiǎn)單。

也許纽帖,哪天宠漩,對(duì)幀率要求極高的超大型游戲也能穩(wěn)定流暢地運(yùn)行在瀏覽器端也說(shuō)不定。

以上懊直。

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扒吁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子室囊,更是在濱河造成了極大的恐慌雕崩,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件融撞,死亡現(xiàn)場(chǎng)離奇詭異盼铁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)尝偎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門饶火,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人致扯,你說(shuō)我怎么就攤上這事肤寝。” “怎么了抖僵?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵醒陆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我裆针,道長(zhǎng),這世上最難降的妖魔是什么寺晌? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任世吨,我火速辦了婚禮,結(jié)果婚禮上呻征,老公的妹妹穿的比我還像新娘耘婚。我一直安慰自己,他們只是感情好陆赋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布沐祷。 她就那樣靜靜地躺著嚷闭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赖临。 梳的紋絲不亂的頭發(fā)上胞锰,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音兢榨,去河邊找鬼嗅榕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛吵聪,可吹牛的內(nèi)容都是我干的凌那。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吟逝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼帽蝶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起块攒,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤励稳,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后局蚀,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體麦锯,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年琅绅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扶欣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡千扶,死狀恐怖料祠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澎羞,我是刑警寧澤髓绽,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站妆绞,受9級(jí)特大地震影響顺呕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜括饶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一株茶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧图焰,春花似錦启盛、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)卧抗。三九已至,卻和暖如春鳖粟,著一層夾襖步出監(jiān)牢的瞬間社裆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工牺弹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浦马,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓张漂,卻偏偏與公主長(zhǎng)得像晶默,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子航攒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容