天天看點

用戶端存儲第20章 用戶端存儲

第20章 用戶端存儲

Web應用允許使用浏覽器提供的API實作将資料存儲到使用者的電腦上。這種用戶端存儲相當于賦予了Web浏覽器記憶功能。比方說,Web應用就可以用這種方式來“記住”使用者的配置資訊甚至是使用者所有的狀态資訊,以便準确地“回憶”起使用者上一次通路時候的狀态。用戶端存儲遵循“同源政策”,是以不同站點的頁面是無法互相讀取對方存儲資料的,而同一站點的不同頁面之間是可以互相共享存儲資料的,它為我們提供了一種通信機制,例如,一個頁面上填寫的表單資料可以顯示在另外一個頁面中。Web應用可以選擇它們存儲資料的有效期:比如采用臨時存儲可以讓資料儲存至目前視窗關閉或者浏覽器退出;采用永久存儲,可以将資料永久地存儲到硬碟上,永不失效。

用戶端存儲有以下幾種形式:

Web存儲

Web存儲最初作為HTML5一部分被定義成API形式,但是後來被剝離出來作為獨立的一份标準了。該标準目前還在草案階段,但其中一部分内容已經被包括IE8在内的所有主流浏覽器實作了。Web存儲标準所描述的API包含了 localStorage 對象和 sessionStorage 對象,這兩個對象實際上是持久化關聯數組,是名值對的映射表,“名”和“值”都是字元串。Web存儲易于使用、支援大容量(但非無限量)資料存儲同時相容目前所有主流浏覽器,但是不相容早期浏覽器。§20.1 會對 localStorage 和 sessionStorage 這兩個對象作詳細介紹。

Cookies

Cookie是一種早期的用戶端存儲機制,起初是針對伺服器端腳本設計使用的。盡管在用戶端提供了非常繁瑣的JavaScript API來操作Cookie,但卻難用之極,而且隻能存儲少量文本資料。不僅如此,任何以Cookie形式存儲的資料,不論伺服器端是否需要,每一次HTTP請求都會把這些資料傳輸到伺服器端。Cookie目前仍然被用戶端開發者大量使用的一個重要原因是:所有浏覽器都支援它。但是,随着Web Storage的普及,Cookie終将會回歸到最初的形态:作為一種被服務端腳本使用的用戶端存儲機制。§20.2會詳細介紹Cookie。

IE User Data

微軟在IE5及之後的IE浏覽器中實作了它專屬的用戶端機制——”userData”。userData可以實作一定量的文本資料存儲,對于IE8以前的IE浏覽器中,可以将其用作是Web存儲的替代方案。關于userData的API會在§20.3中作相應介紹。

離線Web應用

HTML5标準中定義了一組“離線Web應用”API,用以緩存web頁面以及相關資源(腳本、CSS檔案、圖檔等等)。它實作的是将web應用整體存儲在用戶端,而不僅僅是存儲資料。它能夠讓web應用“安裝”在用戶端,這樣一來,哪怕網絡不可用的時候web應用依然是可用的。離線web應用相關的内容會在§20.4中作介紹。

Web資料庫

為了能夠讓開發者像使用資料庫那樣來操作大量資料,很多主流的浏覽器紛紛開始內建用戶端資料庫的功能。Safari、Chrome和Opera都内置了SQL資料庫的用戶端API。不幸的是,這類API的标準化工作以失敗告終,并且Firefox和IE看樣子也都不打算實作這種API。目前還有一種正在标準化的資料庫API,稱為“索引資料庫API(Indexed Database API)”。調用該API傳回的是一個不包含查詢語言的簡單資料庫對象。這兩種用戶端資料庫API都是異步的,都使用了事件處理機制(譯注:類似DOM事件機制),這樣的方式多多少少會顯得有些複雜。本章中不會對他們做介紹,但是 §22.8 中會對索引資料庫API作概要介紹同時會提供一些例子。

檔案系統API

本書第8章介紹過現在主流浏覽器都支援一個檔案對象,用以将擇的檔案通過XMLHttpRequest上傳到服務端。與之相關的規範(草案階段)定義了一組API,用于操作一個私有的本地檔案系統。在該檔案系統中可以進行對檔案的讀寫操作。這些内容正在緊鑼密鼓标準化當中,這些API将在§22.7中作介紹。随着這些API被廣泛地實作和支援,web應用可以使用類似基于檔案的存儲機制,這對于大部分程式員來說再熟悉不過了。

存儲、安全和隐私

Web浏覽器通常會提供“記住密碼”功能,這些密碼會以加密的形式安全地存儲到硬碟上。然而,本章介紹的任何形式的用戶端資料存儲都不牽涉加密:任何存儲在使用者硬碟上的資料都是未加密的。這樣一來,對于擁有電腦通路權限的惡意使用者以及惡意軟體(比如:間諜軟體)同樣也可以擷取到存儲的資料。是以,用戶端存儲不應該用來儲存密碼、商業賬号或者其他類似的敏感資訊。記住:盡管使用者通路你的網站時,願意在表單中輸入一些資訊,但絕不代表使用者願意将這些資訊儲存到硬碟上。就拿信用卡卡号來舉例好了,這是使用者的隐私,使用者并不願意公開,如果你利用用戶端存儲永久将該資訊存儲起來,這無異于你将信用卡号寫在一張便簽紙上,随後黏在使用者的鍵盤上,讓所有人都看到。 還有要謹記的一點:很多web使用者不信任那些使用cookie和其他用戶端存儲機制來做類似“跟蹤‘功能的網站。是以,盡量嘗試用本章讨論的存儲機制來為你的網站提升使用者體驗;而不是用它們來收集和侵犯隐私相關的資料。如果網站濫用用戶端存儲,使用者将會禁用該功能,這樣一來不僅起不到效果,還會導緻依賴用戶端存儲的網站完全不可用。

20.1 localStorage 和 sessionStorage

實作了”Web存儲“草案标準的浏覽器在window對象上定義了兩個屬性:localStorage和sessionStorage。這兩個屬性都代表同一個Storage對象——一個持久化關聯數組,數組使用字元串來索引,存儲的值也都是字元串形式的。Storage對象在使用上和一般的JavaScript對象沒什麼差別:設定對象的屬性為字元串值,随後浏覽器會将該值存儲起來。localStorage和sessionStorage兩者的差別在于存儲的有效期和作用域的不同:資料可以存儲多長時間以及誰擁有資料的通路權。

下面,我們會對存儲的有效期和作用域作詳細的解釋。不過,在此之前,讓我們先來看些例子。下面的代碼使用的是localStorage,但是它對sessionStorage也同樣适用:

var name = localStorage.username; // 查詢一個存儲的值。
name = localStorage["username"]; // 等價于數組表示法
if (!name) {
    name = prompt("What is your name?"); // 詢問使用者一個問題。
    localStorage.username = name; // 存儲使用者的答案。
}

// 疊代所有存儲的name/value對
for(var name in localStorage) { // 疊代所有存儲的名字
    var value = localStorage[name]; // 查詢每個名字對應的值
}
           

Storage對象還定義了一些諸如存儲、擷取、周遊和删除的方法。這些方法會在§20.1.2中作介紹。

”Web存儲”草案标準指出,我們既可以存儲結構化的資料(對象和數組),也可以存儲原始類型資料,還可以存儲諸如日期、正規表達式甚至檔案對象在内的内置類型的資料。但是,截止至本書截稿時,浏覽器僅僅支援存儲字元串類型資料。如果想要存儲和擷取其他類型的資料,不得不自己手動進行編碼和解碼。如下例子所示:

