聊一聊常見的瀏覽器數(shù)據(jù)存儲方案
大家好,CUGGZ。
今天來分享常見的瀏覽器數(shù)據(jù)存儲方案:localStorage、sessionStorage、IndexedDB、Cookies。
1. 概述
現(xiàn)代瀏覽器中提供了多種存儲機(jī)制,打開瀏覽器的控制臺(Mac 可以使用 Command + Option + J 快捷鍵,Windows 可以使用 Control + Shift + J 快捷鍵)。選擇 Application 選項(xiàng)卡,可以在 Storage中 看到 Local Storage、Session Storage、IndexedDB、Web SQL、Cookies 等:
那數(shù)據(jù)存儲在瀏覽器中有什么使用場景呢?在以下情況下,將數(shù)據(jù)存儲在瀏覽器中成為更可行的選擇:
- 在瀏覽器存儲中保存應(yīng)用狀態(tài),比如保持用戶偏好(用戶特定的設(shè)置,例如亮模式或暗模式、字體大小等);
- 創(chuàng)建離線工作的漸進(jìn)式 Web 應(yīng)用,除了初始下載和更新之外沒有服務(wù)器端要求;
- 緩存靜態(tài)應(yīng)用資源,如 HTML、CSS、JS 和圖像等;
- 保存上一個瀏覽會話中的數(shù)據(jù),例如存儲上一個會話中的購物車內(nèi)容,待辦事項(xiàng)列表中的項(xiàng)目,記住用戶是否以前登錄過等。
無論哪種方式,將這些信息保存在客戶端可以減少額外且不必要的服務(wù)器調(diào)用,并幫助提供離線支持。不過,需要注意,由于實(shí)現(xiàn)差異,瀏覽器存儲機(jī)制在不同瀏覽器中的行為可能會有所不同。除此之外,許多瀏覽器已刪除對 Web SQL 的支持,建議將現(xiàn)有用法遷移到 IndexedDB。
所以下面我們將介紹 Local Storage、Session Storage、IndexedDB、Cookies 的使用方式、使用場景以及它們之間的區(qū)別。
2. Web Storage
(1)概述
HTML5 引入了 Web Storage,這使得在瀏覽器中存儲和檢索數(shù)據(jù)變得更加容易。Web Storage API 為客戶端瀏覽器提供了安全存儲和輕松訪問鍵值對的機(jī)制。Web Storage 提供了兩個 API 來獲取和設(shè)置純字符串的鍵值對:
- localStorage:用于存儲持久數(shù)據(jù),除非用戶手動將其從瀏覽器中刪除,否則數(shù)據(jù)將終身存儲。即使用戶關(guān)閉窗口或選項(xiàng)卡,它也不會過期;
- sessionStorage:用于存儲臨時會話數(shù)據(jù),頁面重新加載后仍然存在,關(guān)閉瀏覽器選項(xiàng)卡時數(shù)據(jù)丟失。
(2)方法和屬性
Web Storage API 由 4 個方法 setItem()、getItem()、removeItem() 、clear()、key()和一個 length 屬性組成,以 localStorage 為例:
- setItem()? :用于存儲數(shù)據(jù),它有兩個參數(shù),即key和value?。使用形式:localStorage.setItem(key, value);
- getItem()?:用于檢索數(shù)據(jù),它接受一個參數(shù) key,即需要訪問其值的鍵。使用形式:localStorage.getItem(key);
- removeItem()?:用于刪除數(shù)據(jù),它接受一個參數(shù) key,即需要刪除其值的鍵。使用形式:localStorage.removeItem(key);
- clear()? :用于清除其中存儲的所有數(shù)據(jù),使用形式:localStorage.clear();
- key():該方法用于獲取 localStorage 中數(shù)據(jù)的所有key,它接受一個數(shù)字作為參數(shù),該數(shù)字可以是 localStorage 項(xiàng)的索引位置。
localStorage 和 sessionStorage 都非常適合緩存非敏感應(yīng)用數(shù)據(jù)??梢栽谛枰鎯ι倭亢唵沃挡⒉唤?jīng)常訪問它們是使用它們。它們本質(zhì)上都是同步的,并且會阻塞主 UI 線程,所以應(yīng)該謹(jǐn)慎使用。
(3)存儲事件
我們可以在瀏覽器上監(jiān)聽 localStorage 和 sessionStorage 的存儲變化。storage 事件在創(chuàng)建、刪除或更新項(xiàng)目時觸發(fā)。偵聽器函數(shù)在事件中傳遞,具有以下屬性:
- newValue:當(dāng)在存儲中創(chuàng)建或更新項(xiàng)目時傳遞給 setItem() 的值。當(dāng)從存儲中刪除項(xiàng)目時,此值設(shè)置為 null。
- oldValue:創(chuàng)建新項(xiàng)目時,如果該鍵存在于存儲中,則該項(xiàng)目的先前的值。
- key:正在更改的項(xiàng)目的鍵,如果調(diào)用 .clear(),則值為 null。
- url:執(zhí)行存儲操作的 URL。
- storageArea:執(zhí)行操作的存儲對象(localStorage 或 sessionStorage)。
通常,我們可以使用 window.addEventListener("storage", func) 或使用 onstorage 屬性(如 window.onstorage = func)來監(jiān)聽 storage 事件:
注意,該功能不會在發(fā)生更改的同一瀏覽器選項(xiàng)卡上觸發(fā),而是由同一域的其他打開的選項(xiàng)卡或窗口觸發(fā)。此功能用于同步同一域的所有瀏覽器選項(xiàng)卡/窗口上的數(shù)據(jù)。因此,要對此進(jìn)行測試,需要打開同一域的另一個選項(xiàng)卡。
(4)存儲限制
localStorage 和 sessionStorage 只能存儲 5 MB 的數(shù)據(jù),因此需要確保存儲的數(shù)據(jù)不會超過此限制。
在上面的例子中,收到了一個錯誤,首先創(chuàng)建了一個5MB的大字符串,當(dāng)再添加其他數(shù)據(jù)時就報(bào)錯了。
另外,localStorage 和 sessionStorage 只接受字符串??梢酝ㄟ^ JSON.stringify 和 JSON.parse 來解決這個問題:
如果我們直接將一個對象存儲在 localStorage 中,那將會在存儲之前進(jìn)行隱式類型轉(zhuǎn)換,將對象轉(zhuǎn)換為字符串,再進(jìn)行存儲:
Web Storage 使用了同源策略,也就是說,存儲的數(shù)據(jù)只能在同一來源上可用。如果域和子域相同,則可以從不同的選項(xiàng)卡訪問 localStorage 數(shù)據(jù),而無法訪問 sessionStorage 數(shù)據(jù),即使它是完全相同的頁面。
另外:
- 無法在 web worker 或 service worker 中訪問 Web Storage;
- 如果瀏覽器設(shè)置為隱私模式,將無法讀取到 Web Storage;
- Web Storage 很容易被 XSS 攻擊,敏感信息不應(yīng)存儲在本地存儲中;
- 它是同步的,這意味著所有操作都是一次一個。對于復(fù)雜應(yīng)用,它會減慢應(yīng)用的運(yùn)行時間。
(5)示例
下面來看一個使用 localStorage 的簡單示例,使用 localStorage 來存儲用戶偏好:
這里的代碼很簡單,頁面上有一個單選框,選中按鈕時將頁面切換為黑暗模式,并將這個配置存儲在 localStorage 中。當(dāng)下一次再初始頁面時,獲取 localStorage 中的主題設(shè)置。
3. Cookie
(1)Cookie 概述
Cookie 主要用于身份驗(yàn)證和用戶數(shù)據(jù)持久性。Cookie 與請求一起發(fā)送到服務(wù)器,并在響應(yīng)時發(fā)送到客戶端;因此,cookies 數(shù)據(jù)在每次請求時都會與服務(wù)器交換。服務(wù)器可以使用 cookie 數(shù)據(jù)向用戶發(fā)送個性化內(nèi)容。嚴(yán)格來說,cookie 并不是客戶端存儲方式,因?yàn)榉?wù)器和瀏覽器都可以修改數(shù)據(jù)。它是唯一可以在一段時間后自動使數(shù)據(jù)過期的方式。
每個 HTTP 請求和響應(yīng)都會發(fā)送 cookie 數(shù)據(jù)。存儲過多的數(shù)據(jù)會使 HTTP 請求更加冗長,從而使應(yīng)用比預(yù)期更慢:
瀏覽器限制 cookie 的大小最大為4kb,特定域允許的 cookie 數(shù)量為 20 個,并且只能包含字符串;
cookie 的操作是同步的;
不能通過 web workers 來訪問,但可以通過全局 window 對象訪問。
Cookie 通常用于會話管理、個性化以及跨網(wǎng)站跟蹤用戶行為。我們可以通過服務(wù)端和客戶端設(shè)置和訪問 cookie。Cookie 還具有各種屬性,這些屬性決定了在何處以及如何訪問和修改它們,
Cookie 分為兩種類型:
會話 Cookie:沒有指定 Expires 或 Max-Age 等屬性,因此在關(guān)閉瀏覽器時會被刪除;
持久性 Cookie:指定 Expires 或 Max-Age 屬性。這些 cookie 在關(guān)閉瀏覽器時不會過期,但會在特定日期 (Expires) 或時間長度 (Max-Age) 后過期。
(2)Cookie 操作
下面先來看看如何訪問和操作客戶端和服務(wù)器上的 cookie。
① 客戶端(瀏覽器)
客戶端 JavaScript 可以通過 document.cookie 來讀取當(dāng)前位置可訪問的所有 cookie。它提供了一個字符串,其中包含一個以分號分隔的 cookie 列表,使用 key=value 格式。
可以看到,在語雀主頁中獲取 cookie,結(jié)果中包含了登錄的 cookie、語言、當(dāng)前主題等。
同樣,可以使用 document.cookie 來設(shè)置 cookie 的值,設(shè)置cookie也是用key=value格式的字符串,屬性用分號隔開:
這里用到了兩個屬性 SameSite 和 Secure,下面會介紹。如果已經(jīng)存在同名的 cookie 屬性,就會更新已有的屬性值,如果不存在,就會創(chuàng)建一個新的 key=value。
如果需要經(jīng)常在客戶端處理 Cookie,建議使用像 js-cookie 這樣的庫來處理客戶端 cookie:
這樣不僅為 cookie 上的 CRUD 操作提供了一個干凈的 API,而且還支持 TypeScript,從而幫助避免屬性的拼寫錯誤。
② 服務(wù)端(Node.js)
服務(wù)端可以通過 HTTP 請求的請求頭和響應(yīng)頭來訪問和修改 cookie。每當(dāng)瀏覽器向服務(wù)端發(fā)送 HTTP 請求時,它都會使用 cookie 頭將所有相關(guān) cookie 都附加到該站點(diǎn)。請求標(biāo)頭是一個分號分隔的字符串。
這樣就可以從請求頭中讀取這些 cookie。如果在服務(wù)端使用 Node.js,可以像下面這樣從請求對象中讀取它們,將獲得以分號分隔的 key=value 對:
如果想要設(shè)置 cookie,可以在響應(yīng)頭中添加 Set-Cookie 頭,其中 cookie 采用 key=value 的格式,屬性用分號分隔:
通常我們不會直接編寫 Node.js,而是與 ExpressJS 這樣的 Node.js 框架一起使用。使用 Express 可以更輕松地訪問和修改 cookie。只需添加一個像 cookie-parser 這樣的中間件,就可以通過 req.cookies 以 JavaScript 對象的形式獲得所有的 cookie。還可以使用 Express 內(nèi)置的 res.cookie() 方法來設(shè)置 cookie:
(3)Cookie 屬性
下面來深入了解 cookie 的屬性。除了名稱和值之外,cookie 還具有控制很多方面的屬性,包括安全方面、生命周期以及它們在瀏覽器中的訪問位置和方式等。
① Domain
Domain 屬性告訴瀏覽器允許哪些主機(jī)訪問 cookie。如果未指定,則默認(rèn)為設(shè)置 cookie 的同一主機(jī)。因此,當(dāng)使用客戶端 JavaScript 訪問 cookie 時,只能訪問與 URL 域相同的 cookie。同樣,只有與 HTTP 請求的域共享相同域的 cookie 可以與請求頭一起發(fā)送到服務(wù)端。
注意,擁有此屬性并不意味著可以為任何域設(shè)置 cookie,因?yàn)檫@顯然會帶來巨大的安全風(fēng)險(xiǎn)。此屬性存在的唯一原因就是減少域的限制并使 cookie 在子域上可訪問。例如,如果當(dāng)前的域是 abc.xyz.com,并且在設(shè)置 cookie 時如果不指定 Domain 屬性,則默認(rèn)為 abc.xyz.com,并且 cookie 將僅限于該域。但是,可能希望相同的 cookie 也可用于其他子域,因此可以設(shè)置 Domain=xyz.com 以使其可用于其他子域,如 def.xyz.com 和主域 xyz.com。
② Path
此屬性指定訪問 cookie 必須存在的請求 URL 中的路徑。除了將 cookie 限制到域之外,還可以通過路徑來限制它。路徑屬性為 Path=/store 的 cookie 只能在路徑 /store 及其子路徑 /store/cart、/store/gadgets 等上訪問。
③ Expires/Max-size
該屬性用來設(shè)置 cookie 的過期時間。若設(shè)置其值為一個時間,那么當(dāng)?shù)竭_(dá)此時間后,cookie 就會失效。不設(shè)置的話默認(rèn)值是 Session,意思是cookie會和session一起失效。當(dāng)瀏覽器關(guān)閉(不是瀏覽器標(biāo)簽頁) 后,cookie 就會失效。
除此之外,它還可以通過將過期日期設(shè)置為過去來刪除 cookie。
④ Secure
具有 Secure 屬性的 cookie 僅可以通過安全的 HTTPS 協(xié)議發(fā)送到服務(wù)器,而不會通過 HTTP 協(xié)議。這有助于通過使 cookie 無法通過不安全的連接訪問來防止中間人攻擊。除非網(wǎng)站實(shí)用不安全的 HTTP 連接,否則應(yīng)該始終將此屬性與所有 cookie 一起使用。
⑤ HTTPOnly
此屬性使 cookie 只能通過服務(wù)端訪問。因此,只有服務(wù)斷可以通過響應(yīng)頭設(shè)置它們,然后瀏覽器會將它們與每個后續(xù)請求的頭一起發(fā)送到服務(wù)器,并且它們將無法通過客戶端 JavaScript 訪問。
這可以在一定程度上幫助保護(hù)帶有敏感信息(如身份驗(yàn)證 token)的 cookie 免受 XSS 攻擊,因?yàn)槿魏慰蛻舳四_本都無法讀取 cookie。但這并不意味著可以完全免受 XSS 攻擊。因?yàn)?,如果攻擊者可以在網(wǎng)站上執(zhí)行第三方腳本,那可能無法訪問 cookie,相反,他們可以直接向服務(wù)端執(zhí)行相關(guān)的 API 請求。因此,想象一下用戶訪問了一個頁面,黑客在網(wǎng)站上注入了惡意腳本。他們可以使用該腳本執(zhí)行任何 API,并在他們不知道的情況下代表用戶執(zhí)行操作。
(4)Cookie 工具庫
① Js Cookie(JavaScript)
Js Cookie 是一個簡單、輕量級的 JavaScript API,用于處理瀏覽器 cookie。其支持 AMD、CommonJS 和 ES 模塊、沒有依賴關(guān)系、經(jīng)過徹底測試、支持自定義編碼和解碼、通用瀏覽器支持。
安裝:
使用:
② React Cookie(React)
React Cookie 是一個專門用于 React 的 cookie 庫,它繼承了 Universal Cookie 庫的功能。它提供了一組組件和 Hooks,使 React 中的 cookie 處理非常簡單。如果使用的是 React 16.8+ 版本,就可以使用 hooks 來處理 cookie。否則,必須使用其提供的組件。
安裝:
React Cookie 提供了 3 個 Hook,分別是 cookie、setCookie 和 removeCookie??梢允褂眠@些 Hook 來處理 React 應(yīng)用中的 cookie。
③ Cookies(Node.js)
Cookies 是用于 HTTP cookie 配置的流行 NodeJS 模塊之一??梢暂p松地將其與內(nèi)置的 NodeJS HTTP 庫集成或?qū)⑵溆米?Express 中間件。它允許使用 Keygrip 對 cookie 進(jìn)行簽名以防止篡改、支持延遲 cookie 驗(yàn)證、不允許通過不安全的套接字發(fā)送安全 cookie、允許其他庫在不知道簽名機(jī)制的情況下訪問 cookie。
安裝:
使用:
4. IndexedDB
(1)概述
IndexedDB 提供了一個類似 NoSQL 的 key/value 數(shù)據(jù)庫,它可以存儲大量結(jié)構(gòu)化數(shù)據(jù),甚至是文件和 blob。每個域至少有 1GB 的可用空間,并且最多可以達(dá)到剩余磁盤空間的 60%。
IndexedDB 于 2011 年首次實(shí)現(xiàn),并于 2015 年 1 月成為 W3C 標(biāo)準(zhǔn),它具有良好的瀏覽器支持:
key/value 數(shù)據(jù)庫意味著存儲的所有數(shù)據(jù)都必須分配給一個 key。它將key 與 value 相關(guān)聯(lián),key 用作該值的唯一標(biāo)識符,這意味著可以使用該 key 跟蹤該值。如果應(yīng)用需要不斷獲取數(shù)據(jù),key/value 數(shù)據(jù)庫使用非常高效且緊湊的索引結(jié)構(gòu)來快速可靠地通過 key 定位值。使用該 key,不僅可以檢索存儲的值,還可以刪除、更新和替換該值。
在說 IndexedDB 之前,先來看一些相關(guān)術(shù)語:
- 數(shù)據(jù)庫: 一個域可以創(chuàng)建任意數(shù)量的 IndexedDB 數(shù)據(jù)庫,只有同一域內(nèi)的頁面才能訪問數(shù)據(jù)庫。
- object store:相關(guān)數(shù)據(jù)項(xiàng)的 key/value 存儲。它類似于 MongoDB 中的集合或關(guān)系數(shù)據(jù)庫中的表。
- key:用于引用 object store 中每條記錄(值)的唯一名稱。它可以使用自動增量數(shù)字生成,也可以設(shè)置為記錄中的任何唯一值。
- index:在 object store 中組織數(shù)據(jù)的另一種方式。搜索查詢只能檢查 key 或 index。
- schema:object store、key 和 index 的定義。
- version:分配給 schema 的版本號(整數(shù))。IndexedDB 提供自動版本控制,因此可以將數(shù)據(jù)庫更新到最新 schema。
- 操作:數(shù)據(jù)庫活動,例如創(chuàng)建、讀取、更新或刪除記錄。
(2)特點(diǎn)及使用場景
indexedDB 特點(diǎn)如下:
- 可以將任何 JavaScript 類型的數(shù)據(jù)存儲為鍵值對,例如對象(blob、文件)或數(shù)組等。
- IndexedDB API 是異步的,不會在數(shù)據(jù)加載時停止頁面的渲染。
- 可以存儲結(jié)構(gòu)化數(shù)據(jù),例如 Date、視頻、圖像對象等。
- 支持?jǐn)?shù)據(jù)庫事務(wù)和版本控制。
- 可以存儲大量數(shù)據(jù)。
- 可以在大量數(shù)據(jù)中快速定位/搜索數(shù)據(jù)。
- 數(shù)據(jù)庫是域?qū)S玫模虼巳魏纹渌军c(diǎn)都無法訪問其他網(wǎng)站的 IndexedDB 存儲,這也稱為同源策略。
IndexedDB 使用場景:
存儲用戶生成的內(nèi)容: 例如表單,在填寫表單的過程中,用戶可以離開并稍后再回來完成表單,存儲之后就不會丟失初始輸入的數(shù)據(jù)。
存儲應(yīng)用狀態(tài): 當(dāng)用戶首次加載網(wǎng)站或應(yīng)用時,可以使用 IndexedDB 存儲這些初始狀態(tài)??梢允堑卿浬矸蒡?yàn)證、API 請求或呈現(xiàn) UI 之前所需的任何其他狀態(tài)。因此,當(dāng)用戶下次訪問該站點(diǎn)時,加載速度會增加,因?yàn)閼?yīng)用已經(jīng)存儲了狀態(tài),這意味著它可以更快地呈現(xiàn) UI。
對于離線工作的應(yīng)用: 用戶可以在應(yīng)用離線時編輯和添加數(shù)據(jù)。當(dāng)應(yīng)用程序來連接時,IndexedDB 將處理并清空同步隊(duì)列中的這些操作。
(3)IndexedDB 操作
不同瀏覽器的 IndexedDB 可能使用不同的名稱??梢允褂靡韵路椒z查 IndexedDB 支持:
可以使用 indexedDB.open() 來連接數(shù)據(jù)庫:
indexedDB.open 的第一個參數(shù)是數(shù)據(jù)庫名稱,第二個參數(shù)是可選的版本整數(shù)。
可以使用以下三個事件處理函數(shù)監(jiān)聽 indexedDB 的連接狀態(tài):
① onerror
在無法建立 IndexedDB 連接時,將觸發(fā)該事件:
如果在無痕模式、隱私模式下運(yùn)行瀏覽器,可能不支持 IndexedDB,需要禁用這些模式。
② onupgradeneeded
一旦數(shù)據(jù)庫連接打開,就會觸發(fā) onupgradeneeded 事件,該事件可用于創(chuàng)建 object store。
IndexedDB 使用了 object store 的概念,其本質(zhì)上是數(shù)據(jù)集合的名稱??梢栽趩蝹€數(shù)據(jù)庫中創(chuàng)建任意數(shù)量的 object store。keyPath是 IndexedDB 將用來識別對象字段名稱,通常是一個唯一的編號,也可以通過 autoIncrement: true 來自動為 store 設(shè)置唯一遞增的 ID。除了普通的索引,還可以創(chuàng)建復(fù)合索引,使用多個關(guān)鍵詞的組合進(jìn)行查詢。
③ onsuccess
在連接建立并且所有升級都完成時,將觸發(fā)該事件。上面我們已經(jīng)新建了 schema,接下來就可以在onsuccess 中添加、查詢數(shù)據(jù)。
這里總共有六部分:
為了對數(shù)據(jù)庫執(zhí)行操作,我們必須創(chuàng)建一個 schema,一個 schema 可以是單個操作,也可以是多個必須全部成功的操作,否則都不會成功;
這里用來獲取 cars object store 的引用以及對應(yīng)的索引;
object store 上的 put 方法用于將數(shù)據(jù)添加到數(shù)據(jù)庫中;
這里就是數(shù)據(jù)的查詢,可以使用 keyPath 的值直接查詢項(xiàng)目(第14行);第15行中的 getAll 方法將返回一個包含它找到的每個結(jié)果的數(shù)組,我們正在根據(jù) cars_colour 索引來搜索 Red,應(yīng)該會查找到兩個結(jié)果。第16行根據(jù)復(fù)合索引查找顏色為Blue,并且品牌為 Honda 的結(jié)果。
搜索成功的事件處理函數(shù),它們將在查詢完成時觸發(fā)。
最后,在事務(wù)完成時關(guān)閉與數(shù)據(jù)庫連接。無需使用 IndexedDB 手動觸發(fā)事務(wù),它會自行運(yùn)行。
運(yùn)行上面的代碼,就會得到以下結(jié)果:
可以在 Chrome Devtools 中查看:
下面來看看如何更新和刪除數(shù)據(jù)。
更新: 首先使用個 get 來獲取需要更新的數(shù)據(jù),然后使用 store 上的 put 方法更新現(xiàn)有數(shù)據(jù)。put 是一種“插入或更新”方法,它要么覆蓋現(xiàn)有數(shù)據(jù),要么在新數(shù)據(jù)不存在時插入新數(shù)據(jù)。
這會將數(shù)據(jù)庫中 Silver 色的 Subaru 的顏色更新為綠色。
刪除:可以使用 delete API 來刪除數(shù)據(jù),最簡單的方法是通過其 key 來刪除:
如果不知道 key 并且希望根據(jù)值來刪除,可以這樣:
結(jié)果如下:
5. 存儲空間分析
可以使用基于 Promise 的 Storage API 檢查 Web Storage、IndexedDB 和 Cache API 的剩余空間。異步 .estimate() 方法返回:
- quota 屬性:可用的空間;
- usage 屬性:已用的空間。
Storage API 的瀏覽器兼容性如下: