前端開發的客戶端本地存儲

在前端開發過程當中,爲了與服務器更方便的交互或者提高用戶體驗,咱們都會在客戶端(用戶)本地保存一部分數據,好比cookie/localStorage/sessionStorage。在後端管理系統的前端,更是會涉及到一部分超大數據的請求,一個接口有時會達到5M甚至15M的程度,當這個接口數據並非常常更新時,咱們能夠用兩種方式,一種是分頁請求+預加載+懶加載,另外一種就是本地存儲+熱更新。而因爲第二種方式用戶體驗更優秀,即是我經常使用的方式。php

這篇文章的客戶端本地存儲,咱們主要講到cookie/localStorage/sessionStorage/indexedDB四種技術。html

cookie

HTTP Cookie一般簡稱cookie,該標準用於瀏覽器存儲會話信息,在發起HTTP請求時攜帶Cookie參數:前端

// Request Header 

GET /oss/index.php?r=api/jlog/collect HTTP/1.1
Host: gzhxy.baidu.com:8090
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36

···

Cookie: name=Leon; age=24

Cookie有一些限制:ajax

  • 它是以; (分號+一個空格)分割的鍵值對字符串,在網絡傳送時必須是URL編碼的。
  • 綁定在固定域名下,不容許其它域名訪問。
  • 有些瀏覽器有數量限制,在超出後刪除順序不統一,有些最近最少使用(LRU),有些隨機刪
  • 同一域名下總大小限制5KB,超出限制後靜默失敗

Cookie的參數構成:數據庫

  • 名稱:惟一肯定,不區分大小寫(實際編寫建議區分),必須URL編碼
  • 值:字符串值,必須URL編碼
  • 域:該cookie字段有效的域,能夠爲baidu.com域名,也能夠爲cdn.baidu.com子域名,缺省值爲當前頁面子域名
  • 路徑:該cookie字段有效範圍爲指定域下的具體路徑,如cdn.baidu.com/oss,那麼其餘路徑就沒法訪問
  • 失效時間:cookie被刪除的時間戳,缺省值爲瀏覽器會話結束被刪除,也能夠手動設置,時間格式爲GMT格式Wdy, DD-Mon-YYYY HH:MM:SS GMT,能夠調用Date實例的toGMTString()方法轉換,若是設置爲過去的時間,該cookie被馬上刪除
  • 安全標誌:爲單詞secure而非鍵值對,指定後,只有在SSL鏈接的時候纔會被髮送到服務器,也就是https協議

注意參數中只有名稱和值纔會被髮送給服務器,其他的只是須要瀏覽器識別的命令式參數。後端

cookie的接口設置很是的不人性化,每每須要咱們對其操做進行封裝纔會方便使用。下面咱們對其進行增刪查改。api

查看cookie:decodeURIComponent(document.cookie);瀏覽器

添加或修改cookie:緩存

// 須要改爲本身須要的cookie和域名、路徑以及是否爲https
document.cookie = 'encodeURIComponent(name)=encodeURIComponent(Leon); expries=' + (new Date(Date.now() + 24*60*60*1000)).toGMTString() + '; path=oss; domain=cdn.baidu.com; secure';

刪除cookie: document.cookie = 'name=Leon; expires=' + (new Date(0)).toGMTString();安全

具體的封裝的方式網上有不少,能夠去搜一搜,核心就是對cookie進行字符串檢索和切分,以及將傳入的函數參數最終轉換爲字符串。

Storage

因爲cookie的大小限制和須要全量傳遞給服務器,在不少場景下並不適用,因此HTML5規範中出現了Storage對象,包含localStoragesessionStorage兩種繼承對象,屬於window的屬性。它提供了一般5M的大小空間來保存無需服務器交互的本地數據。

Storage的經常使用方法:

  • clear(): 清除全部值
  • getItem(name): 獲取指定name的值
  • key(index): 得到對應索引值的鍵名
  • removeItem(name): 刪除指定name的鍵值對
  • setItem(name, value): 爲指定name設置對應的值

除了這些方法以外,Storage對象能夠直接經過點語法或者方括號語法訪問屬性和操做屬性,也能夠經過delete關鍵字刪除屬性。該對象中的值均爲字符串。

持久化數據localStorage一般保留到JS刪除或者用戶清除瀏覽器緩存。

會話數據sessionStorage保留到關閉瀏覽器。因爲綁定會話窗口,因此不支持本地文件讀寫。另外在IE8中,該對象爲異步讀寫,須要調用begin()commit()方法保證成功讀取,再也不贅述。

Storage對象作任何的新增,修改或者刪除操做,都會觸發storage事件。該事件只支持在與服務端通訊時,一個頁面修改,另外一個頁面會觸發該事件。

document.addEventListener('storage', function (e) {
    console.log(e);
});

該事件對象的主要屬性:

  • domain: 發生變化的存儲空間的域名
  • key:修改的鍵名
  • newValue:若是是設置值,則爲新值;若是刪除,則爲null
  • oldValue:更改前的值

