前言
從最早期的 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)容:
- 新建書(shū)籍蚯撩。
- 編輯章節(jié)內(nèi)容础倍。
進(jìn)一步對(duì)這兩個(gè)需求進(jìn)行分析,我們還可以得出以下更詳細(xì)的功能需求胎挎。
-
書(shū)籍相關(guān)的:
- 書(shū)籍列表:一個(gè)展示書(shū)籍列表的頁(yè)面沟启,一個(gè)獲取書(shū)籍信息的方法。
- 新建書(shū)籍:一個(gè)新建書(shū)籍的對(duì)話框犹菇,一個(gè)新建書(shū)籍的方法德迹。
- 編輯書(shū)籍:一個(gè)編輯書(shū)籍的對(duì)話框,一個(gè)編輯書(shū)籍的方法揭芍。
- 刪除書(shū)籍:一個(gè)刪除書(shū)籍的對(duì)話框胳搞,一個(gè)刪除書(shū)籍的方法。
-
章節(jié)相關(guān)的:
- 章節(jié)列表:一個(gè)展示章節(jié)列表的頁(yè)面称杨,一個(gè)獲取章節(jié)信息的方法肌毅。
- 章節(jié)內(nèi)容:一個(gè)展示章節(jié)內(nèi)容的頁(yè)面,一個(gè)獲取章節(jié)內(nèi)容的方法列另。
- 新增章節(jié):……
- 修改章節(jié)標(biāo)題:……
- 刪除章節(jié):……
- 編輯章節(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)逐一解答。
-
新建數(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í)別度高的名字赋铝,就很有必要了。
-
有
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 的存在作用丸升。
-
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ū)別是:
- 如果 objectStore 中已有對(duì)應(yīng) id贞岭,則表示更新,否則表示添加搓侄。
- 在設(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ō)不定。
以上懊直。