大家好,Echa。
今天來(lái)分享常見(jiàn)的瀏覽器數(shù)據(jù)存儲(chǔ)方案:localStorage、sessionStorage、IndexedDB、Cookies。
1. 概述
現(xiàn)代瀏覽器中提供了多種存儲(chǔ)機(jī)制,打開(kāi)瀏覽器的控制臺(tái)(mac 可以使用 Command + Option + J 快捷鍵,windows 可以使用 Control + Shift + J 快捷鍵)。選擇 Application 選項(xiàng)卡,可以在 Storage中 看到 Local Storage、Session Storage、IndexedDB、Web SQL、Cookies 等:

那數(shù)據(jù)存儲(chǔ)在瀏覽器中有什么使用場(chǎng)景呢?在以下情況下,將數(shù)據(jù)存儲(chǔ)在瀏覽器中成為更可行的選擇:
- 在瀏覽器存儲(chǔ)中保存應(yīng)用狀態(tài),比如保持用戶偏好(用戶特定的設(shè)置,例如亮模式或暗模式、字體大小等);
- 創(chuàng)建離線工作的漸進(jìn)式 Web 應(yīng)用,除了初始下載和更新之外沒(méi)有服務(wù)器端要求;
- 緩存靜態(tài)應(yīng)用資源,如 html、css、JS 和圖像等;
- 保存上一個(gè)瀏覽會(huì)話中的數(shù)據(jù),例如存儲(chǔ)上一個(gè)會(huì)話中的購(gòu)物車(chē)內(nèi)容,待辦事項(xiàng)列表中的項(xiàng)目,記住用戶是否以前登錄過(guò)等。
無(wú)論哪種方式,將這些信息保存在客戶端可以減少額外且不必要的服務(wù)器調(diào)用,并幫助提供離線支持。不過(guò),需要注意,由于實(shí)現(xiàn)差異,瀏覽器存儲(chǔ)機(jī)制在不同瀏覽器中的行為可能會(huì)有所不同。除此之外,許多瀏覽器已刪除對(duì) Web SQL 的支持,建議將現(xiàn)有用法遷移到 IndexedDB。
所以下面我們將介紹 Local Storage、Session Storage、IndexedDB、Cookies 的使用方式、使用場(chǎng)景以及它們之間的區(qū)別。
2. Web Storage
(1)概述
HTML5 引入了 Web Storage,這使得在瀏覽器中存儲(chǔ)和檢索數(shù)據(jù)變得更加容易。Web Storage API 為客戶端瀏覽器提供了安全存儲(chǔ)和輕松訪問(wèn)鍵值對(duì)的機(jī)制。Web Storage 提供了兩個(gè) API 來(lái)獲取和設(shè)置純字符串的鍵值對(duì):
- localStorage:用于存儲(chǔ)持久數(shù)據(jù),除非用戶手動(dòng)將其從瀏覽器中刪除,否則數(shù)據(jù)將終身存儲(chǔ)。即使用戶關(guān)閉窗口或選項(xiàng)卡,它也不會(huì)過(guò)期;
- sessionStorage:用于存儲(chǔ)臨時(shí)會(huì)話數(shù)據(jù),頁(yè)面重新加載后仍然存在,關(guān)閉瀏覽器選項(xiàng)卡時(shí)數(shù)據(jù)丟失。
(2)方法和屬性
Web Storage API 由 4 個(gè)方法 setItem()、getItem()、removeItem() 、clear()、key()和一個(gè) length 屬性組成,以 localStorage 為例:
- setItem() :用于存儲(chǔ)數(shù)據(jù),它有兩個(gè)參數(shù),即key和value。使用形式:localStorage.setItem(key, value);
- getItem():用于檢索數(shù)據(jù),它接受一個(gè)參數(shù) key,即需要訪問(wèn)其值得鍵。使用形式:localStorage.getItem(key);
- removeItem():用于刪除數(shù)據(jù),它接受一個(gè)參數(shù) key,即需要?jiǎng)h除其值得鍵。使用形式:localStorage.removeItem(key);
- clear() :用于清除其中存儲(chǔ)的所有數(shù)據(jù),使用形式:localStorage.clear();
- key():該方法用于獲取 localStorage 中數(shù)據(jù)的所有key,它接受一個(gè)數(shù)字作為參數(shù),該數(shù)字可以是 localStorage 項(xiàng)的索引位置。
console.log(typeof window.localStorage) // Object
// 存儲(chǔ)數(shù)據(jù)
localStorage.setItem("colorMode", "dark")
localStorage.setItem("username", "zhangsan")
localStorage.setItem("favColor", "green")
console.log(localStorage.length) // 3
// 檢索數(shù)據(jù)
console.log(localStorage.getItem("colorMode")) // dark
// 移除數(shù)據(jù)
localStorage.removeItem("colorMode")
console.log(localStorage.length) // 2
console.log(localStorage.getItem("colorMode")) // null
// 檢索鍵名
window.localStorage.key(0); // favColor
// 清空本地存儲(chǔ)
localStorage.clear()
console.log(localStorage.length) // 0
localStorage 和 sessionStorage 都非常適合緩存非敏感應(yīng)用數(shù)據(jù)。可以在需要存儲(chǔ)少量簡(jiǎn)單值并不經(jīng)常訪問(wèn)它們是使用它們。它們本質(zhì)上都是同步的,并且會(huì)阻塞主 UI 線程,所以應(yīng)該謹(jǐn)慎使用。
(3)存儲(chǔ)事件
我們可以在瀏覽器上監(jiān)聽(tīng) localStorage 和 sessionStorage 的存儲(chǔ)變化。storage 事件在創(chuàng)建、刪除或更新項(xiàng)目時(shí)觸發(fā)。偵聽(tīng)器函數(shù)在事件中傳遞,具有以下屬性:
- newValue:當(dāng)在存儲(chǔ)中創(chuàng)建或更新項(xiàng)目時(shí)傳遞給 setItem() 的值。當(dāng)從存儲(chǔ)中刪除項(xiàng)目時(shí),此值設(shè)置為 null。
- oldValue:創(chuàng)建新項(xiàng)目時(shí),如果該鍵存在于存儲(chǔ)中,則該項(xiàng)目的先前的值。
- key:正在更改的項(xiàng)目的鍵,如果調(diào)用 .clear(),則值為 null。
- url:執(zhí)行存儲(chǔ)操作的 URL。
- storageArea:執(zhí)行操作的存儲(chǔ)對(duì)象(localStorage 或 sessionStorage)。
通常,我們可以使用 window.addEventListener("storage", func) 或使用 onstorage 屬性(如 window.onstorage = func)來(lái)監(jiān)聽(tīng) storage 事件:
window.addEventListener('storage', e => {
console.log(e.key);
console.log(e.oldValu);
console.log(e.newValue);
});
window.onstorage = e => {
console.log(e.key);
console.log(e.oldValu);
console.log(e.newValue);
});
注意,該功能不會(huì)在發(fā)生更改的同一瀏覽器選項(xiàng)卡上觸發(fā),而是由同一域的其他打開(kāi)的選項(xiàng)卡或窗口觸發(fā)。此功能用于同步同一域的所有瀏覽器選項(xiàng)卡/窗口上的數(shù)據(jù)。因此,要對(duì)此進(jìn)行測(cè)試,需要打開(kāi)同一域的另一個(gè)選項(xiàng)卡。
(4)存儲(chǔ)限制
localStorage 和 sessionStorage 只能存儲(chǔ) 5 MB 的數(shù)據(jù),因此需要確保存儲(chǔ)的數(shù)據(jù)不會(huì)超過(guò)此限制。
localStorage.setItem('a', Array(1024 * 1024 * 5).join('a'))
localStorage.setItem('b', 'a')
// Uncaught DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of `a` exceeded the quota.
在上面的例子中,收到了一個(gè)錯(cuò)誤,首先創(chuàng)建了一個(gè)5MB的大字符串,當(dāng)再添加其他數(shù)據(jù)時(shí)就報(bào)錯(cuò)了。
另外,localStorage 和 sessionStorage 只接受字符串。可以通過(guò) JSON.stringify 和 JSON.parse 來(lái)解決這個(gè)問(wèn)題:
const user = {
name : "zhangsan",
age : 28,
gender : "male",
profession : "lawyer"
};
localStorage.setItem("user", JSON.stringify(user));
localStorage.getItem("user"); // '{"name":"zhangsan","age":28,"gender":"male","profession":"lawyer"}'
JSON.parse(localStorage.getItem("user")) // {name: 'zhangsan', age: 28, gender: 'male', profession: 'lawyer'}
如果我們直接將一個(gè)對(duì)象存儲(chǔ)在 localStorage 中,那將會(huì)在存儲(chǔ)之前進(jìn)行隱式類(lèi)型轉(zhuǎn)換,將對(duì)象轉(zhuǎn)換為字符串,再進(jìn)行存儲(chǔ):
const user = {
name : "zhangsan",
age : 28,
gender : "male",
profession : "lawyer"
};
localStorage.setItem("user", user);
localStorage.getItem("user"); // '[object Object]'
Web Storage 使用了同源策略,也就是說(shuō),存儲(chǔ)的數(shù)據(jù)只能在同一來(lái)源上可用。如果域和子域相同,則可以從不同的選項(xiàng)卡訪問(wèn) localStorage 數(shù)據(jù),而無(wú)法訪問(wèn) sessionStorage 數(shù)據(jù),即使它是完全相同的頁(yè)面。
另外:
- 無(wú)法在 web worker 或 service worker 中訪問(wèn) Web Storage;
- 如果瀏覽器設(shè)置為隱私模式,將無(wú)法讀取到 Web Storage;
- Web Storage 很容易被 XSS 攻擊,敏感信息不應(yīng)存儲(chǔ)在本地存儲(chǔ)中;
- 它是同步的,這意味著所有操作都是一次一個(gè)。對(duì)于復(fù)雜應(yīng)用,它會(huì)減慢應(yīng)用的運(yùn)行時(shí)間。
(5)示例
下面來(lái)看一個(gè)使用 localStorage 的簡(jiǎn)單示例,使用 localStorage 來(lái)存儲(chǔ)用戶偏好:
<input type="checkbox" id="darkTheme" name="darkTheme" onclick='onChange(this);'>
<label for="darkTheme">黑暗模式</label><br>
html {
background: white;
}
.dark {
background: black;
color: white;
}
function toggle(on) {
if (on) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
function save(on) {
localStorage.setItem('darkTheme', on.toString());
}
function load() {
return localStorage.getItem('darkTheme') === 'true';
}
function onChange(checkbox) {
const value = checkbox.checked;
toggle(value);
save(value);
}
const initialValue = load();
toggle(initialValue);
document.querySelector('#darkTheme').checked = initialValue;
這里的代碼很簡(jiǎn)單,頁(yè)面上有一個(gè)單選框,選中按鈕時(shí)將頁(yè)面切換為黑暗模式,并將這個(gè)配置存儲(chǔ)在 localStorage 中。當(dāng)下一次再初始頁(yè)面時(shí),獲取 localStorage 中的主題設(shè)置。
3. Cookie
(1)Cookie 概述
Cookie 主要用于身份驗(yàn)證和用戶數(shù)據(jù)持久性。Cookie 與請(qǐng)求一起發(fā)送到服務(wù)器,并在響應(yīng)時(shí)發(fā)送到客戶端;因此,cookies 數(shù)據(jù)在每次請(qǐng)求時(shí)都會(huì)與服務(wù)器交換。服務(wù)器可以使用 cookie 數(shù)據(jù)向用戶發(fā)送個(gè)性化內(nèi)容。嚴(yán)格來(lái)說(shuō),cookie 并不是客戶端存儲(chǔ)方式,因?yàn)榉?wù)器和瀏覽器都可以修改數(shù)據(jù)。它是唯一可以在一段時(shí)間后自動(dòng)使數(shù)據(jù)過(guò)期的方式。
每個(gè) HTTP 請(qǐng)求和響應(yīng)都會(huì)發(fā)送 cookie 數(shù)據(jù)。存儲(chǔ)過(guò)多的數(shù)據(jù)會(huì)使 HTTP 請(qǐng)求更加冗長(zhǎng),從而使應(yīng)用比預(yù)期更慢:
- 瀏覽器限制 cookie 的大小最大為4kb,特定域允許的 cookie 數(shù)量為 20 個(gè),并且只能包含字符串;
- cookie 的操作是同步的;
- 不能通過(guò) web workers 來(lái)訪問(wèn),但可以通過(guò)全局 window 對(duì)象訪問(wèn)。
Cookie 通常用于會(huì)話管理、個(gè)性化以及跨網(wǎng)站跟蹤用戶行為。我們可以通過(guò)服務(wù)端和客戶端設(shè)置和訪問(wèn) cookie。Cookie 還具有各種屬性,這些屬性決定了在何處以及如何訪問(wèn)和修改它們,
Cookie 分為兩種類(lèi)型:
- 會(huì)話 Cookie:沒(méi)有指定 Expires 或 Max-Age 等屬性,因此在關(guān)閉瀏覽器時(shí)會(huì)被刪除;
- 持久性 Cookie:指定 Expires 或 Max-Age 屬性。這些 cookie 在關(guān)閉瀏覽器時(shí)不會(huì)過(guò)期,但會(huì)在特定日期 (Expires) 或時(shí)間長(zhǎng)度 (Max-Age) 后過(guò)期。
(2)Cookie 操作
下面先來(lái)看看如何訪問(wèn)和操作客戶端和服務(wù)器上的 cookie。
① 客戶端(瀏覽器)
客戶端 JAVAScript 可以通過(guò) document.cookie 來(lái)讀取當(dāng)前位置可訪問(wèn)的所有 cookie。它提供了一個(gè)字符串,其中包含一個(gè)以分號(hào)分隔的 cookie 列表,使用 key=value 格式。
document.cookie;
可以看到,在語(yǔ)雀主頁(yè)中獲取 cookie,結(jié)果中包含了登錄的 cookie、語(yǔ)言、當(dāng)前主題等。
同樣,可以使用 document.cookie 來(lái)設(shè)置 cookie 的值,設(shè)置cookie也是用key=value格式的字符串,屬性用分號(hào)隔開(kāi):
document.cookie = "hello=world; domain=example.com; Secure";
這里用到了兩個(gè)屬性 SameSite 和 Secure,下面會(huì)介紹。如果已經(jīng)存在同名的 cookie 屬性,就會(huì)更新已有的屬性值,如果不存在,就會(huì)創(chuàng)建一個(gè)新的 key=value。
如果需要經(jīng)常在客戶端處理 Cookie,建議使用像 js-cookie 這樣的庫(kù)來(lái)處理客戶端 cookie:
Cookies.set('hello', 'world', { domain: 'example.com', secure: true });
Cookies.get('hello'); // -> world
這樣不僅為 cookie 上的 CRUD 操作提供了一個(gè)干凈的 API,而且還支持 TypeScript,從而幫助避免屬性的拼寫(xiě)錯(cuò)誤。
② 服務(wù)端(Node.js)
服務(wù)端可以通過(guò) HTTP 請(qǐng)求的請(qǐng)求頭和響應(yīng)頭來(lái)訪問(wèn)和修改 cookie。每當(dāng)瀏覽器向服務(wù)端發(fā)送 HTTP 請(qǐng)求時(shí),它都會(huì)使用 cookie 頭將所有相關(guān) cookie 都附加到該站點(diǎn)。請(qǐng)求標(biāo)頭是一個(gè)分號(hào)分隔的字符串。