// 當存儲一個數字的時候,它會被自動轉換成字元串。
// 但是,當擷取該值的時候别忘記了手動将其轉換成數字類型。
localStorage.x = 10;
var x = parseInt(localStorage.x);

// 同樣地,存儲一個日期類型資料的時候進行編碼,擷取的時候進行解碼
localStorage.lastRead = (new Date()).toUTCString();
var lastRead = new Date(Date.parse(localStorage.lastRead));

// 使用JSON可以使得對基本資料類型編碼的工作變得很友善
localStorage.data = JSON.stringify(data); // 編碼然後存儲。
var data = JSON.parse(localStorage.data); // 擷取數值之後再解碼。
           

20.1.1 存儲有效期和作用域

localStorage和sessionStorage的差別在于存儲的有效期和作用域的不同。通過localStorage存儲的資料是永久性的,除非web應用刻意删除存儲的資料,或者使用者通過設定浏覽器配置(浏覽器提供的配置界面)來删除,否則資料将一直保留在使用者的電腦上,永不過期。

localStorage的作用域是限定在文檔源(document origin)級别的。正如§13.6.2所介紹的,文檔源是通過協定、主機名以及端口三者來确定的,是以,下面每個URL都擁有不同的文檔源:

http://www.example.com              // 協定: http; 主機名: www.example.com
https://www.example.com            // 不同協定
http://static.example.com              // 不同主機名
http://www.example.com:8000      // 不同端口
           

同源的文檔間共享同樣的localStorage資料(不論該源的腳本是否真正地操作localStorage)。它們可以互相讀取對方的資料,甚至可以覆寫對方的資料。但是,非同源的文檔間互相都不能讀寫對方資料(即使它們運作的腳本是來自同一台第三方伺服器也不行)。

需要注意的是localStorage的作用域也受浏覽器供應商限制。如果你使用Firefox通路站點,那麼下次用另一個浏覽器比方說是Chrome再次通路的時候,那麼本次是無法擷取上次存儲的資料的。

通過sessionStorage存儲的資料和通過localStorage存儲的資料的有效期也是不同的:前者有效期和存儲資料的腳本所在的最頂層的視窗或者是浏覽器标簽頁是一樣的。一旦視窗或者标簽頁被永久關閉了,那麼所有通過sessionStorage存儲的資料也都被删除了。(當時要注意的是,現代浏覽器已經具備了重新打開最近關閉的标簽頁随後恢複上一次浏覽的session功能,是以,這些标簽頁以及與之相關的sessionStorage的有效期可能會更加長些)。

與localStorage一樣,sessionStorage的作用域也是限定在文檔源中,是以非同源文檔間都是無法共享sessionStorage的。不僅如此,sessionStorage 的作用域還被限定在視窗中。如果同源的文檔渲染在不同的浏覽器标簽頁中,那麼他們互相之間擁有的是各自的sessionStorage資料,無法共享;一個标簽頁中的腳本是無法讀取或者覆寫由另一個标簽頁腳本寫入的資料,哪怕這兩個标簽頁渲染的是同一個頁面,運作的是同一個腳本也不行。

要注意的是:這裡提到的基于視窗作用域的sessionStorage指的視窗隻是頂級視窗。如果一個浏覽器标簽頁包含兩個

<iframe>

元素,他們所包含的文檔是同源的,那麼這兩者之間是可以共享sessionStorage的。

20.1.2 存儲API

localStorage和sessionStorage通常被當做普通的JavaScript對象使用:通過設定屬性來存儲字元串值,查詢該屬性來讀取該值。除此之外,這兩個對象還提供了更加正式的API。調用setItem()方法,将對應的名字和值傳遞進去,可以實作資料存儲。調用getItem()方法,将名字傳遞進去,可以擷取對應的值。調用removeItem()方法,将名字傳遞進去,可以删除對應的資料。(在非IE8浏覽器中,還可以使用delete操作符來删除資料,就和對普通的對象使用delete操作符一樣。)調用clear()方法(不需要參數),可以删除所有存儲的資料。最後,使用length屬性以及key()方法,傳入0到length-1的數字,可以枚舉所有存儲資料的名字。下面是一些使用localStrage的例子。這些代碼對sessionStorage也适用:

localStorage.setItem("x", 1);   // 以“x”的名字存儲一個數值
localStorage.getItem("x");   // 擷取數值

// 枚舉所有存儲的名字/值對
for(var i = 0; i < localStorage.length; i++) { // length表示了所有名字/值對的總數
    var name = localStorage.key(i); // 擷取第i對的名字
    var value = localStorage.getItem(name); // 擷取該對的值
}

localStorage.removeItem("x"); // 删除“x”項
localStorage.clear(); // 全部删除
           

盡管通過設定和查詢屬性能更加友善的存儲和擷取資料,但是有的時候還是不得不使用上面提到的這些方法的。比方說,其中clear()方法是唯一能删除存儲對象中所有名/值對的方式。同樣的還有,removeItem()方法也是唯一通用的删除單個名/值對的方式,因為IE8不支援delete操作符。

如果浏覽器提供商完全實作了“Web存儲”的标準,支援對象和數組類型的資料存儲,那麼就會又多了一個使用類似于setItem()和getItem()這類方法的理由了。對象和數組類型的值通常是可變的,是以存儲對象要求存儲他們的副本,以確定之後任何對這類對象的改變都不影響到存儲的對象。同樣的,在擷取該對象的時候也要求擷取的是該對象的副本,以確定對已擷取對象的改動不會影響到存儲的對象。而這類操作如果使用基于屬性的API就會令人困惑。考慮下面這段代碼(假設浏覽器已經支援了結構化資料的存儲):

localStorage.o = {x:1}; // 存儲一個帶有“x”屬性的對象
localStorage.o.x = 2; // 試圖去設定該對象的屬性值
localStorage.o.x // => 1: x沒有變
           

上述第二行代碼想要設定存儲的對象的屬性值,但是事實上,它擷取到的隻是存儲的對象的拷貝,随後設定了該對象的屬性值,然後就将該拷貝的對象廢棄了。真正存儲的對象保持不變。像這樣的情況,使用getItem()就不會這麼讓人困惑了。

localStorage.getItem("o").x = 2; // 我們并不想存儲2
           

PI的理由就是:在還不支援“Web存儲”标準的浏覽器中,其他的存儲機制的頂層API對其也是相容的。下面這段代碼使用cookies和IE userDate來實作存儲API、如果使用基于方法的API,當localStorage可用的時候就可以使用它編寫代碼,而當它在其他浏覽器上不可用的時候依然可以降級到其他的存儲機制中(譯者注:完全相容)。代碼如下所示:

// 識别出使用的是哪類存儲機制
var memory = window.localStorage ||
    (window.UserDataStorage && new UserDataStorage()) ||
    new CookieStorage();
// 然後在對應的機制中查詢資料
var username = memory.getItem("username");
           

20.1.3 存儲事件

無論什麼時候存儲在localStorage或者sessionStorage的資料發生改變,浏覽器都會對其他對該資料可見的視窗對象上觸發存儲事件(但是,在對資料進行改變的視窗對象上是不會觸發的)。如果浏覽器有兩個标簽頁都打開了來自同源的頁面,其中一個頁面在localStorage上存儲了資料,那麼另外一個标簽頁就會接收到一個存儲事件。要記住的是sessionStorage作用域是限制在頂層視窗的,是以對sessionStorage的改變隻有當有相牽連的視窗的時候才會觸發存儲事件。還有要注意的是,隻有當存儲資料真正發生改變的時候才會觸發存儲事件。像給已經存在的存儲項設定一個一模一樣的值,抑或是删除一個本來就不存在的存儲項都是不會觸發存儲事件的。

