JavaScript高程筆記——客戶端存儲

如今愈來愈多的網站是動態網站,經常須要將後端數據傳輸給前端保存或者更新到頁面中,尤爲是用戶偏好設置,保存在客戶端不只能夠減小請求耗時,也能下降服務端的壓力。javascript

客戶端(這裏通常指瀏覽器)目前主要包括三類存儲方式:cookie, Web Storage 和 IndexedDB。其中 Web Storage 又包含 Local Storage 和 Session Storage。html

(如今主流瀏覽器還支持 Web SQL,這裏暫時不作介紹...主要是不大瞭解)前端

1. cookie

cookie,全名叫 HTTP cookie,是第一個客戶端存儲解決方案,最初用於在客戶端存儲回話信息。後來主要用在三個方面:java

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

1.1 組成

Cookie是一段不超過4KB的小型文本數據,主要由如下七個部分組成:web

  1. Name:cookie的名稱
  2. Value:cookie的值
  3. Path:定義該web站點上能夠訪問該cookie的目錄(或者說路徑)
  4. Expires:cookie的有效期,有缺省(會話期cookie)和非缺省(持久性cookie)兩種狀態
  5. Domain:服務器域名
  6. Secure:指定是否使用HTTPS安全協議發送Cookie
  7. HttpOnly:用於防止客戶端腳本經過document.cookie屬性訪問Cookie

1.2 安全策略

Cookie 目前通常用做存儲用戶的登陸信息和登陸狀態,一般都會設置一個過時時間(若是不設置在瀏覽器關閉時就會清除該 Cookie),這個時間格式爲格林尼治標準時間。可是爲了防止信息泄露,這個過時時間通常不會太長,若是用戶期間有過再次進入系統,則會更新該過時時間。typescript

這一點與後端的 token 過時相似。數據庫

Secure 屬性和 HttpOnly 屬性能夠用來確保 Cookie 被正確發送。設置 Secure 屬性,能夠指定使用 HTTPS 協議來發送加密請求到服務端,所以能夠很好的防止"中間人攻擊"。而 HttpOnly 屬性能夠阻止客戶端使用 Document.cookie 來訪問帶 HttpOnly 屬性的 cookie,該屬性能夠有效的防止 「XSS 跨站點腳本」 攻擊。後端

可是 HttpOnly 沒法阻止客戶端從新寫入新的 cookie(新寫入的 cookie 也沒法設置 HttpOnly 屬性)api

Domain 屬性和 Path 屬性定義了 cookie 的做用域,即 cookie 能夠發送給哪些 URL。瀏覽器

Domain 屬性指定了能夠接受該 cookie(或者說該 cookie 能夠發送給哪些)的主機。不指定時默認是 origin,而且不包含子域名;若是要指定的話,則通常會包含子域名,例如:設置了 Domain=mozilla.org,則 Cookie 也包含在子域名中(如developer.mozilla.org)。

Path 屬性則定義了主機(服務端)哪些路徑能夠接受該cookie,而且會包含該路徑的全部子路徑。

目前的全部主流瀏覽器(除 IE 8 -)以外,都支持了一個新屬性 SameSite

SameSite Cookie 容許服務器要求某個 cookie 在跨站請求時不會被髮送,(其中 Site 由可註冊域定義),從而能夠阻止跨站請求僞造攻擊(CSRF)。

SameSite 屬性可接收3個值: "None", "Strict" 和 "Lax"。

  • None: 瀏覽器會在同站請求、跨站請求下繼續發送 cookies,不區分大小寫。
  • Strict: 瀏覽器將只在訪問相同站點時發送 cookie。(在原有 Cookies 的限制條件上的增強,如上文 「Cookie 的做用域」 所述)
  • Lax: 與 Strict 相似,但用戶從外部站點導航至URL時(例如經過連接)除外

1.3 漏洞與缺陷

  1. 殭屍Cookies

「殭屍Cookies」 是指Cookie的一種極端使用,這種類型的 Cookie 很難刪除,或者說刪除後會自動重建,通常是使用 Web Storage API 或者 Flash 本地共享來達到這種目的的,可是這些技術違反了用戶隱私和用戶控制的原則。

  1. 惡意 Cookies

這類 Cookie 一般是經過在 Cookie 植入特殊的標記語言,好比 "< >"用來指示該段內容是 HTML 代碼,這些代碼能夠定義網頁格式,也能夠用來執行代碼段等等。

這種攻擊方式最多見的就是 XSS(跨站腳本)攻擊。

  1. Cookie捕獲/重放

指攻擊者經過木馬等惡意程序,或者跨站腳本等手段竊取用戶硬盤或者內存中的Cookies;或者經過在局域網中監聽網絡通訊、攻擊中間的網絡路由器等將用戶的請求欺騙重定向到攻擊者的主機等等手段也能夠竊取用戶 Cookie。

