【收藏就會】瀏覽器WebStorage緩存使用指南

⚠️本文爲掘金社區首發簽約文章,未獲受權禁止轉載前端

背景

在咱們網頁刷新的時候,頁面上全部數據都會被清空。而在一些網站的搜索上,即便是你關閉了瀏覽器,下次打開時仍是會有數據在頁面上,以下圖一個簡單的搜索記錄功能,當用戶進行搜索時,全部的記錄會被保存起來,不管是刷新仍是重啓瀏覽器,搜索的歷史記錄依舊顯示在頁面上。java

image.png

這一系列的需求均可以經過瀏覽器的存儲技術來實現。本篇文章,咱們就來學習下瀏覽器存儲技術中的WebStorage ,全面瞭解它的基礎使用和進階,以及如何利用這些方法實踐兩個常見場景。介紹使用方法的同時,我也會以封裝一個工具類的方式來統一全部調用方法,學會這一點,可讓你在業務開發調用的時候更加方便。web

爲何選擇WebStorage?

咱們知道,常見的輕量瀏覽器存儲技術包括CookieWebStorage。那麼,咱們爲何選擇WebStorage而不是Cookie呢?面試

首先,WebStorage在使用上相比Cookie更友好,再也不須要刻意封裝成一些工具庫來對一些常見的操做進行簡化的調用,儘管市面上已經有不少成熟的方案幫咱們作了這件事情。chrome

其次,chrome(80+)瀏覽器默認屏蔽全部三方Cookie已經不是什麼值得震驚的事情了,隨着這一次改動,Cookie無疑又被斬斷了一隻有力的手臂。 不瞭解的小夥伴,強烈安利一手這篇文章,裏面很是詳細的對其進行了一些分析。當瀏覽器全面禁用三方 Cookietypescript

除此以外,使用Cookie還須要面臨如下問題:bootstrap

  • 存放數據過小,Cookie的存儲大小隻有4k,若是你須要存儲的數據很是多,那麼很顯然並不可以知足你的需求,且通常沒有人這麼作。
  • 每次都會攜帶在HTTP請求頭中,會與服務端進行一些交互,當我單純存儲一些本地數據時,很明顯會形成性能浪費。

WebStorage在瀏覽器中的主要功能,就是在客戶端進行臨時和永久的數據存儲,不直接參與服務端的通訊和交互,所以能夠很好地避免一些劫持的安全風險。同時,也具有了良好的存儲容量,能勝任絕大部份的應用場景,且每一個存儲都是掛載在對應的空間當中,彼此獨立去管理對應的數據,不會形成串數據和錯數據的一些困擾。後端

基於此,若是有須要存儲到本地的一些數據,仍是儘量使用WebStorage來作爲存儲的首要選擇。瀏覽器

基礎使用

在本章節,我會從一個封裝工具類的角度帶你們學習一些webStorage的基礎使用技巧。這裏也先分享一個在線的源碼地址連接:Storage操做封裝實踐代碼緩存

而後,在瀏覽器調試工具的Application菜單當中,左側能夠看到Storage的調試版,其中就有咱們經過API保存到存儲當中的值,能夠在這裏進行調試。

image.png

環境支持 & 初始化

在開始前第一步確定是須要作一些環境檢查,否則在部分不支持這些特性的瀏覽器下是沒法使用的,這個能夠在caniuse上查看一些瀏覽器支持程度。

image.png

而在咱們的代碼中,也要加上一層容錯判斷,若是須要對其作兼容的話能夠進行一個處理降解。如Cookie或者是IE6中userData持久化用戶數據

下面是一個比較簡單的判斷,也能夠封裝成爲一個簡單的函數來進行調用。若是瀏覽器不支持則拋出一些錯誤到控制檯當中。

class CustomStorage {
  private readStorage: Storage

  constructor () {
    if (!window) {
        throw new Error('當前環境非瀏覽器,沒法消費全局window實例。')
    }
    if (!window.localStorage) {
        throw new Error('當前環境非沒法使用localStorage')
    }
    if (!window.sessionStorage) {
        throw new Error('當前環境非沒法使用sessionStorage')
    }
  }
}
複製代碼

當環境支持使用WebStorage的條件下,就能夠初始化默認的一些數據了,在這裏選擇使用哪一個Storage,同時將配置保存起來。

interface StorageBootStrapConfig {
  /** 當前環境 */
  mode: 'session' | 'local',
  