這樣就可以從請(qǐng)求頭中讀取這些 cookie。如果在服務(wù)端使用 Node.js,可以像下面這樣從請(qǐng)求對(duì)象中讀取它們,將獲得以分號(hào)分隔的 key=value 對(duì):
http.createServer(function (request, response) {
const cookies = request.headers.cookie;
// "cookie1=value1; cookie2=value2"
...
}).listen(8124);
如果想要設(shè)置 cookie,可以在響應(yīng)頭中添加 Set-Cookie 頭,其中 cookie 采用 key=value 的格式,屬性用分號(hào)分隔:
response.writeHead(200, {
'Set-Cookie': 'mycookie=test; domain=example.com; Secure'
});
通常我們不會(huì)直接編寫(xiě) Node.js,而是與 ExpressJS 這樣的 Node.js 框架一起使用。使用 Express 可以更輕松地訪問(wèn)和修改 cookie。只需添加一個(gè)像 cookie-parser 這樣的中間件,就可以通過(guò) req.cookies 以 JavaScript 對(duì)象的形式獲得所有的 cookie。還可以使用 Express 內(nèi)置的 res.cookie() 方法來(lái)設(shè)置 cookie:
const express = require('express')
const cookieParser = require('cookie-parser')
const app = express()
app.use(cookieParser())
app.get('/', function (req, res) {
console.log('Cookies: ', req.cookies)
// Cookies: { cookie1: 'value1', cookie2: 'value2' }
res.cookie('name', 'tobi', { domain: 'example.com', secure: true })
})
app.listen(8080)
(3)Cookie 屬性
下面來(lái)深入了解 cookie 的屬性。除了名稱和值之外,cookie 還具有控制很多方面的屬性,包括安全方面、生命周期以及它們?cè)跒g覽器中的訪問(wèn)位置和方式等。
① Domain
Domain 屬性告訴瀏覽器允許哪些主機(jī)訪問(wèn) cookie。如果未指定,則默認(rèn)為設(shè)置 cookie 的同一主機(jī)。因此,當(dāng)使用客戶端 JavaScript 訪問(wèn) cookie 時(shí),只能訪問(wèn)與 URL 域相同的 cookie。同樣,只有與 HTTP 請(qǐng)求的域共享相同域的 cookie 可以與請(qǐng)求頭一起發(fā)送到服務(wù)端。
注意,擁有此屬性并不意味著可以為任何域設(shè)置 cookie,因?yàn)檫@顯然會(huì)帶來(lái)巨大的安全風(fēng)險(xiǎn)。此屬性存在的唯一原因就是減少域的限制并使 cookie 在子域上可訪問(wèn)。例如,如果當(dāng)前的域是 abc.xyz.com,并且在設(shè)置 cookie 時(shí)如果不指定 Domain 屬性,則默認(rèn)為 abc.xyz.com,并且 cookie 將僅限于該域。但是,可能希望相同的 cookie 也可用于其他子域,因此可以設(shè)置 Domain=xyz.com 以使其可用于其他子域,如 def.xyz.com 和主域 xyz.com。
② Path
此屬性指定訪問(wèn) cookie 必須存在的請(qǐng)求 URL 中的路徑。除了將 cookie 限制到域之外,還可以通過(guò)路徑來(lái)限制它。路徑屬性為 Path=/store 的 cookie 只能在路徑 /store 及其子路徑 /store/cart、/store/gadgets 等上訪問(wèn)。
③ Expires/Max-size
該屬性用來(lái)設(shè)置 cookie 的過(guò)期時(shí)間。若設(shè)置其值為一個(gè)時(shí)間,那么當(dāng)?shù)竭_(dá)此時(shí)間后,cookie 就會(huì)失效。不設(shè)置的話默認(rèn)值是 Session,意思是cookie會(huì)和session一起失效。當(dāng)瀏覽器關(guān)閉(不是瀏覽器標(biāo)簽頁(yè)) 后,cookie 就會(huì)失效。
除此之外,它還可以通過(guò)將過(guò)期日期設(shè)置為過(guò)去來(lái)刪除 cookie。
④ Secure
具有 Secure 屬性的 cookie 僅可以通過(guò)安全的 HTTPS 協(xié)議發(fā)送到服務(wù)器,而不會(huì)通過(guò) HTTP 協(xié)議。這有助于通過(guò)使 cookie 無(wú)法通過(guò)不安全的連接訪問(wèn)來(lái)防止中間人攻擊。除非網(wǎng)站實(shí)用不安全的 HTTP 連接,否則應(yīng)該始終將此屬性與所有 cookie 一起使用。
⑤ HTTPOnly
此屬性使 cookie 只能通過(guò)服務(wù)端訪問(wèn)。因此,只有服務(wù)斷可以通過(guò)響應(yīng)頭設(shè)置它們,然后瀏覽器會(huì)將它們與每個(gè)后續(xù)請(qǐng)求的頭一起發(fā)送到服務(wù)器,并且它們將無(wú)法通過(guò)客戶端 JavaScript 訪問(wèn)。
這可以在一定程度上幫助保護(hù)帶有敏感信息(如身份驗(yàn)證 token)的 cookie 免受 XSS 攻擊,因?yàn)槿魏慰蛻舳四_本都無(wú)法讀取 cookie。但這并不意味著可以完全免受 XSS 攻擊。因?yàn)椋绻粽呖梢栽诰W(wǎng)站上執(zhí)行第三方腳本,那可能無(wú)法訪問(wèn) cookie,相反,他們可以直接向服務(wù)端執(zhí)行相關(guān)的 API 請(qǐng)求。因此,想象一下用戶訪問(wèn)了一個(gè)頁(yè)面,黑客在網(wǎng)站上注入了惡意腳本。他們可以使用該腳本執(zhí)行任何 API,并在他們不知道的情況下代表用戶執(zhí)行操作。
(4)Cookie 工具庫(kù)
① Js Cookie(JavaScript)
Js Cookie 是一個(gè)簡(jiǎn)單、輕量級(jí)的 JavaScript API,用于處理瀏覽器 cookie。其支持 AMD、CommonJS 和 ES 模塊、沒(méi)有依賴關(guān)系、經(jīng)過(guò)徹底測(cè)試、支持自定義編碼和解碼、通用瀏覽器支持。
安裝:
npm i js-cookie
使用:
// 設(shè)置 Cookie
Cookies.set('cookie-name', 'cookie-value', { expires: 14})
// 讀取 Cookie
Cookies.get('cookie-name')
// 刪除 Cookie
Cookies.remove('cookie-name')
② React Cookie(React)
React Cookie 是一個(gè)專門(mén)用于 React 的 cookie 庫(kù),它繼承了 Universal Cookie 庫(kù)的功能。它提供了一組組件和 Hooks,使 React 中的 cookie 處理非常簡(jiǎn)單。如果使用的是 React 16.8+ 版本,就可以使用 hooks 來(lái)處理 cookie。否則,必須使用其提供的組件。
安裝:
npm i react-cookie
React Cookie 提供了 3 個(gè) Hook,分別是 cookie、setCookie 和 removeCookie。可以使用這些 Hook 來(lái)處理 React 應(yīng)用中的 cookie。
const [cookies, setCookie, removeCookie] = useCookies(['cookie-name']);
// 設(shè)置 Cookie
setCookie(name, value, [options]);
// 刪除 Cookie
removeCookie(name, [options])
③ Cookies(Node.js)
Cookies 是用于 HTTP cookie 配置的流行 NodeJS 模塊之一。可以輕松地將其與內(nèi)置的 NodeJS HTTP 庫(kù)集成或?qū)⑵溆米?Express 中間件。它允許使用 Keygrip 對(duì) cookie 進(jìn)行簽名以防止篡改、支持延遲 cookie 驗(yàn)證、不允許通過(guò)不安全的套接字發(fā)送安全 cookie、允許其他庫(kù)在不知道簽名機(jī)制的情況下訪問(wèn) cookie。
安裝:
npm install cookies
使用:
const cookie = require('cookie');
cookies = new Cookies( request, response, [ options ] )
// 讀取 cookies
cookies.get( name, [ options ] )
// 設(shè)置 cookies
cookies.set( name, [ value ], [ options ] )
4. IndexedDB
(1)概述
IndexedDB 提供了一個(gè)類(lèi)似 NoSQL 的 key/value 數(shù)據(jù)庫(kù),它可以存儲(chǔ)大量結(jié)構(gòu)化數(shù)據(jù),甚至是文件和 blob。每個(gè)域至少有 1GB 的可用空間,并且最多可以達(dá)到剩余磁盤(pán)空間的 60%。
IndexedDB 于 2011 年首次實(shí)現(xiàn),并于 2015 年 1 月成為 W3C 標(biāo)準(zhǔn),它具有良好的瀏覽器支持:

key/value 數(shù)據(jù)庫(kù)意味著存儲(chǔ)的所有數(shù)據(jù)都必須分配給一個(gè) key。它將key 與 value 相關(guān)聯(lián),key 用作該值的唯一標(biāo)識(shí)符,這意味著可以使用該 key 跟蹤該值。如果應(yīng)用需要不斷獲取數(shù)據(jù),key/value 數(shù)據(jù)庫(kù)使用非常高效且緊湊的索引結(jié)構(gòu)來(lái)快速可靠地通過(guò) key 定位值。使用該 key,不僅可以檢索存儲(chǔ)的值,還可以刪除、更新和替換該值。

在說(shuō) IndexedDB 之前,先來(lái)看一些相關(guān)術(shù)語(yǔ):
- 數(shù)據(jù)庫(kù): 一個(gè)域可以創(chuàng)建任意數(shù)量的 IndexedDB 數(shù)據(jù)庫(kù),只有同一域內(nèi)的頁(yè)面才能訪問(wèn)數(shù)據(jù)庫(kù)。
- object store:相關(guān)數(shù)據(jù)項(xiàng)的 key/value 存儲(chǔ)。它類(lèi)似于 MongoDB 中的集合或關(guān)系數(shù)據(jù)庫(kù)中的表。
- key:用于引用 object store 中每條記錄(值)的唯一名稱。它可以使用自動(dòng)增量數(shù)字生成,也可以設(shè)置為記錄中的任何唯一值。
- index:在 object store 中組織數(shù)據(jù)的另一種方式。搜索查詢只能檢查 key 或 index。
- schema:object store、key 和 index 的定義。
- version:分配給 schema 的版本號(hào)(整數(shù))。IndexedDB 提供自動(dòng)版本控制,因此可以將數(shù)據(jù)庫(kù)更新到最新 schema。
- 操作:數(shù)據(jù)庫(kù)活動(dòng),例如創(chuàng)建、讀取、更新或刪除記錄。