在捕獲(竊取)到用戶 Cookie 以後,也能夠從新發送(重放) 該 Cookie 到服務器,假冒原用戶的身份發起攻擊。

  1. 會話定置

「會話定置」 則是像受害者主機注入攻擊者控制的惡意 Cookies,使受害者以攻擊者的身份登陸網站竊取會話信息;或者僞造與網站同域的站點來欺騙受害者訪問該僞造網站等。

  1. CSRF攻擊

CSRF(Cross-Site Request Forgery, 跨站請求僞造)攻擊,是一種挾制用戶在當前已登陸的Web應用程序上執行非本意的操做的攻擊方法,指攻擊者經過一些技術手段欺騙用戶的瀏覽器去訪問一個本身曾經認證過的網站並運行一些操做(如發郵件,發消息,甚至財產操做如轉帳和購買商品)。

CSRF攻擊並不能直接獲取用戶帳戶的控制權和用戶的任何信息,而是欺騙瀏覽器,讓瀏覽器以用戶的名義來執行操做或者請求。

  1. Cookie 的容量很小(大部分瀏覽器的限制都是不超過 4KB),而且個數也有限,在發送請求時也會跟隨在請求頭內一塊兒發送

因此 Cookie 一般不適合用來存儲大容量數據,只做爲用戶信息和登陸狀態的關鍵字的保存方式,用來實現客戶端與服務器之間通訊時的用戶認證。

1.4 讀取和設置

一般 JavaScript 訪問和設置 Cookie 都是使用 document.cookie 來讀取和設置。

直接使用 document.cookie 會打印出該站點下全部可訪問的 cookie 的 name=value; 格式的字符串,而且只有 name 和 value 兩個屬性。

使用 document.cookie = "newCookieName=newCookieValue 的時候則會查找站點下全部 Cookie中是否有和 newCookieName 同名的 cookie,有則更新原cookie,沒有則新建一個 cookie 。

2. Web Storage

Web Storage 最初的目標有兩個:一個是提供一個除 Cookie 外新的會話數據存儲途徑;另外一個是提供跨會話持久化存儲大容量數據的機制。

Web Storage 分爲兩類:LocalStorage 和 SessionStorage,09年以後這兩個對象都在 window 對象上提供了一個訪問地址,可直接使用 window.localStorage 或者 window.sessionStorage 來訪問對應的 Storage。

這兩種方式最大的不一樣在於:LocalStorage 是永久存儲機制,SessionStorage 是跨會話的存儲機制。

LocalStorage 和 SessionStorage 都是特定於頁面協議的。

2.1 LocalStorage

LocalStorage 在瀏覽器上體現爲 window 對象的一個只讀屬性,而且這個屬性的值指向瀏覽器的 localStorage 對象,容許腳本訪問當前 Document 源(同域名同端口,而且不包含子域名)下的 Storage。

LocalStorage 主要用於持久化存儲數據,其中的數據老是以鍵值對的形式存儲的,而且全部的鍵和值都會自動轉爲字符串形式

使用方式

獲取當前源下的 localStorage 對象能夠直接使用一個變量接收:

const myLocalStorage = window.localStorage; // 通常狀況下 window 能夠省略
複製代碼

這個對象提供了四個方法,用來增刪改查某個鍵值對和清空本地 localStorage

const myLocalStorage = window.localStorage;

myLocalStorage.setItem('newStorage', 'newValue'); // 添加/修改

let newValue = myLocalStorage.getItem('newStorage'); // 讀取

myLocalStorage.removeItem('newStorage'); // 移除
 
myLocalStorage.clear(); // 清空
複製代碼

2.2 SessionStorage

sessionStorage 對象主要只用來存儲會話數據,只會存儲到瀏覽器關閉。

sessionStoragelocalStorage 同樣, 瀏覽器上都體現爲 window 對象的一個只讀屬性,而且這個屬性的值指向瀏覽器的 sessionStorage 對象,容許腳本訪問當前 Document 源(同域名同端口,而且不包含子域名)下的 Storage。

注意:

1.同源的不一樣標籤頁不必定會共享 sessionStorage,只有經過點擊頁面內連接,或者使用 window.open 打開的新的同源標籤頁纔會和原來的頁面共享一個 sessionStorage。直接經過地址欄輸入地址打開的頁面,即便地址同樣,可是也是不一樣的 sessionStorage

2.在瀏覽器中刷新頁面並不會丟失以前設置的 sessionStorage

使用方式

sessionStoragelocalStorage 同樣,都提供了四個方法用來增刪改查和清空本地數據。