  /** 超時時間 */
  timeout: number
}

/** * 初始化Storage的數據 * @param config StorageBootStrapConfig */
 bootStrap (config: StorageBootStrapConfig): void {
  switch (config.mode) {
    case 'session':
      this.readStorage = window.sessionStorage
      break;

    case 'local':
      this.readStorage = window.localStorage
      break;
  
    default:
      throwErrorMessage('當前配置的mode未再配置區內,能夠檢查傳入配置。')
      break;
  }
  this.config = config
}
複製代碼

那麼,經過bootstrap來初始化當前的一些配置後,在頁面裏就能夠經過當前實例customStorage去使用一些函數方法。

import CustomStorage from 'web-storage-db'

const customStorage = new CustomStorage()

customStorage.bootStrap({
  mode: 'local',
  timeout: 3000,
})

export default customStorage
複製代碼

JSON序列化

對於WebStorage來講,值的存儲是很是依賴JSON的序列化。以下圖:

image.png

當存入Object類型時,存入的數據會變成其類型的字符串,由於WebStorage的存儲只能以字符串的形式存在,因此咱們想要存儲引用類型的數據,就須要依賴JSON序列化的能力了。經過stringifyparse等一些方法對值作出處理,就能很好的存儲一些引用類型。

可是也有一些JSON.stringify不友好的類型數據,儘可能不要去存儲,如undefined, Function, Symbol等等,我在這裏也寫了一個簡單的函數用於檢查存儲值。

/** * 判斷當前值是否可以唄JSON.stringify識別 * @param data 須要判斷的值 * @returns 前參數是否能夠string化 */
export function hasStringify (data: any): boolean {
  if (data === undefined) {
    return false
  }

  if (data instanceof Function) {
    return false
  }

  if (isSymbol(data)) {
    return false
  }

  return true
}
複製代碼

其中isSymbol方法作了一個Symbol類型值的判斷。

/** * 判斷當前類型是不是Symbol * @param val 須要判斷的值 * @returns 當前參數是不是symbol */
export function isSymbol(val: any): boolean {
  return typeof val === 'symbol'
}
複製代碼

存入數據

若是須要將數據存儲到WebStorage當中,其自己提供一個setItem的API 來作這件事情,在這裏以localStorage爲例子,能夠經過如下形式來存入一個值:

// # 原生
window.localStorage.setItem('key', 'value')

// # attribute形式存儲
window.localStorage['key1'] = 'value'
window.localStorage.name = 'wangly19'
複製代碼

image.png

而咱們在使用中,顯然不會去使用原生API的方式處理,絕大部分都會封裝成一個工具方法,來處理一些重複性的工做。就好比在下面的封裝中,我就對存儲數據的內容作了一層包裝,加入了JSON序列化數據過時時間

/** * 設置當前 * @param key 設置當前存儲key * @param value 設置當前存儲value */
 setItem(key: string, value) {
  if (hasStringify(value)) {
    const saveData: StorageSaveFormat = {
      timestamp: new Date().getTime(),
      data: value
    }
    console.log(saveData, 'saveData')
    this.readStorage.setItem(key, JSON.stringify(saveData))
  } else {
    throwErrorMessage('須要存儲的data不支持JSON.stringify方法,請檢查當前數據')
  }
}


// 使用
customStorage.setItem('setItem', [1])
複製代碼

image.png

讀取數據

既然有存入,那麼必然會有讀取,咱們能夠經過getItem或者是Object的形式進行值的讀取。下面,咱們就來看看三種方式的實例吧。

image.png

window.localStorage.setItem('person', JSON.stringify({ 
    name: 'wangly19', 
    age: 22 
}))

const person = window.localStorage.getItem('person')


JSON.parse(person)

// { name: "wangly19", age: 22 }
複製代碼

image.png

上面是普通的使用方式,而咱們封裝時,也會對存入的數據進行一些判斷,將存入的JSON數據作一個解析化的處理,直接返回解析後的數據,更加的方便和易於使用。

/** * 獲取數據 * @param key 獲取當前數據key * @returns 存儲數據 */
getItem<T = any>(key: string): T | null {
  const content: StorageSaveFormat | null = JSON.parse(this.readStorage.getItem(key))
  return content?.data || null
}

// # 使用
customStorage.getItem('setItem') // [1]
複製代碼

移除

對於存儲的移除不只可使用removeItemdelete等操做來對存儲中的值進行移除