IndexedDB

擁有了Storage利器,已經能解決不少問題,可是一般5M的大小限制仍是會限制一部分場景,好比後臺管理系統的接口數據很容易突破5M,這個時候就須要咱們的瀏覽器數據庫IndexedDB了。其實在此以前,各家廠商主要推廣的是Web SQL Database,不事後來被廢棄了,雖然如今在部分平臺上也能使用,但咱們不作介紹了。

IndexedDB數據庫用於瀏覽器保存結構化數據,區別於傳統數據庫它保存的是對象,徹底採用事務類型,全部的操做被轉換爲請求的方式,因此咱們須要對每一步操做添加回調函數。

一個完整的實例爲:

// 判斷可否正確打開數據庫,避免屢次檢測
let dbOpened = false;
// 打開本地持久化數據庫,默認版本爲1
const request = indexedDB.open('jomocha');
// 當打開錯誤時
request.onerror = function(event){
    console.error('打開本地持久化數據錯誤', event);
    OSS.commonUI.showMsg('打開本地持久化數據庫錯誤,試用功能,不影響使用,請聯繫zhaoshuaiqiang', 'error');
};
// 當數據庫首次建立該版本時(首次建立或更新版本)
request.onupgradeneeded = function(event){
    const db = event.target.result;
    // 建立一個數據庫存儲對象,保存全部的維度項,分爲name和list兩個屬性
    const objectStore = db.createObjectStore('dimensions', {
        keyPath: 'name'
    });
    // 定義存儲對象的數據項屬性
    objectStore.createIndex('name', 'name', {
        unique: true
    });
};
// 成功打開了數據庫
request.onsuccess = function(event){
    dbOpened = true;
    const db = event.target.result;
    // 新建一個事務,包含oncomplete 和onerror句柄事件,缺省值爲readonly,只讀模式,可並行
    const transaction = db.transaction(['dimensions']);
    // 打開存儲對象
    const objectStore = transaction.objectStore('dimensions');
    const request = objectStore.get('host');
    request.onsuccess = function (event) {
        // 第一次打開數據庫時,確定沒有數據,因此須要檢測
        if (event.target.result) {
            JomoCha.data = event.target.result.list;
        }
    };
}

數據庫

當咱們須要使用IndexedDB時,首先要調用indexedDB.open()方法打開數據庫,若是該數據存在,則發起打開的請求,若是不存在,則發起建立並打開的請求。該方法會返回一個IDBRequest對象,能夠在該對象上添加回調方法。具體的方法如示例中的最外層請求。

回調函數傳入的事件屬性event.target就指向該請求,即request。若是發生了錯誤,event.target.errorCode將會保存錯誤信息的錯誤碼;若是成功,event.target.result就會保存一個數據庫實例對象。

錯誤碼列表(第二個開始省略前綴IDBDatabaseException.):

  • IDBDatabaseException.UNKNOWN_ERR(1):意外錯誤,沒法歸類
  • NON_TRANSIENT_ERR(2):操做不合法
  • NOT_FOUND_ERR(3):未發現要操做的數據庫
  • CONSTRAINT_ERR(4):違反了數據庫約束
  • DATA_ERR(5):提供給事務的數據不知足要求
  • NOT_ALLOWED_ERR(6):操做不合法
  • TRANSACTION_INACTIVE_ERR(7):試圖重用已完成的事務
  • ABORT_ERR(8):請求中斷,未完成
  • READ_ONLY_ERR(9):試圖在只讀模式下寫入或修改數據
  • TIMEOUT_ERR(10):在有效時間內未完成操做
  • QUOTA_ERR(11):磁盤空間不足

對象存儲空間(表)

成功打開數據庫以後,咱們就能夠打開對象存儲空間了,你能夠把它理解成數據庫中的表,用於保存不一樣的數據,如用戶、交易、購物車等。下面的全部db表明成功回調中的數據庫對象.

咱們先調用db.createObjectStore('dimensions', {keyPath:'name'});來建立一張表,這個dimensions爲惟一表名,其中的keyPath屬性指定該表的鍵名,後面存儲的全部數據都必須擁有該屬性。

看下寫入數據庫的實例:

// 每次同步更新最新數據
$.ajax({
    url: '?r=tools/api/hosts',
    success: function (data) {
        // 若是可以打開本地數據庫,則保存
        if (dbOpened) {
            const request = indexedDB.open('jomocha');
            request.onsuccess = function(event){
                const db = event.target.result;
                // 新建一個事務,讀寫模式,不可並行
                const transaction = db.transaction(['dimensions'], 'readwrite');
                // 打開存儲對象
                const objectStore = transaction.objectStore('dimensions');
                // 使用put方法,有則修改,沒有則添加
                const request = objectStore.put({
                    name: 'host',
                    list: data.data
                });
            }
        }
    }
});

這裏寫入數據庫的對象裏就包含了name參數。咱們拿到數據使用add()put()方法添加數據,這兩種方法區別在於遇到相同的鍵名存在時,add()報錯,put()修改原有值。這兩種方法依然爲請求,能夠對其指定onsuccess()onerror()事件處理回調,示例中省略了。

