詳解瀏覽器存儲

前言

隨着移動網絡的發展與演化,我們手機上現在除了有原生 App,還能跑“WebApp”——它即開即用,用完即走。一個優秀的 WebApp 甚至可以擁有和原生 App 媲美的功能和體驗。WebApp 優異的性能表現,有一部分原因要歸功於瀏覽器存儲技術的提升。cookie存儲數據的功能已經很難滿足開發所需,逐漸被Web Storage、IndexedDB所取代,本文將介紹這幾種存儲方式的差異和優缺點。

一、cookie

1.cookie的來源

cookie 的本職工作並非本地存儲,而是“維持狀態”。因爲HTTP協議是無狀態的,HTTP協議自身不對請求和響應之間的通信狀態進行保存,通俗來說,服務器不知道用戶上一次做了什麼,這嚴重阻礙了交互式Web應用程序的實現。在典型的網上購物場景中,用戶瀏覽了幾個頁面,買了一盒餅乾和兩瓶飲料。最後結帳時,由於HTTP的無狀態性,不通過額外的手段,服務器並不知道用戶到底買了什麼,於是就誕生了cookie。它就是用來繞開HTTP的無狀態性的“額外手段”之一。服務器可以設置或讀取cookie中包含信息,藉此維護用戶跟服務器會話中的狀態。

在剛纔的購物場景中,當用戶選購了第一項商品,服務器在向用戶發送網頁的同時,還發送了一段cookie,記錄着那項商品的信息。當用戶訪問另一個頁面,瀏覽器會把cookie發送給服務器,於是服務器知道他之前選購了什麼。用戶繼續選購飲料,服務器就在原來那段Cookie裏追加新的商品信息。結帳時,服務器讀取發送來的cookie就行了。

2.什麼是cookie

cookie指某些網站爲了辨別用戶身份而儲存在用戶本地終端上的數據(通常經過加密)。 cookie是服務端生成,客戶端進行維護和存儲,存儲在內存或者磁盤中。通過cookie,可以讓服務器知道請求是來源哪個客戶端,就可以進行客戶端狀態的維護,比如登陸後刷新,請求頭就會攜帶登陸時response header中的Set-Cookie,Web服務器接到請求時也能讀出cookie的值,根據cookie值的內容就可以判斷和恢復一些用戶的信息狀態。

簡而言之,cookie 使基於無狀態的HTTP協議記錄穩定的狀態信息成爲了可能。

cookie 主要用於以下三個方面:

  • 會話狀態管理(如用戶登錄狀態、購物車、遊戲分數或其它需要記錄的信息)
  • 個性化設置(如用戶自定義設置、主題等)
  • 瀏覽器行爲跟蹤(如跟蹤分析用戶行爲等)

3.cookie的原理及其構成

第一次訪問網站的時候,瀏覽器發出請求,服務器端生成 cookie在響應中通過Set-Cookie頭部告知客戶端(允許多個Set-Cookie頭部傳遞多個值),客戶端得到 cookie後,後續請求都會自動將 cookie頭部攜帶至請求中發送給服務器(見下面例子),另外,cookie的過期時間、域、路徑、有效期、適用站點都可以根據需要來指定。

// 一個HTTP響應:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value

這個HTTP響應會設置一個名爲"name",值爲"value"的cookie。名和值在發送時都會經過URL編碼。瀏覽器會存儲這些會話信息,並在之後的每個請求中都會通過HTTP頭部cookie再將它們發回服務器,比如:

GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value

cookie在瀏覽器中是由以下參數構成的:

  • name:唯一標識cookie的名稱。cookie名不區分大小寫,因此myCookie和MyCookie是同一個名稱。不過,實踐中最好將cookie名當成區分大小寫來對待,因爲一些服務器軟件可能這樣對待它們。cookie名必須經過URL編碼
  • value:存儲在cookie裏的字符串值。這個值必須經過URL編碼
  • Domain:cookie有效的域。發送到這個域的所有請求都會包含對應的cookie。如果不指定,默認爲文檔來源(由協議、域名和端口共同定義),不包含子域名。如果指定了Domain,則一般包含子域名。因此,指定 Domain 比省略它的限制要少。但是,當子域需要共享有關用戶的信息時,這可能會有所幫助。例如,如果設置 Domain=mozilla.org,則 Cookie 也包含在子域名中(如developer.mozilla.org)。
  • Path:請求URL中包含這個路徑纔會把cookie發送到服務器。