// # removeItem
window.localStorage.removeItem('person')

// # delete
delete window.localStorage.person
delete window.localStorage['peson']
複製代碼

還可使用clear來清除存儲中全部的數據。

window.localStorage.clear()
複製代碼

此外,若是移除某條數據時Storage沒有存儲當前key的數據,那麼咱們就不須要去執行當前移除數據的操做。咱們來看下面封裝的removeItem方法,我加入了一層值是否存在的判斷來決定是否是真的須要執行移除這步操做。

/** * 移除一條數據 * @param key 移除key */
removeItem(key: string) {
  if (this.hasItem(key)) {
      this.readStorage.removeItem(key)
  }
}

/** * 清除存儲中全部數據 */
clearAll() {
  this.readStorage.clear()
}
複製代碼

長度length

WebStorage自帶length屬性,能夠獲取當前Storage的長度。

window.localStorage.length

/** * 返回當前存儲庫大小 * @returns number */
size(): number {
    return this.readStorage.length
}
複製代碼

image.png

keys 和 values

看到這裏,不少朋友應該知道會怎麼實現了吧?沒錯,經過Object.keysObject.values能夠拿到當前Storage中全部的keyvalue。部分埋點SDK會有上報Storage來作數據篩選。

Object.keys(localStorage)
// (4) ["wwwPassLogout", "BIDUPSID", "BDSUGSTORED", "safeIconHis"]
Object.values(localStorage)
// (4) ["0", "30B3EE0AF6EE9F4F89EF16486C288502", "[{\"q\":\"localstorage%20%E8%BF%87%E6%9C%9F%E6%97%B6%…:\"new%20dateshijianchuo\",\"s\":4,\"t\":206989341223}]", ""]
複製代碼

其次就是經過key(index)方法,能夠直接獲取某個位置的值。

window.localStorage.key(0)
window.localStorage.key(1)
window.localStorage.key(2)
複製代碼

工具類當中,我也對其進行了封裝,可使用getKeys, getValues來獲取存儲空間的全部KeyValue的集合。

/** * 獲取全部key * @returns 回storage當中全部key集合 */
getKeys(): Array < string > {
  return Object.keys(this.readStorage)
}

/** * 獲取全部value * @returns 全部數據集合 */
getValues() {
  return Object.values(this.readStorage)
}

// # 使用
customStorage.getKeys()
customStorage.getValues()
複製代碼

image.png

是否存在某個屬性?

判斷當前Storage中是否存在某個屬性,不少同窗都是經過getItem去獲取一個值,而後判斷value是否存在進行一個判斷。

可是很顯然,咱們可以像操做Object的hasOwnProperty方法來判斷當前是否有這個屬性,因爲返回的是boolean類型,相對來講更易於理解。

localStorage.key(2)
// "BDSUGSTORED"
localStorage.hasOwnProperty('BDSUGSTORED')
// true
localStorage.hasOwnProperty('1111')
// false
複製代碼

image.png

基於此,我也封裝了判斷存儲中是否存在該值的hasItem方法,用於作一些key是否在存儲中存在的一些判斷。

/** * 判斷是否存在該屬性 * @param key 須要判斷的key */
hasItem(key: string): boolean {
  return this.readStorage.hasOwnProperty(key)
}
複製代碼

進階使用

在進階使用當中,我會介紹一些工做中可能會碰到的問題,而且給出一些解決方案

過時時間

WebStorageSessionStorage的一個週期是當前會話。而localStorage則若是不手動清除,則不會主動清除存儲的數據。

關鍵詞,面試會問:localStorage若是不是主動清除,存儲數據是不會過時的。

因此,不少時候若是須要過時時間則須要開發者本身去處理,而處理的方式也很是簡單暴力。 那就是給予存儲值時帶一個時間。參考下面代碼,經過new Date().getTime()來取到當前時間,而後設置到存儲當中去。

const person = {
    // 存儲數據
    data: {
        name: 'wangly19',
        age: 22
    },
    // 過時時間
    timestamp: new Date().getTime()
}
window.localStorage.setItem('person', JSON.stringify(person))
複製代碼

獲取時間的時候,會進行一個簡單的判斷,當前時間 - 存儲時間 >= 過時時間,這樣就可以在值操做的時候作一些判斷處理。

// # 原生

let person = localStorage.getItem('person')
person = JSON.parse(val)

