一文讀懂瀏覽器本地存儲(chǔ):Web Storage
一、 簡介
瀏覽器本地存儲(chǔ)是指瀏覽器提供的一種機(jī)制,允許 Web 應(yīng)用程序在瀏覽器端存儲(chǔ)數(shù)據(jù),以便在用戶下次訪問時(shí)可以快速獲取和使用這些數(shù)據(jù)。一共兩種存儲(chǔ)方式:localStorage 和 sessionStorage。下面介紹下兩種緩存的特性和在內(nèi)部平臺(tái)的一些應(yīng)用。
二、localStorage 和 sessionStorage
2.1、區(qū)別
localStorage 和 sessionStorage 的主要區(qū)別是生命周期,具體區(qū)別如下:
localStorage | sessionStorage | |
生命周期 | 持久化存儲(chǔ):除非自行刪除或清除緩存,否則一直存在 | 會(huì)話級別的存儲(chǔ):瀏覽器標(biāo)簽頁或窗口關(guān)閉 |
作用域 | 相同瀏覽器,同域名,不同標(biāo)簽,不同窗口 | 相同瀏覽器,同域名,同源窗口 |
獲取方式 | window.localStorage | window.sessionStorage |
存儲(chǔ)容量 | 5M | 5M |
容量限制的目的是防止濫用本地存儲(chǔ)空間,導(dǎo)致用戶瀏覽器變慢。
2.2、瀏覽器兼容性
1)現(xiàn)在的瀏覽器基本上都是支持這兩種 Storage 特性的。各瀏覽器支持版本如下:
Chrome | Firefox | IE | Opera | Safari | Android | Opera Mobile | Safari Mobile | |
localStorage | 4 | 3.5 | 8 | 10.5 | 4 | 2.1 | 11 | iOS 3.2 |
sessionStorage | 5 | 2 | 8 | 10.5 | 4 | 2.1 | 11 | iOS 3.2 |
2)如果使用的是老式瀏覽器,比如Internet Explorer 6、7 或者其他,就需要在使用前檢測瀏覽器是否支持本地存儲(chǔ)或者是否被禁用。以 localStorage 為例:
if(window.localStorage){
alert("瀏覽器支持 localStorage");
} else {
alert("瀏覽器不支持 localStorage");
}
3)某些瀏覽器版本使用過程中,會(huì)出現(xiàn) Storage 不能正常使用的情況,記得添加 try/catch。以 localStorage 為例:
if(window.localStorage){
try {
localStorage.setItem("username", "name");
alert("瀏覽器支持 localStorage");
} catch (e) {
alert("瀏覽器支持 localStorage 后不可使用");
}
} else {
alert("瀏覽器不支持 localStorage");
}
三、使用說明
3.1、API介紹
localStorage 和 sessionStorage 提供了相同的方法進(jìn)行存儲(chǔ)、檢索和刪除。常用的方法如下:
- 設(shè)置數(shù)據(jù):setItem(key, value)
存儲(chǔ)的值可以是字符串、數(shù)字、布爾、數(shù)組和對象。對象和數(shù)組必須轉(zhuǎn)換為 string 進(jìn)行存儲(chǔ)。JSON.parse() 和 JSON.stringify() 方法可以將數(shù)組、對象等值類型轉(zhuǎn)換為字符串類型,從而存儲(chǔ)到 Storage 中;
localStorage.setItem("username", "name"); // "name"
localStorage.setItem("count", 1); // "1"
localStorage.setItem("isOnline", true); // "true"
sessionStorage.setItem("username", "name");
// user 存儲(chǔ)時(shí),先使用 JSON 序列化,否則保存的是[object Object]
const user = { "username": "name" };
localStorage.setItem("user", JSON.stringify(user));
sessionStorage.setItem("user", JSON.stringify(user));
eg:數(shù)據(jù)沒有序列化,導(dǎo)致保存的數(shù)據(jù)異常
圖片
- 獲取數(shù)據(jù):getItem(key)
如果 key 對應(yīng)的 value 獲取不到,則返回值是 null;
const usernameLocal = localStorage.getItem("username");
const usernameSession = sessionStorage.getItem("username");
// 獲取到的數(shù)據(jù)為string,使用時(shí)反序列化數(shù)據(jù)
const userLocal = JSON.parse(localStorage.getItem("user"));
const userSession = JSON.parse(sessionStorage.getItem("user"));
- 刪除數(shù)據(jù):removeItem(key);
localStorage.removeItem("username");
sessionStorage.removeItem("username");
- 清空數(shù)據(jù):clear();
localStorage.clear();
sessionStorage.clear();
- 在不確定是否存在 key 的情況下,可以使用 hasOwnProperty() 進(jìn)行檢查;
localStorage.hasOwnProperty("userName"); // true
sessionStorage.hasOwnProperty("userName"); // false
- 當(dāng)然,也可以使用 Object.keys() 查看所有存儲(chǔ)數(shù)據(jù)的鍵;
Object.keys(localStorage); // ['username']
Object.keys(sessionStorage);
3.2、瀏覽器查看
本地存儲(chǔ)的內(nèi)容可以在瀏覽器中直接查看,以 Chrome 為例,按住鍵盤 F12 進(jìn)入開發(fā)者工具后,選擇 Application,然后就能在左邊 Storage 列表中找到 localStorage 和 sessionStorgae。
圖片
3.3、監(jiān)聽
當(dāng)存儲(chǔ)的數(shù)據(jù)發(fā)生變化時(shí),其他頁面通過監(jiān)聽 storage 事件,來獲取變更前后的值,以及根據(jù)值的變化來處理頁面的展示邏輯。
JS 原生監(jiān)聽事件,只能夠監(jiān)聽同源非同一個(gè)頁面中的 storage 事件,如果想監(jiān)聽同一個(gè)頁面的,需要改寫原生方法,拋出自定義事件來監(jiān)聽。具體如下:
- 監(jiān)聽同源非同一個(gè)頁面
直接在其他頁面添加監(jiān)聽事件即可。
eg:同域下的 A、B 兩個(gè)頁面,A 修改了 localStorage,B 頁面可以監(jiān)聽到 storage 事件。
window.addEventListener("storage", () => {
// 監(jiān)聽 username 值變化
if (e.key === "username") {
console.log("username 舊值:" + e.oldValue + ",新值:" + e.newValue);
}
})
注:
- 當(dāng)兩次 setItem 更新的值一樣時(shí),監(jiān)聽方法是不會(huì)觸發(fā)的;
- storage 事件只能監(jiān)聽到 localStorage 的變化。
- 監(jiān)聽同一個(gè)頁面
重寫 Storage 的 setItem 事件,同理,也可以監(jiān)聽刪除事件 removeItem 和獲取事件 getItem。
(() => {
const originalSetItem = localStorage.setItem;
// 重寫 setItem 函數(shù)
localStorage.setItem = function (key, val) {
let event = new Event("setItemEvent");
event.key = key;
event.newValue = val;
window.dispatchEvent(event);
originalSetItem.apply(this, arguments);
};
})();
window.addEventListener("setItemEvent", function (e) {
// 監(jiān)聽 username 值變化
if (e.key === "username") {
const oldValue = localStorage.getItem(e.key);
console.log("username 舊值:" + oldValue + ",新值:" + e.newValue);
}
});
四、存儲(chǔ)
瀏覽器默認(rèn)能夠存儲(chǔ) 5M 的數(shù)據(jù),但實(shí)際上,瀏覽器并不會(huì)為其分配特定的存儲(chǔ)空間,而是根據(jù)當(dāng)前瀏覽器的空閑空間來判斷能夠分配多少存儲(chǔ)空間。
4.1、存儲(chǔ)容量
可以使用 Storage 的 length 屬性,對存儲(chǔ)容量進(jìn)行測算,以 localStorage 為例:
let str = "0123456789";
let temp = "";
// 先生成一個(gè) 10KB 的字符串
while (str.length !== 10240) {
str = str + "0123456789";
}
// 清空
localStorage.clear();
// 計(jì)算總量
const computedTotal = () => {
return new Promise((resolve) => {
// 往 localStorage 中累積存儲(chǔ) 10KB
const timer = setInterval(() => {
try {
localStorage.setItem("temp", temp);
} catch (e) {
// 報(bào)錯(cuò)說明超出最大存儲(chǔ)
resolve(temp.length / 1024);
clearInterval(timer);
// 統(tǒng)計(jì)完記得清空
localStorage.clear();
}
temp += str;
}, 0);
});
};
// 計(jì)算使用量
const computedUse = () => {
let cache = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
cache += localStorage.getItem(key).length;
}
}
return (cache / 1024).toFixed(2);
};
(async () => {
const total = await computedTotal();
let use = "0123456789";
for (let i = 0; i < 1000; i++) {
use += "0123456789";
}
localStorage.setItem("use", use);
const useCache = computedUse();
console.log(`最大容量${total}KB`);
console.log(`已用${useCache}KB`);
console.log(`剩余可用容量${total - useCache}KB`);
})();
可見在 Chrome 瀏覽器下,localStorage 有 5M 容量。
圖片
4.2、存儲(chǔ)性能
在某些特殊場景下,需要存儲(chǔ)大數(shù)據(jù),為了更好的利用 Storage 的存儲(chǔ)空間,可以采取以下解決方案,但不應(yīng)該過于頻繁地將大量數(shù)據(jù)存儲(chǔ)在 Storage 中,因?yàn)樵趯懭霐?shù)據(jù)時(shí),會(huì)對整個(gè)頁面進(jìn)行阻塞(不推薦這種方式)。
- 壓縮數(shù)據(jù)
可以使用數(shù)據(jù)壓縮庫對 Storage 中的數(shù)據(jù)進(jìn)行壓縮,從而減小數(shù)據(jù)占用的存儲(chǔ)空間。
eg:使用 lz-string 庫的 compress() 函數(shù)將數(shù)據(jù)進(jìn)行壓縮,并將壓縮后的數(shù)據(jù)存儲(chǔ)到 localStorage 中。
const LZString = require("lz-string");
const data = "This is a test message";
// 壓縮
const compressedData = LZString.compress(data);
localStorage.setItem("test", compressedData);
// 解壓
const decompressedData = LZString.decompress(localStorage.getItem("test"));
- 分割數(shù)據(jù)
將大的數(shù)據(jù)分割成多個(gè)小的片段存儲(chǔ)到 Storage 中,從而減小單個(gè)數(shù)據(jù)占用的存儲(chǔ)空間。
eg:將用戶數(shù)據(jù)分割為單項(xiàng)存儲(chǔ)到 localStorage 中。
for (const key in userInfo) {
localStorage.setItem(key, userInfo[key]);
}
圖片
- 取消不必要的數(shù)據(jù)存儲(chǔ)
可以在代碼中取消一些不必要的數(shù)據(jù)存儲(chǔ),從而減小占用空間。
eg:只存儲(chǔ)用到的用戶名、公司主體和后端所在環(huán)境字段信息。
for (const key in userInfo) {
if (["userName", "legalEntityName", "isOnline"].includes(key)) {
localStorage.setItem(key, userInfo[key]);
}
}
圖片
- 設(shè)置過期時(shí)間
localStorage 是不支持過期時(shí)間的,在存儲(chǔ)信息過多后,會(huì)拖慢瀏覽器速度,也會(huì)因?yàn)闉g覽器存儲(chǔ)容量不夠而報(bào)錯(cuò),可以封裝一層邏輯來實(shí)現(xiàn)設(shè)置過期時(shí)間,以達(dá)到清理的目的。
// 設(shè)置
function set(key, value){
const time = new Date().getTime(); //獲取當(dāng)前時(shí)間
localStorage.setItem(key, JSON.stringify({value, time})); //轉(zhuǎn)換成json字符串
}
// 獲取
function get(key, exp){
// exp 過期時(shí)間
const value = localStorage.getItem(key);
const valueJson = JSON.parse(value);
//當(dāng)前時(shí)間 - 存儲(chǔ)的創(chuàng)建時(shí)間 > 過期時(shí)間
if(new Date().getTime() - valueJson.time > exp){
console.log("expires"); //提示過期
} else {
console.log("value:" + valueJson.value);
}
}
五、應(yīng)用
5.1、使用習(xí)慣記錄
用來緩存一些篩選項(xiàng)數(shù)據(jù),保存用戶習(xí)慣信息,起到避免多次重復(fù)操作的作用。
eg:在 beetle 工程列表中,展示了自已權(quán)限下所有 beetle 的項(xiàng)目,對于參與項(xiàng)目多和參與項(xiàng)目少的人,操作習(xí)慣是不同的,由此,記錄收藏功能狀態(tài)解決了這一問題,讓篩選項(xiàng)記住用戶選擇,方便下次使用。
圖片
圖片
在開發(fā)使用中,直接獲取 localStorage.getItem('isFavor') 作為默認(rèn)值展示,切換后使用 localStorage.setItem() 方法更新保存內(nèi)容。
// 獲取
const isFavor = localStorage.getItem('isFavor');
this.state = {
isFavor: isFavor !== null ? Number(isFavor) : EngineeringTypeEnum.FAVOR,
};
// 展示默認(rèn)值
<Form.Item name='isFavor' initialValue={this.state.isFavor}>
<Select
placeholder='篩選收藏的工程'
onChange={(e) => this.changeFavor(e)}
{...searchSmallFormProps}
>
{EngineeringTypeEnum.property.map(e => (<Option key={e.id} value={e.id}>{e.name}</Option>))}
</Select>
</Form.Item>
// 變更
changeFavor = (e) => {
localStorage.setItem('isFavor', e);
this.setState({ isFavor: e });
};
5.2、首次打開提示
用來緩存用戶導(dǎo)覽,尤其是只需要出現(xiàn)一次的操作說明彈窗等。
eg:當(dāng)?shù)谝淮位蛘咔蹇站彺婧蟮卿洠撁嫘枰霈F(xiàn)操作指南和用戶手冊的彈窗說明。
圖片
在開發(fā)使用中,注意存儲(chǔ)的數(shù)據(jù)類型為 string,轉(zhuǎn)成布爾值是為了在插件中方便控制彈窗的顯示隱藏。
// 獲取
const operationVisible = localStorage.getItem('operationVisible');
this.state = {
operationVisible: operationVisible === null || operationVisible === 'true' ? true : false,
};
// 控制展示
<Modal
title='操作指南'
open={this.state.operationVisible}
onCancel={() => {
this.setState({ operationVisible: false });
localStorage.setItem('operationVisible', false);
}}
footer={null}
destroyOnClose={true}
>
<Divider orientation='left'>動(dòng)作</Divider>
<p>接口 》 用例 》 用例集,3級結(jié)構(gòu)滿足不了后續(xù)的使用,因此增加【動(dòng)作】這一層級,【動(dòng)作】是接口測試的最小單元,多個(gè)【動(dòng)作】可以組合成一個(gè)用例,多個(gè)用例可以聚合為用例集;</p>
<Image src={OperationGuidePng} preview={false} />
</Modal>
5.3、減少重復(fù)訪問接口
在瀏覽頁面時(shí),會(huì)遇到一些經(jīng)常訪問但返回?cái)?shù)據(jù)不更新的接口,這種特別適合用做頁面緩存,只在頁面打開的時(shí)候訪問一次,其他時(shí)間獲取緩存數(shù)據(jù)即可。
eg:在我們的一些內(nèi)部系統(tǒng)中,用戶信息是每個(gè)頁面都要用到的,尤其是 userId 字段,會(huì)與每個(gè)獲取數(shù)據(jù)接口掛鉤,但這個(gè)數(shù)據(jù)是不會(huì)變的,一直請求是沒有意義的,為減少接口的訪問次數(shù),可以將主要數(shù)據(jù)緩存在 localStorage 內(nèi),方便其他接口獲取。
六、總結(jié)
希望通過此篇文章,可以讓大家了解 Web Storage 在瀏覽器數(shù)據(jù)存儲(chǔ)和讀取的相關(guān)操作,以及相關(guān)事件和限制。
它可以用于保存用戶的偏好設(shè)置、表單數(shù)據(jù)等,在開發(fā)中使用可以方便的存儲(chǔ)和讀取數(shù)據(jù),提高用戶體驗(yàn)。當(dāng)然,在使用時(shí)需要特別注意它的限制,以及在存儲(chǔ)、讀取和刪除數(shù)據(jù)過程中的錯(cuò)誤處理。
關(guān)于作者
劉筱雨,轉(zhuǎn)轉(zhuǎn)工程效率組內(nèi)成員,主要負(fù)責(zé)公司內(nèi)部公共系統(tǒng)前端項(xiàng)目。