// 例如,設置 Path=/docs,則以下地址都會匹配:
/docs
/docs/Web/
/docs/Web/HTTP
  • Expires/Max-Age:設置cookie過期時間(Expires)或有效期(Max-Age)(即什麼時間之後就不發送到服務器了)。簡單名/值對形式的cookie只在當前會話期間存在,用戶關閉瀏覽器就會丟失。如果想讓cookie的生命週期超過單個瀏覽對話,那就指定Expires/Max-Age,max-age優先級高於expires。
  • Secure:設置之後,只在使用SSL安全連接的情況下才會把cookie發送到服務器。例如,請求https://www.wrox.com會發送cookie,而請求http://www.wrox.com則不會。
  • HttpOnly:設置了 HttpOnly 屬性的 cookie 不能使用 JavaScript 經由 Document.cookie 屬性、XMLHttpRequest 和 Request APIs 進行訪問,以防範跨站腳本攻擊(XSS)。
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value

這裏創建的cookie對所有wrox.com的子域及該域中的所有頁面有效(通過path=/指定)。不過,這個cookie只能在SSL連接上發送,因爲設置了secure標誌。

要知道,域、路徑、過期時間和secure標誌用於告訴瀏覽器什麼情況下應該在請求中包含cookie。這些參數並不會隨請求發送給服務器,實際發送的只有cookie的名/值對

4.Javascript 中的cookie

一般說來,cookie的生成方式主要有兩種,一種是上文提到的在響應中通過Set-Cookie頭部告知客戶端;另外一種就是在JavaScript中可以通過document.cookie可以讀寫cookie,如下:

//讀取瀏覽器中的cookie
console.log(document.cookie);
//寫入cookie
document.cookie='myname=langlixingzhou;path=/;domain=.baidu.com';

在JavaScript中處理cookie比較麻煩,因爲接口過於簡單,只有BOM的document.cookie屬性。在設置值時,可以通過document.cookie屬性設置新的cookie字符串。這個字符串在被解析後會添加到原有cookie中。設置document.cookie不會覆蓋之前存在的任何cookie,除非設置了已有的cookie。要爲創建的cookie指定額外的信息,只要像Set-Cookie頭部一樣直接在後面追加相同格式的字符串即可:

document.cookie = encodeURIComponent("name") + "=" +
                  encodeURIComponent("Nicholas") + "; domain=.wrox.com; path=/";
// 使用encodeURIComponent()對名稱和值進行編碼

5.cookie的缺陷

  • cookie 不夠大

每個cookie的大小爲4KB(名字和值都包含在這4KB之內),對於複雜的存儲需求來說是不夠用的。當 cookie 超過 4KB 時,它將面臨被裁切的命運。這樣看來,cookie 只能用來存取少量的信息。此外很多瀏覽器對一個站點的cookie個數也是有限制的(一般來說不超過300個cookie)。

  • 過多的 cookie 會帶來巨大的性能浪費

cookie是與特定域綁定的。同一個域名下的所有請求,都會攜帶 cookie。大家試想,如果我們此刻僅僅是請求一張圖片或者一個 CSS 文件,我們也要攜帶一個 cookie 跑來跑去(關鍵是 cookie 裏存儲的信息並不需要),這是一件多麼勞民傷財的事情。cookie 雖然小,但隨着請求的疊加,這樣的不必要的 cookie 帶來的開銷將是無法想象的。

cookie是用來維護用戶信息的,而域名(domain)下所有請求都會攜帶cookie,但對於靜態文件的請求,攜帶cookie信息根本沒有用,此時可以通過CDN(存儲靜態文件的)的域名和主站的域名分開來解決。

  • 由於在HTTP請求中的cookie是明文傳遞的,所以安全性成問題,除非用HTTPS。