為存儲事件注冊處理程式可以通過addEventListener()方法(或者在IE下使用attachEvent()方法)。在絕大多數浏覽器中,還可以使用給window對象設定onstorage屬性的方式,不過Firefox不支援該屬性。

與存儲事件相關的事件對象有五個非常重要的屬性(遺憾的是,IE8不支援它們):

key

被設定或者移除的項的名字或者鍵名。如果調用的是clear()函數,那麼該屬性值為null。

newValue

該項的新的值;或者調用removeItem()時,該屬性值為null。

oldValue

被改變或者删除前,該項原先的值;當插入一個新項的時候,該屬性值為null。

storageArea

這個屬性值就好比是目标window對象上的localStorage屬性或者是sessionStorage屬性

url

觸發該存儲變化腳本所在文檔的URL

最後要注意的是:localStorage和存儲事件都是采用廣播機制的,浏覽器會對目前正在通路同樣站點的所有視窗發送消息。舉個例子,比方說一個使用者要求網站停止動畫效果,那麼站點可能會在localStorage中存儲該使用者的配置,這樣依賴,以後再通路該站點的時候就自動停止動畫效果了。因為存儲了該配置,導緻了觸發一個存儲事件讓其他展現統一站點的視窗也獲得了這樣的一個使用者請求。再比如,一個基于web的圖檔編輯應用,通常允許在其他的視窗中展示工具條。當使用者選擇一個工具的時候,應用就可以使用localStorage來存儲目前的狀态,然後通知其他視窗使用者選擇了新的工具。

20.2 Cookies

cookie是指浏覽器存儲的少量資料,同時它是與具體的web頁面或者站點相關的。Cookie最早是設計為被服務端所用的,從最底層來看,它被實作為是HTTP協定的一種擴充。cookie資料會被自動在web浏覽器和web伺服器之間傳輸的,是以服務端腳本就可以讀寫存儲在用戶端的cookie的值。本節内容将介紹用戶端的腳本通過使用Document對象上的cookie屬性也能實作對cookie的操作。

為什麼叫“Cookie?”

“cookie”這個名字沒有太多的含義,但是在計算機曆史上其實很早就有用到它了。“cookie”和“magic cookie”用于代表少量資料,特别是指類似密碼這種用于識别身份或者許可通路的保密資料。在JavaScript中,cookie被用于儲存狀态以及能夠為web浏覽器提供一種身份識别機制。但是,JavaScript中使用cookie不會采用任何加密機制,是以他們是不安全的。(但是,通過https來傳輸cookie資料是安全的,不過這和cookie本身無關,而和https協定相關。)

操作cookie的API很早就已經定義和實作了,是以該API的相容性很好。但是,該API幾乎形同虛設。根本沒有提供諸如查詢、設定、删除cookie的方法,所有這些操作都要通過以特殊格式的字元串形式讀寫Document對象的cookie屬性來完成。每個cookie的有效期和作用域都可以通過cookie屬性來指定。這些屬性也是通過在同一個cookie屬性上以特殊格式的字元串來設定的。

本節剩餘部分會解釋如何通過cookie屬性來指定cookie的有效期和作用域,以及如何通過JavaScript來設定和查詢cookie的值。最後,将以一個“實作基于cookie的存儲API”例子來結束本節的介紹。

檢測cookie是否啟用

由于濫用第三方cookie(譯注:第三方cookie指的是來自于目前通路站點以為的站點設定的cookie)(如:cookie是和網頁上的圖檔相關而非網頁本身相關)的緣故,導緻cookie在大多數web使用者心目中都留下了很不好的印象。比如,廣告公司可以利用第三方cookie來實作跟蹤使用者的通路行為和習慣,而使用者為了禁止這種”窺探“使用者隐私的行為會在他們的浏覽器中禁用cookie。是以,在使用cookie前,首先要確定cookie是啟用的。在絕大多數浏覽器中,可以通過檢測navigator.cookieEnabled這一屬性。若該值為true,則目前cookie是啟用的,反之則是被禁用的(但是,隻具備”目前浏覽會話生命周期“的非持久化cookie仍然是啟用的)。但是,該屬性不是一個标準的屬性(不是所有浏覽器都支援的)。是以在不支援該屬性的浏覽器上,必須通過使用下面将要介紹的技術嘗試着讀、寫和删除測試cookie資料來測試是否支援cookie。

20.2.1 Cookie屬性:有效期和作用域

除了名(name)和值(value),cookie還有一些可選的屬性來控制cookie的有效期和作用域。cookie預設的有效期很短暫;它隻能持續在web浏覽器的會話期間,一旦使用者關閉浏覽器cookie,儲存的資料就丢失了。要注意的是:這與sessionStorage的有效期還是有差別的:coookie的作用域并不是局限在單個的浏覽器視窗中,它的有效期和整個浏覽器程序而不是單個浏覽器視窗的有效期一緻。如果想要延長cookie的有效期,可以通過設定max-age屬性,但是必須要明确告訴浏覽器cookie的有效期是多長(機關是秒)。一旦設定了有效期,浏覽器就會将cookie資料存儲在一個檔案中,并且直到過了指定的有效期才會删除該檔案。

和localStorage以及sesstionStorage類似,cookie的作用域是通過文檔源和文檔路徑來确定的。該作用域通過cookie的path和domain屬性也是可配置的。預設情況下,cookie和建立它的頁面有關,并對該頁面以及和該頁面同目錄或者子目錄的其他頁面可見。比如,Web頁面http://www.example.com/catalog/index.html頁面建立了一個cookie,那麼該cookie對http://www.example.com/catalog/order .html頁面和http://www.example.com/catalog/widgets/index.html頁面都是可見的,但它對http://www.example.com/about.html頁面不可見。

預設的cookie的可見性行為滿足了最常見的需求。不過,有的時候,你可能希望讓整個網站都能夠使用cookie的值,而不管是哪個頁面建立它的。比方說,當使用者在一個頁面表單中輸入了他的郵件位址,你想将它儲存下來,為了下次該使用者回到這個頁面填寫表單,或者網站其他頁面任何地方要求輸入賬單位址的時候,将其作為預設的郵件位址。要滿足這樣的需求,可以通過設定cookie的路徑(設定cookie的path屬性)。

這樣一來,來自同一伺服器的Web頁面,隻要其URL是以指定的路徑字首開始的,都可以共享cookie。舉例來說,如果http://www.example.com/catalog/widgets/index.html 頁面建立了一個cookie,并且将該路徑設定成“/catalog”,那麼該cookie對于http://www.example.com/catalog/order.html 頁面也是可見的。或者,如果把路徑設定成“/”,那麼該cookie對任何http://www.example.com 這台Web伺服器上的頁面都是可見的。

将cookie的路徑設定成“/”等于是讓cookie和localStorage擁有同樣的作用域,同時請求該站點上任何一個頁面的時候,浏覽器都必須将cookie的名字和值傳遞給伺服器。但是,要注意的是,cookie的path屬性不能被用作通路控制機制。如果一個Web頁面想要讀取同一站點其他頁面的cookie,隻要簡單的将其他頁面以隐藏

<iframe>

的形式嵌入進來,随後讀取對應文檔的cookie就可以了。同源政策(§13.6.2)限制了跨站的cookie窺探,但是對于同一站點的文檔它是完全合法的。

cookie的作用域預設是被文檔源限制。但是,有的大型網站想要子域之間能夠互相共享cookie。比方說,order.example.com域下的頁面想要讀取catalog.example.com域下設定的cookie。這個時候就需要通過設定cookie的domain屬性來達到目的了。如果一個catalog.example.com域下的頁面建立了一個cookie,并将其path屬性設定成“/”,其domain屬性設定成“.example.com”,那該cookie就對所有catalog.example.com、orders.example.com以及任何其他example.com域下的頁面可見了。如果沒有為一個cookie設定域屬性,那麼domain屬性的預設值是目前Web伺服器的主機名。要注意的是,cookie的域隻能設定為目前伺服器的域。

最後要介紹的cookie的屬性是secure,它是一個布爾類型的屬性,用來表明cookie的值以何種形式通過網絡傳遞。cookie預設是以不安全的形式(通過普通的,不安全的HTTP連接配接)被傳遞的。而一旦cookie被辨別為“安全的”,那就隻能當浏覽器和伺服器通過HTTPS或者其他的安全協定連接配接的時候才能傳遞它。

20.2.2 儲存cookies

要給目前文檔設定預設有效期的cookie值,非常簡單,隻需将cookie屬性設定為一個字元串形式的值:

name=value
           

如下所示:

document.cookie = "version=" + encodeURIComponent(document.lastModified);
           

下次讀取cookie屬性的時候,之前存儲的名/值對的資料就在文檔的cookie清單中。由于cookie的名/值中的值是不允許包含分号,逗号和空白符,是以,在存儲前一般可以采用JavaScript核心的全局函數encodeURIComponent()對值進行編碼。相應的,讀取cookie值的時候需要采用decodeURIComponent()函數解碼。

以簡單的名/值對形式存儲的cookie資料有效期隻在目前web浏覽器的會話内,一旦使用者關閉浏覽器,cookie資料就丢失了。如果想要延長cookie的有效期,就需要設定max-age屬性來指定cookie的有效期。按照如下的字元串形式即可:

name=value; max-age=seconds
    下面的函數用來設定一個cookie的值,同時提供可選的max-age屬性:
    // 以名/值的形式存儲cookie
    // 同時采用encodeURIComponent()函數進行編碼,來避免分号、逗号和空白符
    // 如果daysToLive是一個數字, 設定max-age屬性為該數值表示cookie直到制定的天數到
    //了才會過期。如果daysToLive是0就表示删除cookie。
    function setCookie(name, value, daysToLive) {
        var cookie = name + "=" + encodeURIComponent(value); 
        if (typeof daysToLive === "number")
            cookie += "; max-age=" + (daysToLive*60*60*24); 
            document.cookie = cookie;
    }
           

同樣的,如果要設定cookie的path,domain和secure屬性,隻需在cookie值存儲前,以如下字元串形式追加在cookie值的後面:

; path=path 
; domain=domain 
; secure
           

要想改變cookie的值,需要使用相同的名字、路徑和域,但是新的值重新設定cookie。同樣的,設定新max-age屬性就可以改變原來的cookie的有效期。

要想删除一個cookie,需要以相同的名字、路徑和域,然後指定一個任意(非空)的值,以及将max-age屬性指定為0,重新設定一次cookie。

20.2.3 讀取cookies

使用JavaScript來讀取cookie屬性的時候,其傳回的值是一個字元串,該字元串都是由一系列名值對組成,不同名值對之間通過“分号和空格”分開,其内容包含了所有作用在目前文檔的cookie。但是,它并不包含其他設定的cookie屬性。通過document.cookie屬性可以擷取cookie的值,但是為了更好的檢視cookie的值,一般會采用split()方法,将cookie值中的名值對都分離出來。

把cookie的值分離出來之後,必須要采用相應的解碼方式(取決于之前存儲cookie值時采用的編碼方式),把值還原出來。比方說,先采用decodeURIComponent()方法把cookie值解碼出來,之後再利用JSON.parse()方法轉化成json對象。

例20-1,定義了一個getCookie()函數,該函數将document.cookie屬性的值解析出來,将對應的名值對,存儲到一個對象中,函數最後傳回該對象。

例 20-1. 解析document.cookies屬性值

// 将document.cookie的值以一個名值對形式組成的對象傳回
// 假設cookie的值存儲的時候是采用encodeURIComponent()函數編碼的
function getCookies() {
    var cookies = {};                     // 初始化最後要傳回的對象
    var all = document.cookie;     // 擷取所有的cookie值
    if (all === "")                     // 如果該cookie屬性值為空
        return cookies;                     // 傳回一個空對象
    var list = all.split("; ");             // 分離出名值對
    for(var i = 0; i < list.length; i++) {                   // 循環每對資料
        var cookie = list[i];
        var p = cookie.indexOf("=");                     // 查找第一個“=”符号
        var name = cookie.substring(0,p);           // 擷取名字
        var value = cookie.substring(p+1);          // 擷取對應的值
        value = decodeURIComponent(value);   // 對其值進行解碼
        cookies[name] = value;                            // 将名值對存儲到對象中
    }
    return cookies;
}
           

20.2.4 Cookie的局限性

Cookie的設計初衷是給服務端腳本用來存儲少量資料的,該資料會在每次請求中傳遞到伺服器端。 RFC 2965 鼓勵浏覽器供應商對cookie的數目和大小不做限制。可是,要知道,該标準不允許浏覽器儲存超過300個cookie,為每個Web伺服器儲存的cookie數不能超過20個(是對整個伺服器而言,而不僅僅指伺服器上的頁面和站點),而且,每個cookie儲存的資料不能超過4KB(即名字和值的總量不能超過4KB的限制)。實際上,現代浏覽器允許cookie總數超過300個,但是部分浏覽器對單個cookie大小仍然後4KB的限制。

20.2.5 Cookie相關的存儲

例20-2,展示了如何實作基于cookie的一系列存儲API方法。該例定義了一個CookieStorage函數(被執行個體化的時候具有構造器特性),通過将max-age和path屬性傳遞給該構造器,就會傳回一個對象,然後就可以像使用localStorage和sessionStorage一樣來使用這個對象了。但是要注意的是,該例中并沒有實作存儲事件,是以,當你設定和查詢CookieStorage對象的屬性的時候,不會實作自動儲存和擷取對應的值。

例20-2. 實作基于cookie的存儲API

/*
* CookieStorage.js
* 本類實作像localStorage和sessionStorage一樣的存儲API,不同的是,基于HTTP Cookies.
*/
function CookieStorage(maxage, path) { // 兩個參數分别代表了存儲有效期和作用域
    // 擷取一個存儲全部cookie資訊的對象
    var cookies = (function() { // 類似之前介紹的getCookies()方法
        var cookies = {}; // 該對象最終會傳回
        var all = document.cookie; // 以大字元串的形式擷取所有cookie資訊
        if (all === "") // 如果該屬性為空
        return cookies; //傳回一個空對象
        var list = all.split("; "); // 分離出名值對
        for (var i = 0; i < list.length; i++) { // 循環每對資料
            var cookie = list[i];
            var p = cookie.indexOf("="); // 查找第一個“=”符号
            var name = cookie.substring(0, p); // 擷取名字
            var value = cookie.substring(p + 1); // 擷取對應的值
            value = decodeURIComponent(value); // 對其值進行解碼
            cookies[name] = value; // 将名值對存儲到對象中
        }
        return cookies;
    } ());

    // 将所有cookie的名字存儲到一個數組中
    var keys = [];
    for (var key in cookies) keys.push(key);

    // 現在定義存儲API公共的屬性和方法
    // 存儲的cookie的個數
    this.length = keys.length;

    // 傳回第n個cookie的名字, 如果n越界則傳回null
    this.key = function(n) {
        if (n < 0 || n >= keys.length) return null;
        return keys[n];
    };

    // 傳回指定名字的cookie值, 如果不存在則傳回null.
    this.getItem = function(name) {
        return cookies[name] || null;
    };

    // 存儲cookie值
    this.setItem = function(key, value) {
        if (! (key in cookies)) { // 如果要存儲的cookie還不存在
            keys.push(key); // 将指定的名字加入到存儲所有cookie名的數組中
            this.length++; // cookie個數加一
        }
        // 将該名/值對資料存儲到cookie對象中
        cookies[key] = value;
        // 開始正式設定cookie
        // 首先将要存儲的cookie的值進行編碼,同時建立一個“名字=編碼後的值”形式的字元串
        var cookie = key + "=" + encodeURIComponent(value);
        //将cookie的屬性也加入到該字元串中
        if (maxage) cookie += "; max-age=" + maxage;
        if (path) cookie += "; path=" + path;
        // 通過document.cookie屬性來設定cookie
        document.cookie = cookie;
    };

    // 删除指定的cookie
    this.removeItem = function(key) {
        if (! (key in cookies)) return; // 如果不存在,則什麼也不做
        // 從内部維護的cookie删除指定的cookie
        delete cookies[key];

        // 同時将cookie中的名字也在内部的數組中删除
        // 如果使用ES5定義的數組 indexOf()方法會更加簡單
        for (var i = 0; i < keys.length; i++) { // 循環所有的名字
            if (keys[i] === key) { //當我們找到了要找的那個
                keys.splice(i, 1); // 将它從數組中删除
                break;
            }
        }
        this.length--; // cookie個數減一
        // 最終通過将該cookie值設定為空字元串以及将有效期設定為0來删除指定的cookie.
        document.cookie = key + "=; max-age=0";
    };

    // 删除所有的cookie
    this.clear = function() {
        // 循環所有的cookie的名字,并将cookie删除
        for (var i = 0; i < keys.length; i++)
        document.cookie = keys[i] + "=; max-age=0";
        // 重置所有的内部狀态
        cookies = {};
        keys = [];
        this.length = 0;
    };
}
           

20.3 利用IE userData來持久化資料

IE5以及IE5以上版本浏覽器是通過在document元素上附加了一個專屬的“DHTML行為”來實作用戶端存儲的。如以下代碼所示:

var memory = document.createElement("div"); // 建立一個元素
memory.id = "_memory"; // 設定一個id名
memory.style.display = "none"; // 将其隐藏
memory.style.behavior = "url('#default#userData')"; // 附加userData行為
document.body.appendChild(memory); // 将其添加到document元素中
           

一旦給元素賦予了“userData”行為,那該元素就擁有了load()和save()方法。load()方法用于載入存儲的資料。使用它的時候必須傳遞一個字元串作為參數——類似于一個檔案名,該參數用來指定要載入的存儲資料。當資料載入後,就可以通過該元素的屬性來通路這些名/值對形式的資料,可以使用getAttribute()來查詢這些資料。通過setAttribute()方法設定屬性,然後調用sava()方法可以實作存儲新的資料;而要删除資料,通過使用removeAttribute()方法然後調用save()方法即可。如下所示(該例中使用了此前例子中的那個memory元素):

memory.load("myStoredData"); // 根據指定名,載入對應的資料
var name = memory.getAttribute("username"); //擷取其中的資料片段
if (!name) { // 如果沒有指定的資料片段
    name = prompt("What is your name?); // 擷取使用者輸入
memory.setAtttribute("
    username ", name); // 将其設定成memory元素的一個屬性
memory.save("
    myStoredData "); // 儲存
}
           

erData存儲的資料,除非手動去删除它,否則永不失效。但是,你也可以通過設定expires屬性來指定它的過期時間。就拿上面的例子來說,你可以給存儲的資料設定時長100天的有效期,如下所示:

var now = (new Date()).getTime(); // 擷取目前時間,以毫秒為機關
var expires = now + 100 * 24 * 60 * 60 * 1000; // 距離目前時間100天,換算成毫秒的機關
expires = new Date(expires).toUTCString(); // 将其轉換成字元串
memory.expires = expires; // 設定userData的過期時間
           

IE userData的作用域限制在和目前文檔同目錄的文檔中。它的作用域沒有cookie寬泛,cookie對其所在目錄下的子目錄也有效。userData的機制并沒有提供像cookie那樣,可以設定path和domain屬性來控制或者改變其作用域的方式。

userData允許存儲的資料量要比cookie大,但是卻比localStorage以及sessionStorage允許存儲的資料量要小。

例20-3,基于IE的userData實作了存儲API提供的getItem()、setItem()以及removeItem()方法。(但是沒有實作key()或者clear()方法,原因是userData并沒有定義周遊所有存儲項的方法)

例20-3. 基于IE的userData實作部分存儲API

function UserDataStorage(maxage) {
    // 建立一個元素并附加userData行為
    // 該元素獲得save()和load()方法
    var memory = document.createElement("div"); // 建立一個元素
    memory.style.display = "none"; // 将其隐藏
    memory.style.behavior = "url('#default#userData')"; // 附加userData行為
    document.body.appendChild(memory); // 将該元素添加到document元素中
    // 如果傳遞了maxage參數(機關為秒),則将其設定為userData的有效期,以毫秒為機關
    if (maxage) {
        var now = new Date().getTime(); // 目前時間
        var expires = now + maxage * 1000; // 目前時間加上有效期就等于過期時間
        memory.expires = new Date(expires).toUTCString();
    }
    // 通過載入存儲的資料來初始化memory元素
    // 參數是任意的,隻要是在儲存的時候存在的就可以了
    memory.load("UserDataStorage"); // 載入存儲的資料
    this.getItem = function(key) { // 通過屬性來擷取儲存的值
        return memory.getAttribute(key) || null;
    };
    this.setItem = function(key, value) {
        memory.setAttribute(key, value); // 以設定屬性的形式來儲存資料
        memory.save("UserDataStorage"); //儲存資料改變後的狀态
    };
    this.removeItem = function(key) {
        memory.removeAttribute(key); // 删除存儲的資料
        memory.save("UserDataStorage"); // 再次儲存狀态
    };
}
           

由于上述代碼隻在IE浏覽器下有效,最好使用IE條件注釋來避免其他浏覽器載入上述代碼。

<!--[if IE]>
<script src="UserDataStorage.js"></script>
<![endif]-->
           

20.4 應用程式存儲和離線web應用

HTML5中新增了“應用程式緩存”,允許web應用将應用自身本地儲存到使用者的浏覽器中。不像localStorage和sessionStorage隻是儲存應用程式相關的資料,它是将應用自身儲存起來——應用程式所需的所有檔案(HTML,CSS,JavaScript,圖檔等等)。“應用程式緩存”和一般的浏覽器緩存不同:它不會随着使用者清除浏覽器緩存而被清除。同時,緩存起來的應用程式也不會像一般固定大小的緩存那樣,老資料會被最近一次通路的新資料代替掉。它其實不是臨時存儲在緩存中:應用程式更像是被“安裝”在那裡,除非被使用者“解除安裝”或者“删除”,否則他們就會一直“駐紮”在那裡。是以,總的來說,“應用程式”真正意義上不是“緩存”,更好的說法應該稱之為“應用程式存儲”。

讓web應用能夠實作“本地安裝”的目的是要保證他們能夠在離線狀态(比如當在飛機上或者手機沒信号的時候)下依然可通路。将自己“安裝”到應用程式緩存中的web應用,在離線狀态下使用localStorage來儲存應用相關的資料,同時還具備一套同步機制,在再次回到線上狀态的時候,能夠将存儲的資料傳輸給伺服器。在§20.4.3我們會看到一個離線web應用的例子。不過,在這之前,先來介紹下應用程式是如何将自己“安裝”到應用程式緩存中的。

20.4.1 應用程式緩存清單

想要将應用“安裝”到應用程式緩存中,首先要建立一個清單:包含了所有應用程式依賴的檔案清單。然後,通過在應用主HTML頁面的

<html>

标簽中設定manifest屬性,指向到該清單檔案就可以了:

<!DOCTYPE HTML>
<html manifest="myapp.appcache">
<head>...</head>
<body>...</body>
</html>
           

清單檔案中的首行内容必須是“CACHE MANIFEST”。其餘就是要緩存的檔案URL清單,一行一個URL。相對路徑的URL都是相對于清單檔案的。内容中的空行會被忽略,以“#”開始的會被作為注釋而忽略。并且同一行注釋前面是不允許非空字元的。如下所示是一個簡單的清單檔案:

CACHE MANIFEST
# 上一行指定了此檔案是一個清單檔案。本行是注釋。
# 下面的内容都是應用程式依賴的資源檔案的URL
myapp.html
myapp.js
myapp.css
images/background.png
           

緩存清單的MIME類型

應用程式緩存清單檔案約定以.appcache作為檔案字尾名。但是,這也僅僅隻是約定而已,web伺服器真正識别清單檔案的方式是通過“text/cache-manifest”這個MIME類型。如果伺服器将清單檔案的content-type的頭資訊設定成其他MIME類型,那應用程式就不會被緩存了。是以,可能需要對伺服器做一定的配置來使用這個MIME類型,比方說,在web應用目錄下建立一個Apache伺服器的.htaccess檔案。

清單檔案包含要緩存的應用的辨別。如果一個web應用有很多web頁面(使用者可以通路多個HTML頁面),那每個HTML頁面就需要設定

<html manifest=>

來指向清單檔案。事實上,将這些不同的頁面都指向同一個清單檔案,可以很清楚的表達出他們都是需要緩存起來、同時又是來自于同一個web應用的。如果一個應用隻有少量的HTML頁面,那一般會把這些頁面都顯示的列在清單中。但這不是強制的:任何連接配接到清單檔案的檔案都會被認為是web應用的一部分,并會随着應用一起緩存起來。

像之前提到的,一個簡單的清單必須列出所有web應用依賴的資源。一旦一個web應用首次下載下傳下來并緩存,之後任何的請求都将從緩存中去擷取。一個應用從緩存中去載入資源的時候,就要求請求的資源務必要在清單中。不在清單中的資源就不會被載入。這種政策有點離線的味道。如果一個簡單的緩存起來的應用能夠從緩存中載入并運作,那它也可以在離線狀态下運作。通常情況下,很多複雜的web應用無法将他們依賴的所有資源都緩存起來。但是,如果他們同時也有一個複雜的清單的話,他們仍然可以使用應用程式緩存。

20.4.1.1 複雜的清單

一個應用從應用程式緩存中載入的時候,隻有清單檔案中列舉出來的資源檔案會被載入。前面例子中的清單檔案中一次列舉一個URL。事實上,清單檔案還有比這更複雜的文法,列舉資源的方式也還有另外兩種。在清單檔案中可以使用特殊的區域頭(譯者注:類似于HTTP頭)來辨別該頭資訊之後清單項的類型。像該例中列舉的簡單緩存項事實上都屬于“CACHE:”區域,這也是預設的區域。另外兩種區域是以“Network:”和”FALLBACK:” 頭資訊開始的(一個清單檔案可以有任意數量的區域,而且可以相鄰兩個區域之間可以根據需要互相切換)。

“NETWORK:”區域辨別了該區域中的資源從不緩存,總要通過網絡擷取。通常,會将一些服務端的腳本資源放在”NETWORK:”區域中,而一般該區域中的資源的URL都隻是URL字首,用來表示以此URL字首開頭的資源都應該要通過網絡加載。當然了,如果浏覽器處于離線狀态,那這些資源都将擷取失敗。”NETWORK:”區域中的URL還支援”*”通配符。該通配符表示對任何不在清單中的資源,浏覽器都将通過網絡去擷取。這實際上違背了這樣一條規則:緩存應用程式必須要在清單中列舉所有應用相關的資源!

“FALLBACK:”區域中的清單項每行都包含兩個URL。第二個URL是指需要緩存起來的資源,第一個URL是一個字首。任何能夠比對到該字首的資源都不會緩存起來,但是可能的話,他們會從網絡中載入。如果從網絡中載入失敗的話,會使用第二個URL指定的資源來代替,從緩存中擷取。想象一個應用包含了一定數量的視訊教程。這些視訊都很大,顯然把他們緩存到本地是不合适的。是以,在離線狀态下,通過清單檔案中的fallback區域,就可以使用一些文本類的幫助文檔來代替了。

下面是一個更加複雜的緩存清單:

CACHE MANIFEST

CACHE: 
myapp.html 
myapp.css 
myapp.js

FALLBACK: 
videos/ offline_help.html

NETWORK: 
cgi/
           

20.4.2 緩存的更新

當一個web應用從緩存中載入的時候,所有與之相關的檔案也是直接從緩存中擷取。線上狀态下,浏覽器會異步檢查是否清單檔案有更新。如果有更新,新的清單檔案以及所有清單中列舉的檔案都會下載下傳下來重新儲存到應用程式緩存中。但是,要注意的是,浏覽器隻是檢查清單檔案,而不會去檢查緩存的檔案是否有更新:隻檢查清單檔案。比方說,你修改了一個緩存的JavaScript檔案,要想讓該檔案生效,就必須去更新下清單檔案。由于應用程式依賴的檔案清單其實并沒有變化,是以最簡單的方式就是更新版本号:

CACHE MANIFEST 
# MyApp version 1 (更改這個數字以便讓浏覽器重新下載下傳這個檔案)        
MyApp.html 
MyApp.js
           

同樣的,如果想要讓web應用從緩存中“解除安裝”,要在伺服器端删除清單檔案,使得請求該檔案的時候傳回HTTP 404 無法找到 的錯誤,同時,修改HTML檔案與該清單清單“斷開連結”。

要注意的是,浏覽器檢查清單檔案以及更新緩存的操作是異步的,可能是在從緩存中載入應用之前,也有可能同時進行。是以,對于簡單的web應用而言,在你更新清單檔案之後,使用者必須載入應用兩次才能保證最新的版本生效:第一次是從緩存中載入老版本随後更新緩存,第二次才從緩存中載入最新的版本。

浏覽器在更新緩存過程中會觸發一系列事件,你可以通過注冊處理程式來跟蹤這個過程同時提供回報給使用者。 如下所示:

applicationCache.onupdateready = function() {
    var reload = confirm("A new version of this application is available\n" + 
                            "and will be used the next time you reload.\n" + 
                            "Do you want to reload now?");
    if (reload) location.reload();
}
           

要注意的是,該事件處理程式是注冊在ApplicationCache對象上的,該對象是window的applicationCache的屬性值。支援應用程式緩存的浏覽器會定義該屬性。此外,除了上面例子中的updateready時間之外,還有其他7種應用程式緩存事件可以監控。例20-4展示了一個簡單的處理程式通過顯示對應的消息來通知使用者緩存更新的進度,以及目前緩存的狀态。

例 20-4. 處理應用緩存相關事件

// 下面所有的事件處理程式都使用此函數來顯示狀态消息
// 由于都是通過調用status函數來顯示狀态,是以所有處理程式都傳回false來阻止浏覽器顯示其預設狀态消息
function status(msg) {
    // 将消息輸出到id為“statusline”的文檔元素中
    document.getElementById("statusline").innerHTML = msg;
    console.log(msg); // 同時在終端輸出此消息,便于debug
}

// 每當應用程式載入的時候,都會檢查該清單檔案
// “checking”事件也總會首先被觸發
window.applicationCache.onchecking = function() {
    status("Checking for a new version.");
    return false;
};

// 如果清單檔案沒有改動,同時應用程式也已經緩存了
// "noupdate"事件會被觸發,整個過程結束
window.applicationCache.onnoupdate = function() {
    status("This version is up-to-date.")
    return false;
};

// 如果應用程式還未被緩存,或者清單檔案有改動
// 浏覽器會去下載下傳并混存所有清單中的資源
// "downloading"事件被觸發,同時意味着下載下傳過程開始
window.applicationCache.ondownloading = function() {
    status("Downloading new version");
    window.progresscount = 0; // 在下面的"progress"事件處理程式會用到
    return false;
};

// "progress"事件會在下載下傳過程中被間斷性的被觸發
//  通常是在每個檔案下載下傳完畢的時候
window.applicationCache.onprogress = function(e) {
    // 事件對象應當是"process"事件(就像哪些被XHR2使用的)的,
    // 通過該對象可以計算出下載下傳完成比例,但是,如果它不是個“process”事件,
    // 我們計算調用的次數
    var progress = "";
    if (e && e.lengthComputable) // "process"事件: 計算下載下傳完成比例
    progress = " " + Math.round(100 * e.loaded / e.total) + "%"
    else // 否則,輸出調用次數
    progress = " (" + ++progresscount + ")"
    status("Downloading new version" + progress);
    return false;
};
// 當下載下傳完成,首次将應用程式下載下傳到緩存中時,浏覽器
// 會觸發"cached"事件
window.applicationCache.oncached = function() {
    status("This application is now cached locally");
    return false;
};

// 當下載下傳完成,并将緩存中的應用程式更新後,浏覽器會觸發“updateready”事件
// 要注意的是:觸發此事件的時候,使用者任然可以看到老版本的應用程式.
window.applicationCache.onupdateready = function() {
    status("A new version has been downloaded. Reload to run it");
    return false;
};

// 如果浏覽器處于離線狀态,檢查清單清單失敗,則會觸發“error”事件
// 當一個未緩存的應用程式引用了一個不存在的清單檔案,也會觸發此事件
window.applicationCache.onerror = function() {
    status("Couldn't load manifest or cache application");
    return false;
};

// 如果一個緩存了的應用程式引用了一個不存在的清單檔案
// 會觸發"obsolete"事件,同時會将應用從緩存中移除
// 之後都不會從緩存而是通過網絡來加載資源
window.applicationCache.onobsolete = function() {
    status("This application is no longer cached. " + "Reload to get the latest version from the network.");
    return false;
};
           

每次載入一個設定了manifest屬性的HTML檔案,浏覽器都會觸發"checking"事件,并通過網絡載入該清單檔案。不過之後,會随着不同的情況觸發不同的事件。

沒有可用的更新

如果應用程式已經緩存了并且清單檔案沒有改動,則浏覽器會觸發“noupdate”事件。

有可用的更新

如果應用程式已經緩存了并且清單檔案發生了改動,則浏覽器會觸發“downloading”事件,并開始下載下傳和緩存清單檔案中列舉的所有資源。随着下載下傳過程的進行,浏覽器還會觸發"progress"事件,在下載下傳完成後,會觸發"updateready"事件。

首次載入新的應用

如果應用程式還未被緩存,如上所述,“downloading”事件和"progress"事件都會觸發。但是,當下載下傳完成後,會觸發"cached"事件而不是"updateready"事件。

浏覽器處于離線狀态

如果浏覽器處于離線狀态,它無法檢查清單檔案,同時會觸發"error"事件。如果一個未被緩存的應用引用了一個不存在的清單檔案,也會觸發該事件。

清單檔案不存在

如果浏覽器處于線上狀态,應用程式也已經緩存起來了,但是清單檔案不存在(傳回404無法找到錯誤),浏覽器會觸發"obsolete"事件,并将該應用從緩存中移除。

除了使用事件處理程式之外,還可以使用applicationCache.status屬性來檢視目前緩存狀态。該屬性有6個可能的屬性值:

ApplicationCache.UNCACHED (0)

應用程式沒有設定manifest屬性:未被緩存

ApplicationCache.IDLE (1)

清單檔案已經檢查完畢,并且已經緩存了最新應用程式

ApplicationCache.CHECKING (2)

浏覽器正在檢查清單檔案

ApplicationCache.DOWNLOADING (3)

浏覽器正在下載下傳并緩存清單中列舉的所有資源

ApplicationCache.UPDATEREADY (4)

已經下載下傳和緩存了最新版的應用程式

ApplicationCache.OBSOLETE (5)

清單檔案不存在,緩存将被清除

ApplicationCache對象還定義了兩個方法:update()方法調用了更新緩存算法去檢測是否有最新版本的應用程式。這和應用程式第一次載入時,浏覽器去檢測清單檔案(觸發響應的事件)的效果是一樣的。

還有一個方法是:swapCache(),該方法更加巧妙。還記得當浏覽器下載下傳并緩存更新版本的應用時,使用者仍然在運作老版本的應用吧。隻有當使用者再次載入應用時,才會通路到最新版本。是以必須要保證老版本的應用也要工作正常。同時要注意的是,老版本的應用相關資源可能是從緩存中擷取的:比方說,應用程式可能使用XMLHttpRequest去擷取檔案,而這些請求也務必要保證能夠從緩存中擷取到。是以,浏覽器在使用者再次載入應用前必須在緩存中保留老版本的應用。

swapCache()方法告訴浏覽器可以棄用老的緩存,所有的請求都從新緩存中擷取。要注意的是,這并不會重新載入應用程式:所有已經載入的HTML檔案、圖檔、腳本等等資源都不會改變。但是,之後的請求都将從最新的緩存中擷取。這會導緻“版本錯亂”的問題,是以,一般不推薦使用,除非你的應用設計的很好,確定這樣的方式沒有問題。想象下,比方說,有這麼個應用程式,它什麼也不做,就隻是在浏覽器檢查清單檔案整個過程中,顯示過渡畫面(譯者注:類似loading圖)。觸發"noupdate"事件時,它繼續“前進”并載入應用程式首頁。觸發“downloading”事件,并且更新緩存後,它顯示合适的回報給使用者。觸發“updateready”事件時,它調用swapCache()方法,然後從最新的緩存中載入更新過的首頁。

要注意的是,隻有當狀态屬性是ApplicationCache.UPDATEREADY 或者ApplicationCache.OBSOLETE時,才能調用swapCache()方法(當狀态是OBSOLETE時,調用swapCache()方法可以立即棄用廢棄的緩存,讓之後所有的請求都通過網絡擷取)。如果在狀态屬性是其他數值的時候調用swapCache()方法會抛出異常。

20.4.3 離線Web應用

離線web應用指的是将自己“安裝”在應用程式緩存中的程式,使得哪怕在浏覽器處于離線狀态時候依然可通路。舉個最簡單的例子——類似時鐘和萬花筒生成器這樣的應用——這是web應用要離線可用需要做的事情。但是,大多數web用也需要向伺服器上傳資料:哪怕是簡單的遊戲應用都有可能需要上傳使用者的最高得分到伺服器。這類應用也可以成為離線應用。他們可以使用localStorage來存儲應用資料,然後當線上的時候再将資料上傳到伺服器。在本地存儲和伺服器端同步資料是将Web應用轉變為離線應用最巧妙的環節,特别是當使用者需要從多台裝置擷取資料的時候。

為了在離線狀态可用,web應用需要可以告知别人自己是離線還是線上,同時當網絡連接配接的狀态發生改變時候也能“感覺”到。通過navigator.onLine屬性,可以檢測浏覽器是否線上,同時,在Window對象上注冊線上和離線事件處理程式,可以檢測網絡連接配接狀态的改變。

本章将以一個簡單的離線web應用結束,該應用使用了這些技術。該應用名叫“PermaNote”——一個簡單的記事本程式,它将使用者的文本儲存到locaStorage中,并且在網絡可用的時候,将其上傳到伺服器。PermaNote隻允許使用者編輯單個筆記,而且不考慮任何授權和身份驗證的問題——假設服務端有區分使用者的方式,但是不包括任何登入界面。PermaNote應用包含三個檔案。例20-5,是一個緩存清單檔案,它列出了另外兩個檔案,同時指定“note”這個URL不需要緩存:我們使用此URL來實作在服務端讀寫筆記資料。

例 20-5. permanote.appcache

CACHE MANIFEST
# PermaNote v8
permanote.html
permanote.js
NETWORK:
note
           

例20-6是第二個PermaNote應用的檔案:一個HTML檔案,定義了簡單的編輯器的UI:中間是一個

<textarea>

元素,上面是一排的按鈕,下面是狀态條。要注意的是,

<html>

标簽設定了manifest屬性。

例 20-6. permanote.html

<!DOCTYPE HTML>
<html manifest="permanote.appcache">
  <head>
    <title>PermaNote Editor</title>
    <script src="permanote.js"></script>
    <style>
    #editor { width: 100%; height: 250px; }
    #statusline { width: 100%; }
   </style>
  </head>
  <body>
    <div id="toolbar">
      <button id="savebutton" onclick="save()">Save</button>
      <button onclick="sync()">Sync Note</button>
     <button onclick="applicationCache.update()">Update Application</button>
    </div>
    <textarea id="editor"></textarea>
    <div id="statusline"></div>
  </body>
</html>
           

最後,例20-7展示的是PermaNode應用的JavaScript代碼。它定義了一個status()在狀态條上展示消息,一個save()方法來儲存目前版本的筆記到伺服器,以及一個sync()方法來確定本地和伺服器端的筆記資料的同步。其中,save()和sync()使用了第18章中介紹的腳本化的HTTP技術。(有趣的是,save()函數使用了HTTP的”PUT“方法而不是常見的”POST“方法。)

除了這三個基本的方法外,例20-7還定義了一些事件處理程式。為了保持本地和伺服器端的筆記資料同步,應用程式需要一些事件處理程式:

onload

嘗試和伺服器同步,一旦有新版本的筆記并且完成同步後,啟用編輯器視窗 saven()和sync()方法發出HTTP請求,并在XMLHttpRequest對象上注冊onload事件處理程式來擷取上傳或者下載下傳完成的提醒

onbeforeunload

在未上傳前,儲存目前版本的筆記資料到伺服器。

oninput

每當

<textarea>

元素内容發生變化時,都将其内容儲存到localStorage中,并啟動一個計時器。當使用者停止編輯超過5秒,就自動儲存筆記資料到伺服器。

onoffline

當浏覽器進入離線狀态時,在狀态欄中顯示離線狀态

ononline

當浏覽器回到線上狀态時,同步伺服器檢查是否有新版本的資料,并且儲存目前版本資料。

onupdateready

如果新版本的應用(已緩存)準備就緒了,就在狀态條中展示消息來告知使用者。

onnoupdate

如果應用程式緩存沒有發生變化,則通知使用者他/她仍在運作目前版本。

例20-7展示了PermaNote應用的事件驅動邏輯的概覽:

例 20-7. permanote.js

// 一些貫穿始終的變量
var editor, statusline, savebutton, idletimer;
// 首次載入應用
window.onload = function() {
    // 第一次載入時,初始化本地存儲
    if (localStorage.note == null) localStorage.note = "";
    if (localStorage.lastModified == null) localStorage.lastModified = 0;
    if (localStorage.lastSaved == null) localStorage.lastSaved = 0;

    // 查找UI元素,并初始化全局變量
    editor = document.getElementById("editor");
    statusline = document.getElementById("statusline");
    savebutton = document.getElementById("savebutton");

    editor.value = localStorage.note; // 初始化編輯器,将儲存的筆記資料填充為其内容
    editor.disabled = true; // 同步前禁止編輯
    // 一旦文本區有内容輸入
    editor.addEventListener("input", function(e) {
        // 将新的值儲存到locaStorage中
        localStorage.note = editor.value;
        localStorage.lastModified = Date.now();
        // 重置閑置計時器
        if (idletimer) clearTimeout(idletimer);
        idletimer = setTimeout(save, 5000);
        // 啟用儲存按鈕
        savebutton.disabled = false;
    },
    false);

    // 每次載入應用程式時,嘗試同步伺服器
    sync();
};

// 離開頁面前儲存資料到伺服器
window.onbeforeunload = function() {
    if (localStorage.lastModified > localStorage.lastSaved) save();
};

// 離線時,通知使用者
window.onoffline = function() {
    status("Offline");
}

// 再次傳回線上狀态時,進行同步
window.ononline = function() {
    sync();
};

// 當有新版本應用的時候,提醒使用者
// 這裡我們也可以采用location.reload()方法來強制重新載入應用
window.applicationCache.onupdateready = function() {
    status("A new version of this application is available. Reload to run it");
};

// 當沒有新版本的時候也通知使用者
window.applicationCache.onnoupdate = function() {
    status("You are running the latest version of the application.");
};

// 一個用于在狀态條中顯示消息的函數
function status(msg) {
    statusline.innerHTML = msg;
}

// 每當筆記内容更新後,如果使用者停止編輯超過5分鐘,
// 就會自動将筆記資料上傳到伺服器(線上狀态下)
function save() {
    if (idletimer) clearTimeout(idletimer);
    idletimer = null;
    if (navigator.onLine) {
        var xhr = new XMLHttpRequest();
        xhr.open("PUT", "/note");
        xhr.send(editor.value);
        xhr.onload = function() {
            localStorage.lastSaved = Date.now();
            savebutton.disabled = true;
        };
    }
}

// 檢查服務端是否有新版本的筆記,
// 如果沒有,則将目前版本儲存到伺服器端
function sync() {
    if (navigator.onLine) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/note");
        xhr.send();
        xhr.onload = function() {
            var remoteModTime = 0;
            if (xhr.status == 200) {
                var remoteModTime = xhr.getResponseHeader("Last-Modified");
                remoteModTime = new Date(remoteModTime).getTime();
            }
            if (remoteModTime > localStorage.lastModified) {
                status("Newer note found on server.");
                var useit = confirm("There is a newer version of the note\n" + 
                        "on the server. Click Ok to use that version\n" + 
                        "or click Cancel to continue editing this\n" + 
                        "version and overwrite the server");
                var now = Date.now();
                if (useit) {
                    editor.value = localStorage.note = xhr.responseText;
                    localStorage.lastSaved = now;
                    status("Newest version downloaded.");
                }
                else status("Ignoring newer version of the note.");
                localStorage.lastModified = now;
            }
            else status("You are editing the current version of the note.");
            if (localStorage.lastModified > localStorage.lastSaved) {
                save();
            }

            editor.disabled = false; // 再次啟用編輯器
            editor.focus(); // 将光标定位到編輯器中
        }
    }
    else { // 離線狀态下,不能同步
        status("Can't sync while offline");
        editor.disabled = false;
        editor.focus();
    }
}
           

繼續閱讀