事務

在建立完成數據後,就能夠對其進行查詢了,indexedDB中全部讀寫操做都要經過事務,咱們調用db.transaction();方法來打開全部存儲空間(表),也能夠傳入參數來控制咱們須要打開的表:

// 打開一張表
db.transaction('user');
// 打開多張表
db.transaction(['user', 'dimensions']);

該方法還接受第二個參數做爲訪問方式,包含3種:readonly(缺省值)、readwriteversionchange。最後一個比較特別,爲在版本更新時使用,不能夠與其餘事務併發執行,容許任何操做,包括刪除和建立索引。

如今經過事務咱們已經肯定了操做空間,接下來就能夠獲取具體的數據對象了,下面的transaction表明着上面的db.transaction()方法返回的事務。事務能夠執行多個請求,自己也是一個請求,能夠指定相應的oncomplete()onerror()事件處理回調。其中的oncomplete()事件不能拿到該事務中請求的數據。

使用transaction.objectStore('dimension');獲取數據對象,而後就能夠經過add/put/get/delete/clear方法進行增刪查改,均爲請求,須要設置onsuccess()onerror()回調。

遊標遍歷

咱們可使用get()方法檢索出具體的單個對象,可是若是須要遍歷,咱們就要使用到遊標查詢了,也就是指定範圍,而後遍歷數據。下面的objectStore指代上面transaction.objectStore()返回的數據對象集。

// 指定遊標範圍
const cursorRange = IDBKeyRange.bound('001', '100');
// 打開遊標查詢
const request = objectStore.openCursor(cursorRange);
request.onsuccess = function (event) {
    const cursor = event.target.result;
    // 必須檢查cursor是否存在,若是該項存在,則爲IDBCursor實例,不然爲null
    if (cursor) {
        console.log(cursor.key + ': ' + cursor.value);
        cursor.continue();
    } else {
        console.log('遍歷完成');
    }
}
request.onerror = function (event) {
    console.error('遊標區間獲取失敗');
}

在遊標遍歷時,具體的數據會保存在event.target.result裏,若是該項存在,則會爲一個IDBCursor對象實例,不然爲null。實例的屬性:

  • direction:數值,表示遊標的方向,next/nextunique/prev/prevunique,帶有unique的會去重
  • key:數據對象鍵
  • value:數據對象值
  • primaryKey:遊標當前的使用鍵值,後面會說

在遍歷到遊標中具體的每一項時,可使用update()delete()來修改,若是想要移動遊標:

  • continue(key):存在key移動到指定項,不然下一項
  • advance(count):存在count移動指定項數,不然上一項

咱們經過IDBKeyRange對象來控制鍵範圍,有4種方式指定:

  • only(key):只取得想要的鍵值對,等同於直接調用get(key)
  • lowerBound(key, true):第一個元素爲key,若是第二個參數爲true,從該項的下一項開始,缺省值爲false
  • upperBound(key, true):最後一個元素爲key,若是第二個參數爲true,從該項的下一項開始,缺省值爲false
  • bound(lowerkey, upperkey, true, true):前二者的結合,1和3對應lower,2和4對應upper

openCursor()方法接收的第一個參數爲範圍區間,若是爲null,則默認所有範圍;第二個參數爲方向,爲next/nextunique/prev/prevunique四個,帶有unique的會去重

索引

可使用objectStore.createIndex('name', 'name', {unique: true});來建立索引,分別爲索引名,索引的屬性名,該屬性值是否惟一。調用objectStore.delete('name');來刪除索引。刪除索引不會影響數據,因此沒有回調函數。

若是咱們不想在主鍵上遍歷遊標或者獲取數據,能夠在數據集上取得新的索引列表:

// 直接切換索引
const index = objectStore('dimensions');
// 在該索引上進行遊標遍歷
request = index.openCursor();

若是咱們在非主鍵的遊標中,想要去的主鍵值,調用index.getKey('007');

併發問題

數據庫操做存在併發問題,但因爲是異步,咱們不用擔憂,可是若是存在新版本變動仍是會致使問題。因此打開數據庫時指定onversionchange()處理事件,能夠避免這個問題,在setVesion()時,若是觸發onerror()表明已經打開了該數據庫,沒法如今更新版本,提示用戶關閉其它網頁,從新調用更新。

總結

若是須要與服務器實時交互,使用cookie,若是須要保存一些小信息字段,使用localStorage,若是隻須要本次會話有效,使用sessionStorage,若是數據很大,使用indexedDB。使用什麼技術跟業務場景匹配,可是技術仍是都要了解都要會,畢竟,巧婦難爲無米之炊。

參考資料

  1. JavaScript高級程序設計 23章-離線應用與客戶端存儲
  2. storage事件使用:https://www.cnblogs.com/incon...
  3. IDBCursor對象:https://developer.mozilla.org...
相關文章
相關標籤/搜索