6.cookie與安全

[圖片上傳失敗...(image-ed8008-1649840506574)]

有兩種方法可以確保 cookie 被安全發送,並且不會被意外的參與者或腳本訪問:Secure 屬性和HttpOnly 屬性。

標記爲 Secure 的 cookie 只應通過被 HTTPS 協議加密過的請求發送給服務端,因此可以預防中間人攻擊。但即便設置了 Secure 標記,敏感信息也不應該通過 cookie 傳輸,因爲 cookie 有其固有的不安全性,Secure 標記也無法提供確實的安全保障, 例如,可以訪問客戶端硬盤的人可以讀取它。

從 Chrome 52 和 Firefox 52 開始,不安全的站點(http:)無法使用cookie的 Secure 標記。

JavaScript Document.cookie API 無法訪問帶有 HttpOnly 屬性的cookie;此類 cookie 僅作用於服務器。例如,持久化服務器端會話的 cookie 不需要對 JavaScript 可用,而應具有 HttpOnly 屬性。此預防措施有助於緩解跨站點腳本(XSS)攻擊。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2019 07:28:00 GMT; Secure; HttpOnly

對cookie的限制及其特性決定了cookie並不是存儲大量數據的理想方式,讓“專業的人做專業的事情”,Web Storage 出現了。

HTML5中新增了本地存儲的解決方案----Web Storage,這樣有了Web Storage後,cookie能只做它應該做的事情了—— 作爲客戶端與服務器交互的通道,保持客戶端狀態。

二、Web Storage

Web Storage的目的是解決通過客戶端存儲不需要頻繁發送回服務器的數據時使用cookie的問題。Web Storage API包含了兩個對象:localStorage和sessionStorage,本質上是映射字符串鍵和值的對象化。localStorage是永久存儲機制,sessionStorage是跨會話的存儲機制。這兩種瀏覽器存儲API提供了在瀏覽器中不受頁面刷新影響而存儲數據的兩種方式。

1、Storage 對象

Window 對象的localStorage 和 sessionStorage 屬性引用的是 Storage對象。Storage對象用於保存名/值對數據,直至存儲空間上限(由瀏覽器決定)。一般來說,客戶端數據的大小限制是按照每個源(協議、域和端口)來設置的,因此每個源有固定大小的數據存儲空間。不同瀏覽器給localStorage和sessionStorage設置了不同的空間限制,但大多數會限制爲每個源5MB。

Storage對象定義瞭如下方法:

  • clear():刪除所有值;不在Firefox中實現。
  • getItem(name):取得給定name的值。
  • key(index):取得給定數值位置的名稱。
  • removeItem(name):刪除給定name的名/值對。
  • setItem(name, value):設置給定name的值。

Storage 對象中的鍵值對總是以字符串的形式存儲,這意味着數值類型會自動轉化爲字符串類型。

2、sessionStorage

sessionStorage對象只存儲會話數據,這意味着數據只會存儲到瀏覽器關閉。這跟瀏覽器關閉時會消失的會話cookie類似。存儲在sessionStorage中的數據不受頁面刷新影響,可以在瀏覽器崩潰並重啓後恢復(取決於瀏覽器,Firefox和WebKit支持,IE不支持)。

sessionStorage 特別應該注意一點就是,即便是相同域名下的兩個頁面,只要它們不在同一個瀏覽器窗口中打開,那麼它們的 sessionStorage 數據便無法共享。

localStorage 與 sessionStorage 在 API 方面無異,這裏我們以 sessionStorage 爲例:

  • 存儲數據:setItem()
sessionStorage.setItem('user_name', 'juejin')
  • 讀取數據: getItem()
sessionStorage.getItem('user_name')
  • 刪除某一鍵名對應的數據: removeItem()
sessionStorage.removeItem('user_name')
  • 清空數據記錄:clear()
sessionStorage.clear()

雖然Web Storage存儲數據會帶來諸多便利,但實際開發中使用它也有不便之處:

  • sessionStorage本身有API,但是隻是簡單的 key/value形式
  • sessionStorage只存儲字符串,需要轉換成json對象

基於上面兩點,開發過程中會對它進行封裝後再調用:

 // 礙於文章篇幅,並未將完整代碼展示出來
 // 想要獲取完整的代碼,可以加wx:qqlcx55
 // 將屬性存儲在某一模塊下
  setItem(key,value,module_name){
    if (module_name){
      let val = this.getItem(module_name);
      val[key] = value;
      this.setItem(module_name, val);
    }else{
      let val = this.getStorage();
      val[key] = value;
      window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(val));
    }
  },
  // 獲取某一個模塊下面的屬性
  getItem(key,module_name){
    if (module_name){
      let val = this.getItem(module_name);
      if(val) return val[key];
    }
    return this.getStorage()[key];
  },
  getStorage(){
    return JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || '{}');
  }

3、localStorage

localStorage 類似 sessionStorage,但其區別在於:存儲在 localStorage 的數據可以長期保留;而當頁面會話結束(即當頁面被關閉時),存儲在 sessionStorage 的數據會被清除 。要訪問同一個localStorage對象,頁面必須來自同一個域(子域不可以)、在相同的端口上使用相同的協議。

考慮到 localStorage 的特點之一是持久,有時我們更傾向於用它來存儲一些內容穩定的資源。比如圖片內容豐富的電商網站會用它來存儲 Base64 格式的圖片字符串:

有的網站還會用它存儲一些不經常更新的 CSS、JS 等靜態資源。

4、Web Storage與cookie 之間的區別

我們先來說說兩者的共同點,然後再細說下哪些地方有區別:

  • 共同點:都是保存在瀏覽器端,且都遵循同源策略。
  • 不同點:在於生命週期與作用域的不同

作用域:localStorage只要在相同的協議、相同的主機名、相同的端口下,就能讀取/修改到同一份localStorage數據。不過sessionStorage比localStorage更嚴苛一點,除了協議、主機名、端口外,還要求在同一窗口(也就是瀏覽器的標籤頁)下

[圖片上傳失敗...(image-2ae5d2-1649840506574)]
生命週期:localStorage 是持久化的本地存儲,存儲在其中的數據是永遠不會過期的,使其消失的唯一辦法是手動刪除;而 sessionStorage 是臨時性的本地存儲,它是會話級別的存儲,當會話結束(頁面被關閉)時,存儲內容也隨之被釋放。

說到底,Web Storage 是對 Cookie 的拓展,它只能用於存儲少量的簡單數據。當遇到大規模的、結構複雜的數據時,Web Storage 也愛莫能助了。這時候我們就要清楚我們的終極大 boss——IndexedDB!

四、IndexedDB

Indexed Database API簡稱IndexedDB,是瀏覽器中存儲結構化數據的一個方案。IndexedDB背後的思想是創造一套API,方便JavaScript對象的存儲和獲取,同時也支持查詢和搜索。

IndexedDB是類似於MySQL或Web SQL Database的數據庫。與傳統數據庫最大的區別在於,IndexedDB使用對象存儲而不是表格保存數據。IndexedDB數據庫就是在一個公共命名空間下的一組對象存儲,類似於NoSQL風格的實現。既然是數據庫了,那就不是 5M、10M 這樣小打小鬧級別了。理論上來說,IndexedDB 是沒有存儲上限的(一般來說不會小於 250M)。

1.IndexedDB的特點

  • 鍵值對儲存。

IndexedDB 內部採用對象倉庫(object store)存放數據。所有類型的數據都可以直接存入,包括 JavaScript 對象。對象倉庫中,數據以"鍵值對"的形式保存,每一個數據記錄都有對應的主鍵,主鍵是獨一無二的,不能有重複,否則會拋出一個錯誤。

  • 異步

IndexedDB的設計幾乎完全是異步的。爲此,大多數操作以請求的形式執行,這些請求會異步執行,產生成功的結果或錯誤。絕大多數IndexedDB操作要求添加onerror和onsuccess事件處理程序來確定輸出。IndexedDB 操作時不會鎖死瀏覽器,用戶依然可以進行其他操作,這與 localStorage 形成對比,後者的操作是同步的。異步設計是爲了防止大量數據的讀寫,拖慢網頁的表現。

  • 支持事務。