const mySessionStorage = window.localStorage;

mySessionStorage.setItem('newStorage', 'newValue'); // 添加/修改

let newValue = mySessionStorage.getItem('newStorage'); // 讀取

mySessionStorage.removeItem('newStorage'); // 移除
 
mySessionStorage.clear(); // 清空
複製代碼

2.3 事件

每當 Storage 對象發生變化時( sessionStoragelocalStorage 上的任何更改),都會在文檔上觸發 storage 事件。

這個事件對應的事件實例對象有4個屬性:

  1. domain:本地存儲對應的域
  2. key:被新增/修改/刪除的鍵名
  3. newValue:被設置的新值,刪除時爲null
  4. oldValue:變化以前的值
window.addEventListener("storage", event => alert('Storage changed for ${event.domain}'));
複製代碼

須要注意的是,這個事件並不會區分是 sessionStorage 仍是 localStorage 發生的改變。

2.4 異同點

由前面的內容能夠了解到,sessionStoragelocalStorage 最大的區別就是存儲時效的不一樣,當咱們在須要作本地數據的持久化時,一般會將數據(例如用戶的個性化配置等)保存在 localStorage 中。

localStoragesessionStorage 在存儲量上大體相同,上限都是 5MB 左右,具體狀況根據瀏覽器的不一樣可能略有出入;而且這二者並不會跟隨 http 網絡請求被髮送;在設計時也爲用戶(開發者)提供了良好易用的 API 和事件。

3. IndexedDB

MDN定義:IndexedDB,是一種底層 API,用於在客戶端存儲大量的結構化數據(也包括文件/二進制大型對象(blobs))。該 API 使用索引實現對數據的高性能搜索。

3.1 核心概念

IndexedDB 是一個事務型數據庫系統,相似於基於 SQL 的 RDBMS(Relational Database Management System,關係數據庫管理系統)。可是 IndexedDB 是基於 JavaScript 的面向對象的數據庫,主要用來存儲和檢索用 爲索引的 對象,而且支持二進制數據。

IndexedDB 在設計的時候幾乎全是異步結構,因此在操做(調用)IndexedDB 的時候基本上都是以請求的形式執行,而且會返回成功時的結果或者失敗時的錯誤。

IndexedDB 支持事務,意味着在一個完整的操做過程當中,只要發生錯誤,便會退回到該事務發生以前的狀態,避免數據的部分改變。

IndexedDB 跟 Web Storage 同樣也受同源策略的限制,可是同源下不一樣標籤頁會共享存儲數據與相關事件(這一點可能會形成併發問題,後面會講解)。

3.2 如何使用

IndexedDB 是一個比較複雜的 API,涉及很多概念。它把不一樣的實體,抽象成一個個對象接口。學習這個 API,就是學習它的各類對象接口。

  • 數據庫:IDBDatabase 對象,存儲一系列數據的容器對象
  • 對象倉庫:IDBObjectStore 對象,相似關係數據庫的表格
  • 索引: IDBIndex 對象
  • 事務: IDBTransaction 對象
  • 操做請求:IDBRequest 對象
  • 指針: IDBCursor 對象
  • 主鍵集合:IDBKeyRange 對象
  • ...

---- 摘自 阮一峯的網絡日誌: 瀏覽器數據庫 IndexedDB 入門教程

使用 IndexedDB 的基本模式就是:

  1. 打開/建立 數據庫
  2. 在數據庫中建立一個對象倉庫
  3. 啓動一個事務,併發送一個事務請求來執行對應的數據庫操做
  4. 經過監聽對應類型的 DOM 事件來等待或者判斷數據庫操做的執行結果
  5. 根據操做結果進行後續操做

根據以上步驟,能夠演示一個基礎的 IndexedDB 數據庫操做流程。

第一步: 打開/建立數據庫

const IndexedDB = window.indexedDB;

let db, dbRequest;

dbRequest = indexedDB.open("test", 1);
dbRequest.onerror = event => (alert(`Failed to open: ${event.target.errorCode}`));
dbRequest.onsuccess = event => (db = event.target.result);
複製代碼

這裏首先是建立一個打開(在不存在 test 這個數據庫的時候則是建立)數據庫的 IDBRequest 請求實例,並在這個實例上添加 onerroronsuccess 事件處理的回調函數,來處理請求失敗或者成功時的事件(幾乎全部的請求都須要處理這兩個事件的回調函數)。

open(dbName: String, version: Number = 1) 能夠接受第二個正整數參數 version,用來指定對應數據庫的版本,若是已經存在了這個數據庫的話,則會將該數據庫進行升級(第二步會對升級形成的影響進行說明)。

第二步: 建立數據倉庫

在第一步的 open 操做中,會建立一個新的數據庫(或者讀取原有的倉庫,後面爲了簡潔,都會省略獲取部分,除非有特殊說明),而且在建立完成後,會觸發一個 upgradeneeded 事件。 open 操做返回的請求實例 dbRequest 中也會有一個 upgradeneeded 屬性。咱們能夠經過設置該屬性爲一個回調函數,在回調中建立須要的數據倉庫。

dbRequest.onupgradeneeded = function(event) {
    // 保存 IDBDataBase 接口
    let db = event.target.result;

    // 首先利用 contains 判斷是否已經存在 person 倉庫
	if (!db.objectStoreNames.contains('person')) {
        // 爲該數據庫建立一個對象倉庫
        let objectStore = db.createObjectStore("person", { keyPath: "myKey" });
    }
};
複製代碼

這一步完成以後,咱們就在這個數據庫中建立了一個名字叫 person 的數據倉庫,相似關係數據庫中的一個數據表。

createObjectStore(storeName: String, options: Object) 方法接收兩個參數,第一個是倉庫名,第二個是鍵的配置項,可配置的有兩個屬性: keyPathautoIncrement

keyPath 屬性可設置一個字符串值,表示該數據倉庫下的每條數據使用哪個字段來做爲鍵。

autoIncrement 屬性可設置一個布爾值,表示是否開啓鍵生成器;開啓時默認從1開始,新數據會在以前的鍵的基礎上加1做爲新的鍵,且不會減少。

這兩個屬性通常只設置一個,生成或者選取的鍵相似於關係表中的「主鍵」,或者說「索引」

第3、4、五步:建立事務進行數據庫操做並根據返回結果進行後續操做

在數據庫和數據倉庫都建立完成以後,剩下的全部操做幾乎都須要用事務來完成。每個系列操做,都會對應一個事務實例。

實例化是一個事務使用 db.transaction() 方法:

// 爲了說明實例化方法須要的參數,這裏用 TS 來舉例
let myTransaction: IDBTransacation = db.transaction(storeNames: string[], mode?: "readonly" | "readwrite" | "versionchange", options?: { error(): void });
複製代碼

mode 參數的不一樣值對應不一樣的操做權限,具體內容請查看 MDN IDBTransactionMDN IndexedDB 增長、讀取和刪除數據

由於事務自己也是一個請求,因此也須要配置對應的事件處理函數 onseccessonerror,除了這兩個事件外事務還有另一個事件處理函數 oncomplete,這個函數一般用來代替 onsuccess 用來處理事務請求成功以後的事件(某些事務沒法經過 oncomplete 事件返回的 event 對象來訪問數據時,則仍是隻能用 onsuccess)。

可使用 add()put() 方法添加和更新對象,使用 get() 取得對象,使用 delete() 刪除對象,使用 clear() 刪除全部對象。其中,get()delete() 方法都接收對象鍵做爲參數,這 5 個方法都建立新的請求對象。

下面是經過事務來進行增刪改查操做的例子:

const transaction = db.transaction(['person'], "readwrite");
const objectStore = transaction.objectStore('person');

const errorFunction = () => console.log('事務處理失敗');

// 讀取
const read = function() {
    const readRequest = objectStore.get(1);
    readRequest.onerror = errorFunction;
    readRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('Name: ' + request.result.name);
            console.log('Age: ' + request.result.age);
        } else {
            console.log('未得到數據記錄');
        }
    };
}

// 增長
const add = function() {
    const addRequest = objectStore.add({ id: 1, name: '張三', age: 24 });
    addRequest.onerror = errorFunction;
    addRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('數據寫入成功');
        } else {
            console.log('數據寫入失敗');
        }
    };
}

// 修改
const update = function() {
    const updateRequest = objectStore.put({ id: 1, name: '張三', age: 24 });
    updateRequest.onerror = errorFunction;
    updateRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('數據修改爲功');
        } else {
            console.log('數據修改失敗');
        }
    };
}

// 刪除
const delete = function() {
    const deleteRequest = objectStore.delete(1);
    deleteRequest.onerror = errorFunction;
    deleteRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('數據刪除成功');
        } else {
            console.log('數據刪除失敗');
        }
    };
}

// 遍歷
const traverse = function() {
    const traverseRequest = objectStore.openCursor();
    traverseRequest.onerror = errorFunction;
    traverseRequest.onsuccess = function(event) {
        console.log('遍歷結束');
    };
}

// 清空
const clear = function() {
    const clearRequest = objectStore.clear();
    clearRequest.onerror = errorFunction;
    clearRequest.onsuccess = function(event) {
        console.log('清空成功');
    };
}
複製代碼
相關文章
相關標籤/搜索