// 這裏可使用一些庫在作處理,如`dayjs`
if(new Date().getTime() - person.timestamp > [過時時間]) {
    // 數據已通過期的一些操做
} else {
    // 正常處理
}
複製代碼

所以,須要在原有的getItem的方法上,添加一條過時時間的判斷,我也直接封裝在函數內處理這一份邏輯。

/** * 獲取數據 * @param key 獲取當前數據key * @returns 存儲數據 */
getItem<T = any>(key: string): T | null {
  const content: StorageSaveFormat | null = JSON.parse(this.readStorage.getItem(key))
  if (content?.timestamp && new Date().getTime() - content.timestamp >= this.config.timeout) {
    this.removeItem(key)
    return null
  }
  return content?.data || null
}
複製代碼

監聽函數

WebStorage修改時,會觸發瀏覽器storage事件。

而在應用中可使用addEventListener添加一個storage事件對其進行綁定。

而這個觸發機制能夠看下圖。在不一樣窗口對storage觸發的時候會輸出當前的event信息。在event當中,咱們能夠拿到觸發的url,新值, 舊值, 觸發的key等信息,咱們能夠經過這個API去作一些瀏覽器URL監聽的事情。

<script>
 document.body.innerHTML = '初始化數據'
 window.addEventListener("storage", function (event) {
 const values = {
 url: event.url,
 key: event.key,
 old: event.oldValue,
 new: event.newValue,
 }
 document.body.innerHTML = JSON.stringify(values)
 });
 </script>
複製代碼

修改數據

因爲原生沒有changeItem這類的方法,所以咱們須要本身去作一些方法的封裝來方便咱們頻繁的須要去修改存儲當中數據。

以下面的一個相似於useState回調的形式來作一些值的修改。

changeItem('name', (oldValue) => {
    const name = `update: ${oldValue} update`
    return name
})
複製代碼

實現方式也相對比較易懂,經過getItem先獲取數據,而後在經過setItem設置onChange回調函數的值,將一個連貫的操做串聯起來。

/** * 修改當前存儲內容數據 * @param key 當前存儲key * @param onChange 修改函數 * @param baseValue 基礎數據 */
 changeItem<S = any>(
  key: string, 
  onChange: (oldValue: S) => S | null, baseValue?: any
) {
  const data = this.getItem<S>(key)
  this.setItem(key, onChange(data || baseValue))
}

// # 使用
customStorage.changeItem('key', (oldValue) => {
    retutn oldValue + 'newUpadte'
})
複製代碼

空間 & 溢出

若是是重度使用用戶,如一些文檔構建項目,每每不少都是會往localStorage中存不少數據,不少開發者都會擔憂會不會直接溢出

因此在這裏,也設想了一些解決方案來處理這些問題。

存儲狀態 & StorageEstimate

在安全的上下文和支持的瀏覽器下,經過StorageEstimate能夠獲取到當前瀏覽器的一個緩存狀況,如:使用多少, 總共多少。

以下代碼,首先判斷了瀏覽器是否存在navigator,而後繼續判斷了navigator是否有storage,最後再去執行estimate異步獲取咱們的存儲信息。

if (navigator && navigator.storage) {
    navigator.storage.estimate().then(estimate => {
        console.log(estimate)
    });
}
複製代碼

image.png

該Web API須要當前項目在https下。獲取到的quota(存儲總量)相對來講在3M左右,在開發場景下,這絕對是一個安全的內存範圍。

緩存溢出清理

若是是在內存瀕臨溢出的場景下,那麼咱們就須要釋放一些空間來作處理後面的數據修改了。 首先咱們對帶有時間的數據進行彙總排序,以下方法就是將storage中全部帶有timestamp字段的數據彙總後進行排序。

/** * 獲取當前清除存儲空間,而且進行排序 */
getClearStorage() {
  const keys: string[] = Object.keys(this.readStorage)
  const db: Array<{
    key: string,
    data: StorageSaveFormat
  }> = []
  keys.forEach(name => {
    const item = this.getItem(name)
    if (item.timestamp) {
      db.push({
        key: name,
        data: item
      })
    }
  })
  return db.sort((a, b) => {
    return a.data.timestamp - b.data.timestamp
  })
}
複製代碼

當擁有了一個排序好的數據列表時,就須要考慮數據清空了,按照時間線將距離當前越久的時間清除。而這個時候,須要理解一個條件: 總大小(quota) - (使用大小)usage > [當前存入大小currentSize]

當咱們有一個排序好的存儲時,只須要循環判斷當前空間是否知足需求便可,若是知足跳出循環。反之繼續異步,直到咱們的空間夠爲止。

initCacheSize單純對容量數據最一個刷新。獲取新的容量數據。

/** * 容量清理,直到知足存儲大小爲止 */
detectionStorageContext(currentSize: number) {
  if (this.usage + currentSize >= this.quota) {
      const storage = this.getClearStorage()
      for (let { key, data } of storage) {
          // 若是知足要求就跳出,還不夠就繼續清除。
          if (this.usage + currentSize < this.quota) break
          // 刷新容量大小
          this.removeItem(key)
          initCacheSize()
      }
  }
}
複製代碼

最後一步就是在setItem中執行detectionStorageContext, 每次更新存儲內容都會先判斷下是否要溢出,若是添加或者修改的數據會溢出,那麼我就會作一個空間清理了。

實踐場景

本章節,主要講述了一些簡單的WebStorage的使用場景。

搜索歷史

到這裏,咱們的一個工具類就已經基本成型了。最後,再回到一開始的案例中,咱們就能夠經過工具類中的changItem迅速的實現這個搜索歷史的功能,而沒必要關心一些數據兼容上的問題。咱們須要關注的只是存儲值的設置。

image.png

事例代碼以下:

export default function Search() {
  const [searchList, setSearchList] = useState([]);

  useEffect(() => {
    const data = localStore.getItem('search')
    setSearchList(data || [])
  }, [])

  const onSearch = (value) => {
    if (value) {
      localStore.changeItem(
        'search',
        (oldValue) => {
          if (oldValue.includes(value)) {
            return oldValue;
          }

          if (oldValue) {
            const newValue = [...oldValue, value];
            setSearchList(newValue);
            console.log(newValue, 'value');
            return newValue;
          }

          if (value) {
            setSearchList([value]);
            return [value];
          }

          return [];
        },
        [],
      );
    }
  };

  return (
    <div className="demo-app"> <Search placeholder="請輸入搜索內容" enterButton="Search" size="large" suffix={suffix} onSearch={onSearch} /> <div className="tag-wrapper"> {searchList.map((e) => { return ( <Tag key={e} style={{ margin: 10, }} color="#108ee9" > {e} </Tag> ); })} </div> </div>
  );
}
複製代碼

圖片數據

瀏覽器對於請求是有限制的,而咱們項目中絕大部份圖片實際上是經過後端接口進行返回的,在這裏以emoji表情包作個例子。

咱們拿知乎的表情包數據來進行一個模擬,發現一共有73條數據,若是每次刷新網頁都請求一次後端數據是一件很是難受的事情,而這些數據顯然也不須要存放在Store當中,在必定的時間中,發生改變的概率很小,那麼咱們將它放在本地存儲顯然是一個不錯的選擇。

image.png

在頁面加載時,我會對接口數據請求加一層判斷,只有數據爲空時纔會請求後端圖標數據列表。若是是過時時間的話,獲取數據時會清空本地圖標數據,而後從新請求後端圖標數據,在從新放入緩存中而且更新新的過時時間。

const emojiRef = useRef(localStore.getItem('emoji'));

useEffect(() => {
    if (!emojiRef.current) {
      fetchEmojiIcon()
    }
  })
複製代碼

若是你項目中存在大量的資源路徑,能夠將其放在localStorage中進行存儲,方便須要用到時進行使用。

image.png

資源 & 資料

總結

本文對WebStorage中絕大部分使用技巧都作了一些使用的總結,將經常使用的一些操做存儲方法都進行了封裝,同時也對工做中常常碰到的一些複雜場景,如過時時間、數據更改、緩存溢出等功能進行了一些敘述,最後將其封裝到了工具類 當中,方便在平常開發中進行調用。

最後在對WebStorage有了一些瞭解以後,那麼咱們在後續工做中,是否是能夠思考有些數據能夠考慮放到存儲當中去?在節省資源的同時,也能有更好的性能,同時也緩解了部分服務端的壓力。

近期好文

尾註

若是本文對你有幫助,但願可以給我點一讚支持一下。

本文首發於:掘金技術社區 類型:簽約文章 做者:wangly19 收藏於專欄:javaScript基礎進階 公衆號: ItCodes 程序人生

相關文章
相關標籤/搜索