(2)特點(diǎn)及使用場(chǎng)景
indexedDB 特點(diǎn)如下:
- 可以將任何 JavaScript 類(lèi)型的數(shù)據(jù)存儲(chǔ)為鍵值對(duì),例如對(duì)象(blob、文件)或數(shù)組等。
- IndexedDB API 是異步的,不會(huì)在數(shù)據(jù)加載時(shí)停止頁(yè)面的渲染。
- 可以存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù),例如 Date、視頻、圖像對(duì)象等。
- 支持?jǐn)?shù)據(jù)庫(kù)事務(wù)和版本控制。
- 可以存儲(chǔ)大量數(shù)據(jù)。
- 可以在大量數(shù)據(jù)中快速定位/搜索數(shù)據(jù)。
- 數(shù)據(jù)庫(kù)是域?qū)S玫模虼巳魏纹渌军c(diǎn)都無(wú)法訪問(wèn)其他網(wǎng)站的 IndexedDB 存儲(chǔ),這也稱為同源策略。
IndexedDB 使用場(chǎng)景:
- 存儲(chǔ)用戶生成的內(nèi)容: 例如表單,在填寫(xiě)表單的過(guò)程中,用戶可以離開(kāi)并稍后再回來(lái)完成表單,存儲(chǔ)之后就不會(huì)丟失初始輸入的數(shù)據(jù)。
- 存儲(chǔ)應(yīng)用狀態(tài): 當(dāng)用戶首次加載網(wǎng)站或應(yīng)用時(shí),可以使用 IndexedDB 存儲(chǔ)這些初始狀態(tài)。可以是登錄身份驗(yàn)證、API 請(qǐng)求或呈現(xiàn) UI 之前所需的任何其他狀態(tài)。因此,當(dāng)用戶下次訪問(wèn)該站點(diǎn)時(shí),加載速度會(huì)增加,因?yàn)閼?yīng)用已經(jīng)存儲(chǔ)了狀態(tài),這意味著它可以更快地呈現(xiàn) UI。
- 對(duì)于離線工作的應(yīng)用: 用戶可以在應(yīng)用離線時(shí)編輯和添加數(shù)據(jù)。當(dāng)應(yīng)用程序來(lái)連接時(shí),IndexedDB 將處理并清空同步隊(duì)列中的這些操作。
(3)IndexedDB 操作
不同瀏覽器的 IndexedDB 可能使用不同的名稱。可以使用以下方法檢查 IndexedDB 支持:
const indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB ||
window.shimIndexedDB;
if (!indexedDB) {
console.log("不支持 IndexedDB");
}
可以使用 indexedDB.open() 來(lái)連接數(shù)據(jù)庫(kù):
const dbOpen = indexedDB.open('performance', 1);
indexedDB.open 的第一個(gè)參數(shù)是數(shù)據(jù)庫(kù)名稱,第二個(gè)參數(shù)是可選的版本整數(shù)。
可以使用以下三個(gè)事件處理函數(shù)監(jiān)聽(tīng) indexedDB 的連接狀態(tài):
① onerror
在無(wú)法建立 IndexedDB 連接時(shí),將觸發(fā)該事件:
// 連接失敗
dbOpen.onerror = e => {
reject(`IndexedDB error: ${ e.target.errorCode }`);
};
如果在無(wú)痕模式、隱私模式下運(yùn)行瀏覽器,可能不支持 IndexedDB,需要禁用這些模式。
② onupgradeneeded
一旦數(shù)據(jù)庫(kù)連接打開(kāi),就會(huì)觸發(fā) onupgradeneeded 事件,該事件可用于創(chuàng)建 object store。
dbOpen.onupgradeneeded = e => {
const db = dbOpen.result;
// 創(chuàng)建 object store
const store = db.createObjectStore("cars", { keyPath: "id" });
// 使用自動(dòng)遞增的id
// const store = db.createObjectStore('cars', { autoIncrement: true });
// 創(chuàng)建索引
store.createIndex("cars_colour", ["colour"], {
unique: true
});
// 創(chuàng)建復(fù)合索引
store.createIndex("colour_and_make", ["colour", "make"], {
unique: false,
});
};
IndexedDB 使用了 object store 的概念,其本質(zhì)上是數(shù)據(jù)集合的名稱。可以在單個(gè)數(shù)據(jù)庫(kù)中創(chuàng)建任意數(shù)量的 object store。keyPath是 IndexedDB 將用來(lái)識(shí)別對(duì)象字段名稱,通常是一個(gè)唯一的編號(hào),也可以通過(guò) autoIncrement: true 來(lái)自動(dòng)為 store 設(shè)置唯一遞增的 ID。除了普通的索引,還可以創(chuàng)建復(fù)合索引,使用多個(gè)關(guān)鍵詞的組合進(jìn)行查詢。
③ onsuccess
在連接建立并且所有升級(jí)都完成時(shí),將觸發(fā)該事件。上面我們已經(jīng)新建了 schema,接下來(lái)就可以在onsuccess 中添加、查詢數(shù)據(jù)。
// 連接成功
dbOpen.onsuccess = () => {
this.db = dbOpen.result;
//1
const transaction = db.transaction("cars", "readwrite");
//2
const store = transaction.objectStore("cars");
const colourIndex = store.index("cars_colour");
const makeModelIndex = store.index("colour_and_make");
//3
store.put({ id: 1, colour: "Red", make: "Toyota" });
store.put({ id: 2, colour: "Red", make: "Kia" });
store.put({ id: 3, colour: "Blue", make: "Honda" });
store.put({ id: 4, colour: "Silver", make: "Subaru" });
//4
const idQuery = store.get(4);
const colourQuery = colourIndex.getAll(["Red"]);
const colourMakeQuery = makeModelIndex.get(["Blue", "Honda"]);
// 5
idQuery.onsuccess = function () {
console.log('idQuery', idQuery.result);
};
colourQuery.onsuccess = function () {
console.log('colourQuery', colourQuery.result);
};
colourMakeQuery.onsuccess = function () {
console.log('colourMakeQuery', colourMakeQuery.result);
};
// 6
transaction.oncomplete = function () {
db.close();
};
};
這里總共有六部分:
- 為了對(duì)數(shù)據(jù)庫(kù)執(zhí)行操作,我們必須創(chuàng)建一個(gè) schema,一個(gè) schema 可以是單個(gè)操作,也可以是多個(gè)必須全部成功的操作,否則都不會(huì)成功;
- 這里用來(lái)獲取 cars object store 的引用以及對(duì)應(yīng)的索引;
- object store 上的 put 方法用于將數(shù)據(jù)添加到數(shù)據(jù)庫(kù)中;
- 這里就是數(shù)據(jù)的查詢,可以使用 keyPath 的值直接查詢項(xiàng)目(第14行);第15行中的 getAll 方法將返回一個(gè)包含它找到的每個(gè)結(jié)果的數(shù)組,我們正在根據(jù) cars_colour 索引來(lái)搜索 Red,應(yīng)該會(huì)查找到兩個(gè)結(jié)果。第16行根據(jù)復(fù)合索引查找顏色為Blue,并且品牌為 Honda 的結(jié)果。
- 搜索成功的事件處理函數(shù),它們將在查詢完成時(shí)觸發(fā)。
- 最后,在事務(wù)完成時(shí)關(guān)閉與數(shù)據(jù)庫(kù)連接。無(wú)需使用 IndexedDB 手動(dòng)觸發(fā)事務(wù),它會(huì)自行運(yùn)行。
運(yùn)行上面的代碼,就會(huì)得到以下結(jié)果:

可以在 Chrome Devtools 中查看:

下面來(lái)看看如何更新和刪除數(shù)據(jù)。
- 更新: 首先使用個(gè) get 來(lái)獲取需要更新的數(shù)據(jù),然后使用 store 上的 put 方法更新現(xiàn)有數(shù)據(jù)。put 是一種“插入或更新”方法,它要么覆蓋現(xiàn)有數(shù)據(jù),要么在新數(shù)據(jù)不存在時(shí)插入新數(shù)據(jù)。
const subaru = store.get(4);
subaru.onsuccess= function () {
subaru.result.colour = "Green";
store.put(subaru.result);
}
這會(huì)將數(shù)據(jù)庫(kù)中 Silver 色的 Subaru 的顏色更新為綠色。
- 刪除:可以使用 delete API 來(lái)刪除數(shù)據(jù),最簡(jiǎn)單的方法是通過(guò)其 key 來(lái)刪除:
const deleteCar = store.delete(1);
deleteCar.onsuccess = function () {
console.log("Removed");
};
如果不知道 key 并且希望根據(jù)值來(lái)刪除,可以這樣:
const redCarKey = colourIndex.getKey(["Red"]);
redCarKey.onsuccess = function () {
const deleteCar = store.delete(redCarKey.result);
deleteCar.onsuccess = function () {
console.log("Removed");
};
};
結(jié)果如下:

5. 存儲(chǔ)空間分析
可以使用基于 Promise 的 Storage API 檢查 Web Storage、IndexedDB 和 Cache API 的剩余空間。異步 .estimate() 方法返回:
- quota 屬性:可用的空間;
- usage 屬性:已用的空間。
(async () => {
if (!navigator.storage) return;
const storage = await navigator.storage.estimate();
console.log(`可用大小: ${ storage.quota / 1024 } Kb`);
console.log(`已用大小: ${ storage.usage / 1024 } Kb`);
console.log(`已用占比: ${ Math.round((storage.usage / storage.quota) * 100) }%`);
console.log(`剩余大小: ${ Math.floor((storage.quota - storage.usage) / 1024) } Kb`);
})();
Storage API 的瀏覽器兼容性如下:
