JS學習21(離線應用與客戶端儲存)

Web應用與傳統客戶端最大的區別就是需要連接網絡,沒有網絡整個應用就無法運行,這個一直是Web應用最大的痛點之一。
HTML5為了解決這個問題添加了對離線應用的支持。開發離線Web應用有幾個關鍵點。確保應用知道設備是否能上網以便下一步執行正確的操作,然后應用還必須能訪問一定的資源。最后必須有一塊本地空間用于保存數據,無論是否能上網都能讀寫數據。

離線檢測

為檢測設備是離線還是在線,HTML5定義了navigator.onLine這個屬性值為true表示設備可以上網。
還有兩個事件:online和offline,這兩個事件會在網絡狀態變化時在window對象上觸發。

EventUtil.addHandler(window, "online", function(){
    alert("online");
});
EventUtil.addHandler(window, "offline", function(){
    alert("Offline");
});

支持離線檢測的瀏覽器有IE6+、navigator.onLine、Firefox3、Safari4、Opera10.6、Chrome、iOS 3.2版Safari、Android 版 WebKit。

應用緩存

application cache,這是專門為了開發離線Web應用而設計的,它從瀏覽器緩存中分出來一部分,想要在這個部分中保存數據,可以使用一個描述文件,列出要下載和緩存的資源。這里有個簡單的例子:

CACHE MANIFEST
#Comment

file.js
file.css

這里僅簡單的列出了要下載的文件。描述文件的選項非常多,想要進一步了解的話給大家一個網址咯。Go offline with application cache
要將描述文件與頁面關聯起來,可以使用,下面的屬性。

<html manifest="/offline.manifest">

同時,有相應的JS API提供給開發者來獲取其狀態。
這個API的核心是applicationCache對象,這個對象有一個status屬性,表示應用緩存的狀態:

  • 0:無緩存
  • 1:閑置,應用緩存未更新
  • 2:檢查中,正在下載描述文件并檢查更新
  • 3:下載中,應用緩存正在下載描述文件中指定的資源
  • 4:更新完成,應用緩存已經更新了資源,且所有資源下載完畢,可以通過swapCache()來使用了
  • 5:廢棄,應用緩存的描述文件已經不存在了,頁面無法再訪問應用緩存

同時針對上面的狀態也有相應的事件:

  • checking,查找更新時觸發
  • error,檢查更新或下載資源期間發生錯誤時觸發
  • noupdate,檢查描述文件發現沒有更新時觸發
  • downloading,開始下載資源時觸發
  • progress,下載的過程中不斷觸發
  • updateready,下載完畢且可以使用swapCache()時觸發
  • cached,應用緩存完整可用時觸發

在頁面剛剛加載時,會自動檢查有沒有描述文件是否更新,并根據具體情況觸發上述事件。
有用的方法有兩個:

  • update(),會去檢查描述文件是否更新,就像頁面剛剛加載那樣,并觸發相應的事件
  • swapCache(),在新緩存可用時可以調用這個方法來啟用新應用緩存
EventUtil.addHandler(applicationCache, "updateready", function(){               
    applicationCache.swapCache();
});

數據存儲

隨著Web應用的出現,也產生了應該直接在客戶端上儲存用戶信息能力的要求,包括用戶的登陸信息,偏好設置或其他數據。最最開始解決這個問題的方案是cookie。

Cookie

Cookie最初是用來在客戶端儲存會話信息的。該標準要求服務器對任意HTTP請求發送Set-Cookie HTTP頭作為相應的一部分,其中包含會話的信息。例如:

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

這里就設置了一個以name為名稱,value為值的一個cookie。
瀏覽器會儲存這樣的會話信息。并在這之后通過為每一個請求添加Cookie HTTP頭部將信息發送回服務器:

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

這個信息對于服務器來說就可以唯一驗證請求的身份
限制
cookie在性質上是綁定在特定域名下的。當設定了一個cookie,再給創建它的域名發送請求時都會包含這個cookie,而發向其他域的請求中并不會包含這個cookie。這個限制保證了cookie只能讓批準的接受者訪問。
每個域的cookie總數是有限的,各瀏覽器不同,最小的規定一個域有30個cookie,大小一般不超過4095B。
cookie的構成
cookie由瀏覽器保存的一下幾塊信息構成:

  • 名稱:一個唯一確定cookie的名稱
  • 值:儲存在cookie中的字符串值
  • 域:這個cookie對哪個域有效,如果這個域包含子域,那對子域同樣有有效。如果設定是沒有明確指定,這個值會被認為是設置cookie的那個域
  • 路徑:用于指定向域中的哪個路徑發送cookie,例如,你可以指定cookie只發送到www.baidu.com/img,那再訪問www.baidu.com時就不會發送cookie。及時它們同域
  • 失效時間:cookie應該被刪除的時間戳,默認瀏覽器會話結束就刪除
  • 安全標志:指定后,cookie只有在使用SSL連接時才會發送到服務器

設置時像這樣:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com path=/; secure
Other-header: other-header-value

JS中的cookie
JS中訪問cookie的接口是BOM的document.cookie。
獲取時,這個屬性返回字符串,包括當前頁面可用的(根據cookie的域,路徑,失效時間等等)所有cookie的名稱和值組成的鍵值對。

name1=value1;name2=value2;name3=value3

這些是經過URI編碼的值。要使用decodeURIComponent()解碼。
設置時和使用HTTP頭部設置一樣:

document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nicholas") + "; domain=.wrox.com; path=/";

由于在JS中使用cookie不是很直觀,寫個工具類比較好。

var CookieUtil = {
    get: function (name){
        var cookieName = encodeURIComponent(name) + "=",
            cookieStart = document.cookie.indexOf(cookieName),
            cookieValue = null;
        if (cookieStart > -1){
            var cookieEnd = document.cookie.indexOf(";", cookieStart);
            if (cookieEnd == -1){
                cookieEnd = document.cookie.length;

            }
            cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd));
        }
        return cookieValue;
    },
    set: function (name, value, expires, path, domain, secure) {
        var cookieText = encodeURIComponent(name) + "=" +
            encodeURIComponent(value);
        if (expires instanceof Date) {
            cookieText += "; expires=" + expires.toGMTString();
        }
        if (path) {
            cookieText += "; path=" + path;
        }
        if (domain) {
            cookieText += "; domain=" + domain;
        }
        if (secure) {
            cookieText += "; secure";
        }
        document.cookie = cookieText;
    },
    unset: function (name, path, domain, secure){
        this.set(name, "", new Date(0), path, domain, secure);
    }
};
CookieUtil.set("book", "Professional JavaScript");
alert(CookieUtil.get("book"));
CookieUtil.unset("book");
alert(CookieUtil.get("book"));

子cookie
因為cookie有單域名下數量的限制,一些開發人員使用了一種稱為子cookie的概念,子cookie是存放在單個cookie中更小段的數據,也就是使用cookie值來儲存多個名值對。最常見的格式如下:

name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5

這樣對于獲取到cookie就又多了一層障礙,為了更好的操縱cookie當然要在來個工具類咯~

var SubCookieUtil = {

    //獲取這個name的cookie中所有的子cookie并放入一個對象中
    getAll: function(name){
        var cookieName = encodeURIComponent(name) + "=",
            cookieStart = document.cookie.indexOf(cookieName),
            cookieValue = null,
            cookieEnd,
            subCookies,
            len,
            i,
            parts,
            result = {};
        if (cookieStart > -1){
            cookieEnd = document.cookie.indexOf(";", cookieStart);
            if (cookieEnd == -1){
                cookieEnd = document.cookie.length;
            }
            cookieValue = document.cookie.substring(cookieStart +
                cookieName.length, cookieEnd);
            if (cookieValue.length > 0){
                subCookies = cookieValue.split("&");
                for (i=0, len=subCookies.length; i < len; i++){
                    parts = subCookies[i].split("=");
                    result[decodeURIComponent(parts[0])] =
                        decodeURIComponent(parts[1]);
                }
                return result;
            }
        }
        return null;
    },
    //使用getAll方法返回的子cookie的對象,找到想要的子cookie
    get: function (name, subName){
        var subCookies = this.getAll(name);
        if (subCookies){
            return subCookies[subName];
        } else {
            return null;
        }
    },
    //將所有子cookie和相應參數序列化存進cookie里
    setAll: function(name, subcookies, expires, path, domain, secure){
        var cookieText = encodeURIComponent(name) + "=",
            subcookieParts = new Array(),
            subName;
        for (subName in subcookies){
            //這里使用hasOwnProperty來確定不會循環到原型鏈中其實不是子cookie的屬性
            if (subName.length > 0 && subcookies.hasOwnProperty(subName)){
                subcookieParts.push(encodeURIComponent(subName) + "=" +
                    encodeURIComponent(subcookies[subName]));
            }
        }
        if (subcookieParts.length > 0){
            cookieText += subcookieParts.join("&");
            if (expires instanceof Date) {
                cookieText += "; expires=" + expires.toGMTString();
            }
            if (path) {
                cookieText += "; path=" + path;
            }
            if (domain) {
                cookieText += "; domain=" + domain;
            }
            if (secure) {
                cookieText += "; secure";
            }
        } else {
            cookieText += "; expires=" + (new Date(0)).toGMTString();
        }
            document.cookie = cookieText;
    },
    //更改一個子cookie,這個方法會先找到這個子cookie所在的cookie中所有的子cookie,將這個新子cookie放到存著所有子cookie的對象中
    //再調用setAll方法保存
    set: function (name, subName, value, expires, path, domain, secure) {
        var subcookies = this.getAll(name) || {};
        subcookies[subName] = value;
        this.setAll(name,subcookies,expires, path, domain, secure);
    },
    unset: function (name, subName, path, domain, secure){
        var subcookies = this.getAll(name);
        if (subcookies){
            delete subcookies[subName];
            this.setAll(name, subcookies, null, path, domain, secure);
        }
    },
    unsetAll: function(name, path, domain, secure){
        this.setAll(name, null, new Date(0), path, domain, secure);
    }
};
document.cookie="data=name=Nicholas&book=Professional%20JavaScript";
var data = SubCookieUtil.getAll("data");
alert(data);
alert(data.name); //"Nicholas"
alert(data.book); //"Professional JavaScript"
alert(SubCookieUtil.get("data", "name")); //"Nicholas"
alert(SubCookieUtil.get("data", "book"));
alert(SubCookieUtil.get("data", "class"));
SubCookieUtil.set("data", "class", "how to kill people");
alert(SubCookieUtil.get("data", "class"));

JS不能訪問的cookie
為了保證cookie的安全,有的cookie會不允許JS獲取

Web儲存機制

Web Storage的目標是克服cookie的限制,當數據需要被嚴格控制在客戶端上時,無需持續的將數據發回到服務器。其兩個主要目標是:

  • 提供一種在cookie之外儲存會話數據的途徑
  • 提供一種儲存大量可以夸會話存在的數據的機制

Storage類型
可以以名值對的方式儲存字符串值,有如下方法:

  • clear()
  • getItem(name)
  • key(index)
  • removeItem(name)
  • setItem(name, value)

sessionStorage對象
這個對象儲存特定于某個會話的數據,這也就意味著這里的數據只保存到瀏覽器關閉。不過刷新頁面時這里的數據是可以存住的。存在這里的數據只能由最初儲存數據的頁面訪問。sessionStorage 其實是Storage的一個實例,所以上面的方法同樣可用。

sessionStorage.setItem("name", "Nicholas");
sessionStorage.book = "Professional JavaScript";
for (var i=0, len = sessionStorage.length; i < len; i++){
    var key = sessionStorage.key(i);
    var value = sessionStorage.getItem(key);
    alert(key + "=" + value);
}
sessionStorage.removeItem("book");

globalStorage對象
這個對象的目的是跨會話存儲數據,但是有域的限制,在儲存數據時,首先要指定的就是域:

globalStorage["wrox.com"].name = "Nicholas";  
var name = globalStorage["wrox.com"].name; 

globalStorage不是Storage的實例globalStorage["wrox.com"]才是哦。
這個域名下的所有子域可以訪問這里的數據。
對每個空間訪問的限制是根據域名,協議,端口來限制的,同一個域名。使用HTTP訪問就訪問不到HTTPS時存的數據。端口號不同也是一樣。

globalStorage[location.host ].name = "Nicholas";
globalStorage[location.host ].book = "Professional JavaScript";
globalStorage[location.host ].removeItem("name");
var book = globalStorage[location.host ].getItem("book");

localStorage對象
這個對象是為了取代globalStorage而存在的。這個也是跨會話的,不需要指定域名,只有完全相同的域名才能訪問,子域名都不行。

localStorage.setItem("name", "Nicholas");
localStorage.book = "Professional JavaScript";
var name = localStorage.getItem("name");
var book = localStorage.book;
alert(name);

Storage事件
對storage對象進行的任何修改都會觸發storage事件,這個事件的event有如下屬性:

  • domain
  • key
  • newValue
  • oldValue

各瀏覽器對這個事件的支持并不全面
大小限制
各瀏覽器對Storage大小的限制并不相同,不過都是根據域名來區分的。

IndexedDB

Indexed Database API,是用來在瀏覽器中保存結構化數據的一種數據庫。它的思想是創建一套API,方便保存和讀取JavaScript對象,同時還支持查詢及搜索。
IndexedDB設計的操作完全是異步進行的。每次對數據庫的操作都會返回一個相應的IDBRequest對象的實例來代表這次請求。在這個實例上可以設置事件,等待成功或失敗事件被觸發,在里面做相應的操作。IndexedDB是全局對象。API不穩定,有的瀏覽器為其加了前綴。

var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;

數據庫
打開數據庫,把數據庫名傳入,存在會打開,不存在會創建并打開。打開的數據庫的請求是一個IDBRequest對象,通過事件來觀察請求是否成功。成功就會返回一個IDBDatabase對象。
可以給database設置一個版本號,同樣是返回一個IDBRequest,同樣的操作模式。

var request, database;
request = indexedDB.open("admin");
request.onerror = function(event){
    alert("Something bad happened while trying to open: " +
        event.target.errorCode);
};
request.onsuccess = function(event){
    database = event.target.result;
    setVersion();
};
function setVersion() {
    if (database.version != "1.0"){
        request = database.setVersion("1.0");
        request.onerror = function(event){
            alert("Something bad happened while trying to set version: " +
                event.target.errorCode);
        };
        request.onsuccess = function(event){
            alert("Database initialization complete. Database name: " + database.name + ", Version: " + database.version);
        };
    } else {
        alert("Database already initialized. Database name: " + database.name + ", Version: " + database.version);
    }
}

對象儲存空間
建立了與數據庫的連接后,就可以使用對象儲存空間(相當于表,其中的對象相當于表中的紀錄)。
創建對象儲存空間,需要兩個信息,這個空間的名字,以及Key Path和Key Generator,這兩個值確定了這個空間中儲存的每個記錄以什么來標識。

  • No No:This object store can hold any kind of value, even primitive values like numbers and strings. You must supply a separate key argument whenever you want to add a new value.
  • Yes No:This object store can only hold JavaScript objects. The objects must have a property with the same name as the key path.
  • No Yes:This object store can hold any kind of value. The key is generated for you automatically, or you can supply a separate key argument if you want to use a specific key.
  • Yes Yes:This object store can only hold JavaScript objects. Usually a key is generated and the value of the generated key is stored in the object in a property with the same name as the key path. However, if such a property already exists, the value of that property is used as key rather than generating a new key.

創建空間需要在打開數據庫時返回的IDBRequest上的onupgradeneeded事件中進行,否則會報錯的。這個事件會在新創建數據庫或更新數據庫版本號時(open()時傳入更高的版本號,數據庫的版本就會自己更新)被觸發。

request.onupgradeneeded = function (event) {
    var db = event.target.result;

    // Create an objectStore to hold information about our customers. We're
    // going to use "ssn" as our key path because it's guaranteed to be
    // unique - or at least that's what I was told during the kickoff meeting.
    var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

    // Create an index to search customers by name. We may have duplicates
    // so we can't use a unique index.
    objectStore.createIndex("name", "name", { unique: false });

    // Create an index to search customers by email. We want to ensure that
    // no two customers have the same email, so use a unique index.
    objectStore.createIndex("email", "email", { unique: true });

    // Use transaction oncomplete to make sure the objectStore creation is
    // finished before adding data into it.
    objectStore.transaction.oncomplete = function(event) {
        // Store values in the newly created objectStore.
        var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
        for (var i in customerData) {
            customerObjectStore.add(customerData[i]);
        }
    };
};

在創建了空間后,就可以用add()或put()來向其中添加要保存的對象。對于添加唯一標識已經存在的對象,add會報錯,put則會更新原有值。
事務
在創建好空間后,就相當于數據庫的結構已經確定了,在這之后對數據的操作就要通過事務了。
創建事務需要指定針對哪個儲存空間以及讀寫模式,可以一下打開多個儲存空間。

var transaction = db.transaction(["customers"], "readwrite");

取得事務索引后使用objectStore()訪問儲存空間,然后就可以使用add()、put()、get()、delete()、clear()。這五個方法都會返回一個請求對象,通過事件來操作

var request = db.transaction("users").objectStore("users").get("007"); 
request.onerror = function(event){
    alert("Did not get the object!");
};
request.onsuccess = function(event){
    var result = event.target.result;
    alert(result.firstName);    //"James"
};
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,836評論 18 139
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,339評論 11 349
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,738評論 18 399
  • 村上春樹的文字,讓人上癮。 他的小說里,音樂和性是不可或缺的兩大元素。音樂高雅又不失穩重,引領我們體會與音樂相伴的...
    婷你說閱讀 1,700評論 0 1
  • 2017年11月21號星期二哈爾濱晴-20°~-5° 學習是一種習慣,干什么都是一種習慣,從十月一號回到家里就不開...
    c0deb784fc0b閱讀 242評論 0 0