IndexedDB 支持事務(transaction),這意味着一系列操作步驟之中,只要有一步失敗,整個事務就都取消,數據庫回滾到事務發生之前的狀態,不存在只改寫一部分數據的情況。

  • 同源限制

IndexedDB 受到同源限制,每一個數據庫對應創建它的域名。網頁只能訪問自身域名下的數據庫,而不能訪問跨域的數據庫。

  • 儲存空間大

IndexedDB 的儲存空間比 localStorage 大得多,一般來說不少於 250MB,甚至沒有上限。

  • 支持二進制儲存。

IndexedDB 不僅可以儲存字符串,還可以儲存二進制數據(ArrayBuffer 對象和 Blob 對象)。

2.IndexedDB 使用流程

在IndexedDB大部分操作並不是我們常用的調用方法,返回結果的模式,而是請求——響應的模式。

接下來,我們遵循 MDN 推薦的操作模式,通過一個基本的 IndexedDB 使用流程,旨在對 IndexedDB 形成一個感性的認知:

  • 打開/創建一個 IndexedDB 數據庫(當該數據庫不存在時,open 方法會直接創建一個名爲 admin 新數據庫)
  // 後面的回調中,我們可以通過event.target.result拿到數據庫實例
  let db
  // 參數1位數據庫名,參數2爲版本號
  const request = window.indexedDB.open("admin", 1)
  // 使用IndexedDB失敗時的監聽函數
  request.onerror = function(event) {
     console.log('無法使用IndexedDB')
   }
  // 成功
  request.onsuccess  = function(event){
    // 此處就可以獲取到db實例
    db = event.target.result
    console.log("你打開了IndexedDB")
  }
  • 創建一個 object store(object store 對標到數據庫中的“表”單位)
// onupgradeneeded事件會在初始化數據庫/版本發生更新時被調用,我們在它的監聽函數中創建object store
request.onupgradeneeded = function(event){
  let objectStore
  // 如果同名表未被創建過,則新建test表
  if (!db.objectStoreNames.contains('test')) {
    objectStore = db.createObjectStore('test', { keyPath: 'id' })
  }
}  
  • 構建一個事務來執行一些數據庫操作,像增加或提取數據等
  // 創建事務,指定表格名稱和讀寫權限
  const transaction = db.transaction(["test"],"readwrite")
  // 拿到Object Store對象
  const objectStore = transaction.objectStore("test")
  // 向表格寫入數據
  objectStore.add({id: 1, name: 'juejin'})
  • 通過監聽正確類型的事件以等待操作完成。
  // 操作成功時的監聽函數
  transaction.oncomplete = function(event) {
    console.log("操作成功")
  }
  // 操作失敗時的監聽函數
  transaction.onerror = function(event) {
    console.log("這裏有一個Error")
  }

3.Web Storage、cookie 和 IndexedDB之間的區別

[圖片上傳失敗...(image-efc7d8-1649840506574)]

有了這些存儲手段,就可以在客戶端通過使用JavaScript存儲可觀的數據。因爲這些數據沒有加密,所以要注意不能使用它們存儲敏感信息。

總結

正是瀏覽器存儲、緩存技術的出現和發展,爲我們的前端應用帶來了無限的轉機。近年來基於存儲、緩存技術的第三方庫層出不絕,此外還衍生出了 PWA 這樣優秀的 Web 應用模型。總結下本文幾個核心觀點:

  • Cookie 的本職工作並非本地存儲,而是“維持狀態”。
  • Web Storage定義了兩個對象用於存儲數據:sessionStorage和localStorage。前者用於嚴格保存瀏覽器一次會話期間的數據,因爲數據會在瀏覽器關閉時被刪除。後者用於會話之外持久保存數據。
  • IndexedDB是類似於SQL數據庫的結構化數據存儲機制。不同的是,IndexedDB存儲的是對象,而不是數據表。

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章