二十、離線應(yīng)用與客戶端存儲(chǔ)

??支持離線 Web 應(yīng)用開發(fā)是 HTML5 的另一個(gè)重點(diǎn)。

??所謂離線 Web 應(yīng)用,就是在設(shè)備不能上網(wǎng)的情況下仍然可以運(yùn)行的應(yīng)用。

??HTML5 吧離線應(yīng)用作為重點(diǎn),主要是基于開發(fā)人員的心愿。前端開發(fā)人員一直希望 Web 應(yīng)用能夠與傳統(tǒng)的客戶端應(yīng)用同場競技,起碼做到只要設(shè)備有電就能使用。

??開發(fā)離線 Web 應(yīng)用需要幾個(gè)步驟:

  • 首先是確保應(yīng)用知道設(shè)備是否能上網(wǎng),以便下一步執(zhí)行正確的操作。
  • 然后,應(yīng)用還必須能訪問一定的資源(圖像、JavaScript、CSS 等),只有這樣才能正常工作。
  • 最后,必須有一塊本地空間用于保存數(shù)據(jù),無論能否上網(wǎng)都不妨礙讀寫。

??HTML5 及其相關(guān)的 API 讓開發(fā)離線應(yīng)用成為現(xiàn)實(shí)。

1、離線檢測

??開發(fā)離線應(yīng)用的第一步是要知道設(shè)備是在線還是離線,HTML5 為此定義了一個(gè) navigator.onLine 屬性,這個(gè)屬性值為 true 表示設(shè)備能上網(wǎng),值為 false 表示設(shè)備離線。這個(gè)屬性的關(guān)鍵是瀏覽器必須知道設(shè)備能否訪問網(wǎng)絡(luò),從而返回正確的值。實(shí)際應(yīng)用中,navigator.onLine 在不同瀏覽器間還有些小的差異。

  • IE6+和 Safari 5+能夠正確檢測到網(wǎng)絡(luò)已斷開,并將 navigator.onLine 的值轉(zhuǎn)換為 false。
  • Firefox 3+和 Opera 10.6+支持 navigator.onLine 屬性,但你必須手工選中菜單項(xiàng)“文件 → Web 開發(fā)人員(設(shè)置)→ 脫機(jī)工作”才能讓瀏覽器正常工作。
  • Chrome 11 及之前版本始終將 navigator.onLine 屬性設(shè)置為 true。這是一個(gè)有待修復(fù)的bug。(這個(gè) bug 在 2011 年 10 月已被修復(fù)。)

??由于存在上述兼容性問題,單獨(dú)使用 navigator.onLine 屬性不能確定網(wǎng)絡(luò)是否連通。即便如此,在請求發(fā)生錯(cuò)誤的情況下,檢測這個(gè)屬性仍然是管用的。以下是檢測該屬性狀態(tài)的示例。

if (navigator.onLine){
    // 正常工作
} else {
    // 執(zhí)行離線狀態(tài)時(shí)的任務(wù)
}

??除 navigator.onLine 屬性之外,為了更好地確定網(wǎng)絡(luò)是否可用,HTML5 還定義了兩個(gè)事件:online 和 offline。
??當(dāng)網(wǎng)絡(luò)從離線變?yōu)樵诰€或者從在線變?yōu)殡x線時(shí),分別觸發(fā)這兩個(gè)事件。這兩個(gè)事件在 window 對(duì)象上觸發(fā)。

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

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

??為了檢測應(yīng)用是否離線,在頁面加載后,最好先通過 navigator.onLine 取得初始的狀態(tài)。然后,就是通過上述兩個(gè)事件來確定網(wǎng)絡(luò)連接狀態(tài)是否變化。
??當(dāng)上述事件觸發(fā)時(shí),navigator.onLine 屬性的值也會(huì)改變,不過必須要手工輪詢這個(gè)屬性才能檢測到網(wǎng)絡(luò)狀態(tài)的變化。
??支持離線檢測的瀏覽器有 IE 6+(只支持 navigator.onLine 屬性)、Firefox 3、Safari 4、Opera 10.6、Chrome、iOS 3.2 版 Safari 和 Android 版 WebKit。

2、應(yīng)用緩存

??HTML5 的應(yīng)用緩存(application cache),或者簡稱為 appcache,是專門為開發(fā)離線 Web 應(yīng)用而設(shè)計(jì)的。
??Appcache 就是從瀏覽器的緩存中分出來的一塊緩存區(qū)。要想在這個(gè)緩存中保存數(shù)據(jù),可以使用一個(gè)描述文件(manifest file),列出要下載和緩存的資源。下面是一個(gè)簡單的描述文件示例。

CACHE MANIFEST
#Comment

file.js
file.css

??在最簡單的情況下,描述文件中列出的都是需要下載的資源,以備離線時(shí)使用。設(shè)置描述文件的選項(xiàng)非常多,本書不打算詳細(xì)解釋每一個(gè)選項(xiàng)。要了解這些選項(xiàng),推薦讀者閱讀 HTML5Doctor 中的文章“Go offline with application cache”,網(wǎng)址為:http://html5doctor.com/go-offline-with-application-cache。
??要將描述文件與頁面關(guān)聯(lián)起來,可以在<html>中的 manifest 屬性中指定這個(gè)文件的路徑,例如:

<html manifest="/offline.manifest">

??以上代碼告訴頁面,/offline.manifest 中包含著描述文件。這個(gè)文件的 MIME 類型必須是 text/cache-manifest。(描述文件的擴(kuò)展名以前推薦用 manifest,但現(xiàn)在推薦的是 appcache。)

??雖然應(yīng)用緩存的意圖是確保離線時(shí)資源可用,但也有相應(yīng)的 JavaScript API 讓你知道它都在做什么。
??這個(gè) API 的核心是 applicationCache 對(duì)象,這個(gè)對(duì)象有一個(gè) status 屬性,屬性的值是常量,表示應(yīng)用緩存的如下當(dāng)前狀態(tài)。

  • 0:無緩存,即沒有與頁面相關(guān)的應(yīng)用緩存。
  • 1:閑置,即應(yīng)用緩存未得到更新。
  • 2:檢查中,即正在下載描述文件并檢查更新。
  • 3:下載中,即應(yīng)用緩存正在下載描述文件中指定的資源。
  • 4:更新完成,即應(yīng)用緩存已經(jīng)更新了資源,而且所有資源都已下載完畢,可以通過 swapCache() 來使用了。
  • 5:廢棄,即應(yīng)用緩存的描述文件已經(jīng)不存在了,因此頁面無法再訪問應(yīng)用緩存。

??應(yīng)用緩存還有很多相關(guān)的事件,表示其狀態(tài)的改變。以下是這些事件。

  • checking:在瀏覽器為應(yīng)用緩存查找更新時(shí)觸發(fā)。
  • error:在檢查更新或下載資源期間發(fā)生錯(cuò)誤時(shí)觸發(fā)。
  • noupdate:在檢查描述文件發(fā)現(xiàn)文件無變化時(shí)觸發(fā)。
  • downloading:在開始下載應(yīng)用緩存資源時(shí)觸發(fā)。
  • progress:在文件下載應(yīng)用緩存的過程中持續(xù)不斷地觸發(fā)。
  • updateready:在頁面新的應(yīng)用緩存下載完畢且可以通過 swapCache() 使用時(shí)觸發(fā)。
  • cached:在應(yīng)用緩存完整可用時(shí)觸發(fā)。

??一般來講,這些事件會(huì)隨著頁面加載按上述順序依次觸發(fā)。不過,通過調(diào)用 update() 方法也可以手工干預(yù),讓應(yīng)用緩存為檢查更新而觸發(fā)上述事件。

applicationCache.update();

??update() 一經(jīng)調(diào)用,應(yīng)用緩存就會(huì)去檢查描述文件是否更新(觸發(fā) checking 事件),然后就像頁面剛剛加載一樣,繼續(xù)執(zhí)行后續(xù)操作。
??如果觸發(fā)了 cached 事件,就說明應(yīng)用緩存已經(jīng)準(zhǔn)備就緒,不會(huì)再發(fā)生其他操作了。
??如果觸發(fā)了 updateready 事件,則說明新版本的應(yīng)用緩存已經(jīng)可用,而此時(shí)你需要調(diào)用 swapCache() 來啟用新應(yīng)用緩存。

EventUtil.addHandler(applicationCache, "updateready", function(){
    applicationCache.swapCache();
});

??支持 HTML5 應(yīng)用緩存的瀏覽器有 Firefox 3+、Safari 4+、Opera 10.6、Chrome、iOS 3.2+版 Safari及 Android 版 WebKit。在 Firefox 4 及之前版本中調(diào)用 swapCache() 會(huì)拋出錯(cuò)誤。

3、數(shù)據(jù)存儲(chǔ)

??隨著 Web 應(yīng)用程序的出現(xiàn),也產(chǎn)生了對(duì)于能夠直接在客戶端上存儲(chǔ)用戶信息能力的要求。想法很合乎邏輯,屬于某個(gè)特定用戶的信息應(yīng)該存在該用戶的機(jī)器上。無論是登錄信息、偏好設(shè)定或其他數(shù)據(jù),Web 應(yīng)用提供者發(fā)現(xiàn)他們在找各種方式將數(shù)據(jù)存在客戶端上。
??這個(gè)問題的第一個(gè)方案是以 cookie 的形式出現(xiàn)的,cookie 是原來的網(wǎng)景公司創(chuàng)造的。一份題為“Persistent Client State: HTTP Cookes”(持久客戶端狀態(tài):HTTP Cookies)的標(biāo)準(zhǔn)中對(duì) cookie 機(jī)制進(jìn)行了闡述(該標(biāo)準(zhǔn)還可以在這里看到:http://curl.haxx.se/rfc/cookie_spec.html)。今天,cookie 只是在客戶端存儲(chǔ)數(shù)據(jù)的其中一種選項(xiàng)。

3.1、Cookie

??HTTP Cookie,通常直接叫做 cookie,最初是在客戶端用于存儲(chǔ)會(huì)話信息的。該標(biāo)準(zhǔn)要求服務(wù)器對(duì)任意 HTTP 請求發(fā)送 Set-Cookie HTTP 頭作為響應(yīng)的一部分,其中包含會(huì)話信息。例如,這種服務(wù)器響應(yīng)的頭可能如下:

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

??這個(gè) HTTP 響應(yīng)設(shè)置以 name 為名稱、以 value 為值的一個(gè) cookie,名稱和值在傳送時(shí)都必須是 URL 編碼的。瀏覽器會(huì)存儲(chǔ)這樣的會(huì)話信息,并在這之后,通過為每個(gè)請求添加 Cookie HTTP 頭將信息發(fā)送回服務(wù)器,如下所示:

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

??發(fā)送回服務(wù)器的額外信息可以用于唯一驗(yàn)證客戶來自于發(fā)送的哪個(gè)請求。

1. 限制

??cookie 在性質(zhì)上是綁定在特定的域名下的。當(dāng)設(shè)定了一個(gè) cookie 后,再給創(chuàng)建它的域名發(fā)送請求時(shí),都會(huì)包含這個(gè) cookie。這個(gè)限制確保了儲(chǔ)存在 cookie 中的信息只能讓批準(zhǔn)的接受者訪問,而無法被其他
域訪問。
??由于 cookie 是存在客戶端計(jì)算機(jī)上的,還加入了一些限制確保 cookie 不會(huì)被惡意使用,同時(shí)不會(huì)占據(jù)太多磁盤空間。每個(gè)域的 cookie 總數(shù)是有限的,不過瀏覽器之間各有不同。如下所示。

  • IE6 以及更低版本限制每個(gè)域名最多 20 個(gè) cookie。
  • IE7 和之后版本每個(gè)域名最多 50 個(gè)。IE7 最初是支持每個(gè)域名最大 20 個(gè) cookie,之后被微軟的一個(gè)補(bǔ)丁所更新。
  • Firefox 限制每個(gè)域最多 50 個(gè) cookie。
  • Opera 限制每個(gè)域最多 30 個(gè) cookie。
  • Safari 和 Chrome 對(duì)于每個(gè)域的 cookie 數(shù)量限制沒有硬性規(guī)定。

??當(dāng)超過單個(gè)域名限制之后還要再設(shè)置 cookie,瀏覽器就會(huì)清除以前設(shè)置的 cookie。IE 和 Opera 會(huì)刪除最近最少使用過的(LRU,Least Recently Used)cookie,騰出空間給新設(shè)置的 cookie。Firefox 看上去好像是隨機(jī)決定要清除哪個(gè) cookie,所以考慮 cookie 限制非常重要,以免出現(xiàn)不可預(yù)期的后果。
??瀏覽器中對(duì)于 cookie 的尺寸也有限制。大多數(shù)瀏覽器都有大約 4096B(加減 1)的長度限制。為了最佳的瀏覽器兼容性,最好將整個(gè) cookie 長度限制在 4095B(含 4095)以內(nèi)。尺寸限制影響到一個(gè)域下所有的 cookie,而并非每個(gè) cookie 單獨(dú)限制。
??如果你嘗試創(chuàng)建超過最大尺寸限制的 cookie,那么該 cookie 會(huì)被悄無聲息地丟掉。注意,雖然一個(gè)字符通常占用一字節(jié),但是多字節(jié)情況則有不同。

2. cookie 的構(gòu)成

??cookie 由瀏覽器保存的以下幾塊信息構(gòu)成。

  • 名稱:一個(gè)唯一確定 cookie 的名稱。cookie 名稱是不區(qū)分大小寫的,所以 myCookie 和 MyCookie 被認(rèn)為是同一個(gè) cookie。然而,實(shí)踐中最好將 cookie 名稱看作是區(qū)分大小寫的,因?yàn)槟承┓?wù)器會(huì)這樣處理 cookie。cookie 的名稱必須是經(jīng)過 URL 編碼的。
  • :儲(chǔ)存在 cookie 中的字符串值。值必須被 URL 編碼。
  • :cookie 對(duì)于哪個(gè)域是有效的。所有向該域發(fā)送的請求中都會(huì)包含這個(gè) cookie 信息。這個(gè)值可以包含子域(subdomain,如 www.wrox.com),也可以不包含它(如 .wrox.com,則對(duì)于 wrox.com 的所有子域都有效)。如果沒有明確設(shè)定,那么這個(gè)域會(huì)被認(rèn)作來自設(shè)置 cookie 的那個(gè)域。
  • 路徑:對(duì)于指定域中的那個(gè)路徑,應(yīng)該向服務(wù)器發(fā)送 cookie。例如,你可以指定 cookie 只有從 http://www.wrox.com/books/ 中才能訪問,那么 http://www.wrox.com 的頁面就不會(huì)發(fā)送 cookie 信息,即使請求都是來自同一個(gè)域的。
  • 失效時(shí)間:表示 cookie 何時(shí)應(yīng)該被刪除的時(shí)間戳(也就是,何時(shí)應(yīng)該停止向服務(wù)器發(fā)送這個(gè) cookie)。默認(rèn)情況下,瀏覽器會(huì)話結(jié)束時(shí)即將所有 cookie 刪除;不過也可以自己設(shè)置刪除時(shí)間。這個(gè)值是個(gè) GMT 格式的日期(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定應(yīng)該刪除 cookie 的準(zhǔn)確時(shí)間。因此,cookie 可在瀏覽器關(guān)閉后依然保存在用戶的機(jī)器上。如果你設(shè)置的失效日期是個(gè)以前的時(shí)間,則 cookie 會(huì)被立刻刪除。
  • 安全標(biāo)志:指定后,cookie 只有在使用 SSL 連接的時(shí)候才發(fā)送到服務(wù)器。例如,cookie 信息只能發(fā)送給 https://www.wrox.com,而 http://www.wrox.com 的請求則不能發(fā)送 cookie。

??每一段信息都作為 Set-Cookie 頭的一部分,使用分號(hào)加空格分隔每一段,如下例所示。

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
Other-header: other-header-value

??該頭信息指定了一個(gè)叫做 name 的 cookie,它會(huì)在格林威治時(shí)間 2007 年 1 月 22 日 7:10:24 失效,同時(shí)對(duì)于 www.wrox.comwrox.com 的任何子域(如 p2p.wrox.com)都有效。

??secure 標(biāo)志是 cookie 中唯一一個(gè)非名值對(duì)兒的部分,直接包含一個(gè) secure 單詞。如下:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value

??這里,創(chuàng)建了一個(gè)對(duì)于所有 wrox.com 的子域和域名下(由 path 參數(shù)指定的)所有頁面都有效的 cookie。因?yàn)樵O(shè)置了 secure 標(biāo)志,這個(gè) cookie 只能通過 SSL 連接才能傳輸。

??尤其要注意,域、路徑、失效時(shí)間和 secure 標(biāo)志都是服務(wù)器給瀏覽器的指示,以指定何時(shí)應(yīng)該發(fā)送 cookie。這些參數(shù)并不會(huì)作為發(fā)送到服務(wù)器的 cookie 信息的一部分,只有名值對(duì)兒才會(huì)被發(fā)送。

3. JavaScript 中的 cookie

??在 JavaScript 中處理 cookie 有些復(fù)雜,因?yàn)槠浔娝苤孽磕_的接口,即 BOM 的 document. cookie 屬性。這個(gè)屬性的獨(dú)特之處在于它會(huì)因?yàn)槭褂盟姆绞讲煌憩F(xiàn)出不同的行為。
??當(dāng)用來獲取屬性值時(shí),document.cookie 返回當(dāng)前頁面可用的(根據(jù) cookie 的域、路徑、失效時(shí)間和安全設(shè)置)所有 cookie 的字符串,一系列由分號(hào)隔開的名值對(duì)兒,如下例所示。

name1=value1;name2=value2;name3=value3

??所有名字和值都是經(jīng)過 URL 編碼的,所以必須使用 decodeURIComponent()來解碼。
??當(dāng)用于設(shè)置值的時(shí)候,document.cookie 屬性可以設(shè)置為一個(gè)新的 cookie 字符串。這個(gè) cookie 字符串會(huì)被解釋并添加到現(xiàn)有的 cookie 集合中。設(shè)置 document.cookie 并不會(huì)覆蓋 cookie,除非設(shè)置的cookie 的名稱已經(jīng)存在。設(shè)置 cookie 的格式如下,和 Set-Cookie 頭中使用的格式一樣。

name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure

??這些參數(shù)中,只有 cookie 的名字和值是必需的。下面是一個(gè)簡單的例子。

document.cookie = "name=Nicholas";

??這段代碼創(chuàng)建了一個(gè)叫 name 的 cookie,值為 Nicholas。當(dāng)客戶端每次向服務(wù)器端發(fā)送請求的時(shí)候,都會(huì)發(fā)送這個(gè) cookie;當(dāng)瀏覽器關(guān)閉的時(shí)候,它就會(huì)被刪除。雖然這段代碼沒問題,但因?yàn)檫@里正好名稱和值都無需編碼,所以最好每次設(shè)置 cookie 時(shí)都像下面這個(gè)例子中一樣使用 encodeURIComponent()。

document.cookie = encodeURIComponent("name") + "=" +
 encodeURIComponent("Nicholas");

??要給被創(chuàng)建的 cookie 指定額外的信息,只要將參數(shù)追加到該字符串,和 Set-Cookie 頭中的格式一樣,如下所示。

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

??由于 JavaScript 中讀寫 cookie 不是非常直觀,常常需要寫一些函數(shù)來簡化 cookie 的功能。
??基本的 cookie 操作有三種:讀取、寫入和刪除。它們在 CookieUtil 對(duì)象中如下表示。

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.get() 方法根據(jù) cookie 的名字獲取相應(yīng)的值。它會(huì)在 document.cookie 字符串中查找 cookie 名加上等于號(hào)的位置。如果找到了,那么使用 indexOf() 查找該位置之后的第一個(gè)分號(hào)(表示了該 cookie 的結(jié)束位置)。如果沒有找到分號(hào),則表示該 cookie 是字符串中的最后一個(gè),則余下的字符串都是 cookie 的值。該值使用 decodeURIComponent() 進(jìn)行解碼并最后返回。如果沒有發(fā)現(xiàn) cookie,則返回 null。

??CookieUtil.set() 方法在頁面上設(shè)置一個(gè) cookie,接收如下幾個(gè)參數(shù):cookie 的名稱,cookie 的值,可選的用于指定 cookie 何時(shí)應(yīng)被刪除的 Date 對(duì)象,cookie 的可選的 URL 路徑,可選的域,以及可選的表示是否要添加 secure 標(biāo)志的布爾值。參數(shù)是按照它們的使用頻率排列的,只有頭兩個(gè)是必需的。
??在這個(gè)方法中,名稱和值都使用 encodeURIComponent() 進(jìn)行了URL編碼,并檢查其他選項(xiàng)。如果 expires 參數(shù)是 Date 對(duì)象,那么會(huì)使用 Date 對(duì)象的 toGMTString() 方法正確格式化 Date 對(duì)象,并添加到 expires 選項(xiàng)上。方法的其他部分就是構(gòu)造 cookie 字符串并將其設(shè)置到 document.cookie 中。
??沒有刪除已有 cookie 的直接方法。所以,需要使用相同的路徑、域和安全選項(xiàng)再次設(shè)置 cookie,并將失效時(shí)間設(shè)置為過去的時(shí)間。
??CookieUtil.unset() 方法可以處理這種事情。它接收 4 個(gè)參數(shù):要?jiǎng)h除的 cookie 的名稱、可選的路徑參數(shù)、可選的域參數(shù)和可選的安全參數(shù)。
??這些參數(shù)加上空字符串并設(shè)置失效時(shí)間為 1970 年 1 月 1 日(初始化為 0ms 的 Date 對(duì)象的值),傳給 CookieUtil.set()。這樣就能確保刪除 cookie。
??可以像下面這樣使用上述方法。

// 設(shè)置 cookie
CookieUtil.set("name", "Nicholas");
CookieUtil.set("book", "Professional JavaScript");

// 讀取 cookie 的值
console.log(CookieUtil.get("name")); // "Nicholas"
console.log(CookieUtil.get("book")); // "Professional JavaScript"

// 刪除 cookie
CookieUtil.unset("name");
CookieUtil.unset("book"); 

// 設(shè)置 cookie,包括它的路徑、域、失效日期
CookieUtil.set("name", "Nicholas", "/books/projs/", "www.wrox.com", new Date("January 1, 2010"));

// 刪除剛剛設(shè)置的 cookie
CookieUtil.unset("name", "/books/projs/", "www.wrox.com");

// 設(shè)置安全的 cookie
CookieUtil.set("name", "Nicholas", null, null, null, true);

??這些方法通過處理解析、構(gòu)造 cookie 字符串的任務(wù)令在客戶端利用 cookie 存儲(chǔ)數(shù)據(jù)更加簡單。

4. 子 cookie

??為了繞開瀏覽器的單域名下的 cookie 數(shù)限制,一些開發(fā)人員使用了一種稱為
子 cookie(subcookie)的概念。子 cookie 是存放在單個(gè) cookie 中的更小段的數(shù)據(jù)。也就是使用 cookie 值來存儲(chǔ)多個(gè)名稱值對(duì)兒。子 cookie 最常見的的格式如下所示。

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

??子 cookie 一般也以查詢字符串的格式進(jìn)行格式化。然后這些值可以使用單個(gè) cookie 進(jìn)行存儲(chǔ)和訪問,而非對(duì)每個(gè)名稱-值對(duì)兒使用不同的 cookie 存儲(chǔ)。最后網(wǎng)站或者 Web 應(yīng)用程序可以無需達(dá)到單域名 cookie 上限也可以存儲(chǔ)更加結(jié)構(gòu)化的數(shù)據(jù)。
??為了更好地操作子 cookie,必須建立一系列新方法。子 cookie 的解析和序列化會(huì)因子 cookie 的期望用途而略有不同并更加復(fù)雜些。例如,要獲得一個(gè)子 cookie,首先要遵循與獲得 cookie 一樣的基本步驟,但是在解碼 cookie 值之前,需要按如下方法找出子 cookie 的信息。

var SubCookieUtil = {

    get: function (name, subName) {
        var subCookies = this.getAll(name);
        if (subCookies) {
            return subCookies[subName];
        } else {
            return null;
        }
    },

    getAll: function (name) {
        var cookieName = encodeURIComponent(name) + '=',
            cookieStart = document.cookie.indexOf(cookieName),
            cookieValue = null,
            cookieEnd,
            subCookies,
            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;
    },

    // 省略了更多代碼
}

??獲取子 cookie 的方法有兩個(gè):get() 和 getAll()。
??其中 get() 獲取單個(gè)子 cookie 的值,getAll() 獲取所有子 cookie 并將它們放入一個(gè)對(duì)象中返回,對(duì)象的屬性為子 cookie 的名稱,對(duì)應(yīng)值為子 cookie 對(duì)應(yīng)的值。
??get() 方法接收兩個(gè)參數(shù):cookie 的名字和子 cookie 的名字。它其實(shí)就是調(diào)用 getAll() 獲取所有的子 cookie,然后只返回所需的那一個(gè)(如果 cookie 不存在則返回 null)。

??SubCookieUtil.getAll() 方法和 CookieUtil.get() 在解析 cookie 值的方式上非常相似。區(qū)別在于 cookie 的值并非立即解碼,而是先根據(jù)&字符將子 cookie 分割出來放在一個(gè)數(shù)組中,每一個(gè)子 cookie 再根據(jù)等于號(hào)分割,這樣在 parts 數(shù)組中的前一部分便是子 cookie 名,后一部分則是子 cookie 的值。
??這兩個(gè)項(xiàng)目都要使用 decodeURIComponent() 來解碼,然后放入 result 對(duì)象中,最后作為方法的返回值。如果 cookie 不存在,則返回 null。
??可以像下面這樣使用上述方法:

//假設(shè) document.cookie=data=name=Nicholas&book=Professional%20JavaScript

// 取得全部子 cookie
var data = SubCookieUtil.getAll("data");
console.log(data.name); // "Nicholas"
console.log(data.book); // "Professional JavaScript"

// 逐個(gè)獲取子 cookie
console.log(SubCookieUtil.get("data", "name")); // "Nicholas"
console.log(SubCookieUtil.get("data", "book")); // "Professional JavaScript"

??要設(shè)置子 cookie,也有兩種方法:set() 和 setAll()。以下代碼展示了它們的構(gòu)造。

var SubCookieUtil = {

    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);
    },

    setAll: function (name, subcookies, expires, path, domain, secure) {
        
        var cookieText = encodeURIComponent(name) + '=',
            subcookiesParts = new Array(),
            subName;

        for(subName in subcookies) {
            if (subName.length > 0 && subcookies.hasOwnProperty(subName)) {
                subcookiesParts.push(encodeURIComponent(subName) + '=' + encodeURIComponent(subcookies[subName]));
            }
        }

        if (subcookiesParts.length > 0) {
            cookieText += subcookiesParts.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;
    },

    // 省略了更多代碼
};

??這里的 set() 方法接收 7 個(gè)參數(shù):cookie 名稱、子 cookie 名稱、子 cookie 值、可選的 cookie 失效日期或時(shí)間的 Date 對(duì)象、可選的 cookie 路徑、可選的 cookie 域和可選的布爾 secure 標(biāo)志。
??所有的可選參數(shù)都是作用于 cookie 本身而非子 cookie。為了在同一個(gè) cookie 中存儲(chǔ)多個(gè)子 cookie,路徑、域和 secure 標(biāo)志必須一致;針對(duì)整個(gè) cookie 的失效日期則可以在任何一個(gè)單獨(dú)的子 cookie 寫入的時(shí)候同時(shí)設(shè)置。
??在這個(gè)方法中,第一步是獲取指定 cookie 名稱對(duì)應(yīng)的所有子 cookie。邏輯或操作符“||”用于當(dāng) getAll() 返回 null 時(shí)將 subcookies 設(shè)置為一個(gè)新對(duì)象。然后,在 subcookies 對(duì)象上設(shè)置好子 cookie 值并傳給 setAll()。
??而 setAll() 方法接收 6 個(gè)參數(shù):cookie 名稱、包含所有子 cookie 的對(duì)象以及和 set() 中一樣的 4 個(gè)可選參數(shù)。這個(gè)方法使用 for-in 循環(huán)遍歷第二個(gè)參數(shù)中的屬性。為了確保確實(shí)是要保存的數(shù)據(jù),使用了 hasOwnProperty() 方法,來確保只有實(shí)例屬性被序列化到子 cookie 中。由于可能會(huì)存在屬性名為空字符串的情況,所以在把屬性名加入結(jié)果對(duì)象之前還要檢查一下屬性名的長度。將每個(gè)子 cookie的名值對(duì)兒都存入 subcookieParts 數(shù)組中,以便稍后可以使用 join() 方法以 & 號(hào)組合起來。剩下的方法則和 CookieUtil.set() 一樣。
??可以按如下方式使用這些方法。

// 假設(shè) document.cookie=data=name=Nicholas&book=Professional%20JavaScript

// 設(shè)置兩個(gè) cookie
SubCookieUtil.set("data", "name", "Nicholas");
SubCookieUtil.set("data", "book", "Professional JavaScript");

// 設(shè)置全部子 cookie 和失效日期
SubCookieUtil.setAll("data", { name: "Nicholas", book: "Professional JavaScript" }, new Date("January 1, 2010"));

// 修改名字的值,并修改 cookie 的失效日期
SubCookieUtil.set("data", "name", "Michael", new Date("February 1, 2010"));

??子 cookie 的最后一組方法是用于刪除子 cookie 的。普通 cookie 可以通過將失效時(shí)間設(shè)置為過去的時(shí)間的方法來刪除,但是子 cookie 不能這樣做。為了刪除一個(gè)子 cookie,首先必須獲取包含在某個(gè) cookie 中的所有子 cookie,然后僅刪除需要?jiǎng)h除的那個(gè)子 cookie,然后再將余下的子 cookie 的值保存為 cookie 的值。請看以下代碼。

var SubCookieUtil = {

    // 這里省略了更多代碼
    
    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);
    }
};

??這里定義的兩個(gè)方法用于兩種不同的目的。unset() 方法用于刪除某個(gè) cookie 中的單個(gè)子 cookie 而不影響其他的;而 unsetAll() 方法則等同于 CookieUtil.unset(),用于刪除整個(gè) cookie。
??和 set() 及 setAll() 一樣,路徑、域和 secure 標(biāo)志必須和之前創(chuàng)建的 cookie 包含的內(nèi)容一致。這兩個(gè)方法可以像下面這樣使用。

// 僅刪除名為 name 的子 cookie
SubCookieUtil.unset("data", "name");

// 刪除整個(gè) cookie
SubCookieUtil.unsetAll("data");

??如果你擔(dān)心開發(fā)中可能會(huì)達(dá)到單域名的 cookie 上限,那么子 cookie 可是一個(gè)非常有吸引力的備選方案。不過,你需要更加密切關(guān)注 cookie 的長度,以防超過單個(gè) cookie 的長度限制。

5. 關(guān)于 cookie 的思考

??還有一類 cookie 被稱為“HTTP 專有 cookie”。HTTP 專有 cookie 可以從瀏覽器或者服務(wù)器設(shè)置,但是只能從服務(wù)器端讀取,因?yàn)?JavaScript 無法獲取 HTTP 專有 cookie 的值。
??由于所有的 cookie 都會(huì)由瀏覽器作為請求頭發(fā)送,所以在 cookie 中存儲(chǔ)大量信息會(huì)影響到特定域的請求性能。cookie 信息越大,完成對(duì)服務(wù)器請求的時(shí)間也就越長。盡管瀏覽器對(duì) cookie 進(jìn)行了大小限制,不過最好還是盡可能在 cookie 中少存儲(chǔ)信息,以避免影響性能。
??cookie 的性質(zhì)和它的局限使得其并不能作為存儲(chǔ)大量信息的理想手段,所以又出現(xiàn)了其他方法。

??一定不要在 cookie 中存儲(chǔ)重要和敏感的數(shù)據(jù)。cookie 數(shù)據(jù)并非存儲(chǔ)在一個(gè)安全環(huán)境中,其中包含的任何數(shù)據(jù)都可以被他人訪問。所以不要在 cookie 中存儲(chǔ)諸如信用卡號(hào)或者個(gè)人地址之類的數(shù)據(jù)。

3.2、IE 用戶數(shù)據(jù)

??在 IE5.0 中,微軟通過一個(gè)自定義行為引入了持久化用戶數(shù)據(jù)的概念。用戶數(shù)據(jù)允許每個(gè)文檔最多 128KB 數(shù)據(jù),每個(gè)域名最多 1MB 數(shù)據(jù)。
??要使用持久化用戶數(shù)據(jù),首先必須如下所示,使用 CSS 在某個(gè)元素上指定 userData 行為:

<div style="behavior:url(#default#userData)" id="dataStore"></div>

??一旦該元素使用了 userData 行為,那么就可以使用 setAttribute() 方法在上面保存數(shù)據(jù)了。
??為了將數(shù)據(jù)提交到瀏覽器緩存中,還必須調(diào)用 save() 方法并告訴它要保存到的數(shù)據(jù)空間的名字。數(shù)據(jù)空間名字可以完全任意,僅用于區(qū)分不同的數(shù)據(jù)集。請看以下例子。

var dataStore = document.getElementById("dataStore");
dataStore.setAttribute("name", "Nicholas");
dataStore.setAttribute("book", "Professional JavaScript");
dataStore.save("BookInfo");

??在這段代碼中,<div>元素上存入了兩部分信息。在用 setAttribute() 存儲(chǔ)了數(shù)據(jù)之后,調(diào)用了 save() 方法,指定了數(shù)據(jù)空間的名稱為 BookInfo。下一次頁面載入之后,可以使用 load() 方法指定同樣的數(shù)據(jù)空間名稱來獲取數(shù)據(jù),如下所示。

dataStore.load("BookInfo"); 

console.log(dataStore.getAttribute("name")); // "Nicholas"
console.log(dataStore.getAttribute("book")); // "Professional JavaScript" 

??對(duì) load() 的調(diào)用獲取了 BookInfo 數(shù)據(jù)空間中的所有信息,并且使數(shù)據(jù)可以通過元素訪問;只有到載入確切完成之后數(shù)據(jù)方能使用。如果 getAttribute() 調(diào)用了不存在的名稱或者是尚未載入的名程,則返回 null。
??你可以通過 removeAttribute() 方法明確指定要?jiǎng)h除某元素?cái)?shù)據(jù),只要指定屬性名稱。刪除之后,必須像下面這樣再次調(diào)用 save() 來提交更改。

dataStore.removeAttribute("name");
dataStore.removeAttribute("book");
dataStore.save("BookInfo");

??這段代碼刪除了兩個(gè)數(shù)據(jù)屬性,然后將更改保存到緩存中。
??對(duì) IE 用戶數(shù)據(jù)的訪問限制和對(duì) cookie 的限制類似。要訪問某個(gè)數(shù)據(jù)空間,腳本運(yùn)行的頁面必須來自同一個(gè)域名,在同一個(gè)路徑下,并使用與進(jìn)行存儲(chǔ)的腳本同樣的協(xié)議。
??和 cookie 不同的是,你無法將用戶數(shù)據(jù)訪問限制擴(kuò)展到更多的客戶。還有一點(diǎn)不同,用戶數(shù)據(jù)默認(rèn)是可以跨越會(huì)話持久存在的,同時(shí)也不會(huì)過期;數(shù)據(jù)需要通過 removeAttribute() 方法專門進(jìn)行刪除以釋放空間。

??和 cookie 一樣,IE 用戶數(shù)據(jù)并非安全的,所以不能存放敏感信息。

3.3、Web 存儲(chǔ)機(jī)制

??Web Storage 最早是在 Web 超文本應(yīng)用技術(shù)工作組(WHAT-WG)的 Web 應(yīng)用 1.0 規(guī)范中描述的。
??這個(gè)規(guī)范的最初的工作最終成為了 HTML5 的一部分。Web Storage 的目的是克服由 cookie 帶來的一些限制,當(dāng)數(shù)據(jù)需要被嚴(yán)格控制在客戶端上時(shí),無須持續(xù)地將數(shù)據(jù)發(fā)回服務(wù)器。Web Storage 的兩個(gè)主要目標(biāo)是:

  • 提供一種在 cookie 之外存儲(chǔ)會(huì)話數(shù)據(jù)的途徑;
  • 提供一種存儲(chǔ)大量可以跨會(huì)話存在的數(shù)據(jù)的機(jī)制。

??最初的 Web Storage 規(guī)范包含了兩種對(duì)象的定義:sessionStorage 和 globalStorage。這兩個(gè)對(duì)象在支持的瀏覽器中都是以 windows 對(duì)象屬性的形式存在的,支持這兩個(gè)屬性的瀏覽器包括 IE8+、Firefox 3.5+、Chrome 4+和 Opera 10.5+。 Firefox 2 和 3 基于早期規(guī)范的內(nèi)容部分實(shí)現(xiàn)了 Web Storage,當(dāng)時(shí)只實(shí)現(xiàn)了 globalStorage,沒有實(shí)現(xiàn) localStorage。

1. Storage 類型

??Storage 類型提供最大的存儲(chǔ)空間(因?yàn)g覽器而異)來存儲(chǔ)名值對(duì)兒。Storage 的實(shí)例與其他對(duì)象類似,有如下方法。

  • clear(): 刪除所有值;Firefox 中沒有實(shí)現(xiàn) 。
  • getItem(name):根據(jù)指定的名字 name 獲取對(duì)應(yīng)的值。
  • key(index):獲得 index 位置處的值的名字。
  • removeItem(name):刪除由 name 指定的名值對(duì)兒。
  • setItem(name, value):為指定的 name 設(shè)置一個(gè)對(duì)應(yīng)的值。

??其中,getItem()、removeItem() 和 setItem()方法可以直接調(diào)用,也可通過 Storage 對(duì)象間接調(diào)用。因?yàn)槊總€(gè)項(xiàng)目都是作為屬性存儲(chǔ)在該對(duì)象上的,所以可以通過點(diǎn)語法或者方括號(hào)語法訪問屬性來讀取值,設(shè)置也一樣,或者通過 delete 操作符進(jìn)行刪除。
??不過,我們還建議讀者使用方法而不是屬性來訪問數(shù)據(jù),以免某個(gè)鍵會(huì)意外重寫該對(duì)象上已經(jīng)存在的成員。
??還可以使用 length 屬性來判斷有多少名值對(duì)兒存放在 Storage 對(duì)象中。但無法判斷對(duì)象中所有數(shù)據(jù)的大小,不過 IE8 提供了一個(gè) remainingSpace 屬性,用于獲取還可以使用的存儲(chǔ)空間的字節(jié)數(shù)。

??Storage 類型只能存儲(chǔ)字符串。非字符串的數(shù)據(jù)在存儲(chǔ)之前會(huì)被轉(zhuǎn)換成字符串。

2. sessionStorage 對(duì)象

??sessionStorage 對(duì)象存儲(chǔ)特定于某個(gè)會(huì)話的數(shù)據(jù),也就是該數(shù)據(jù)只保持到瀏覽器關(guān)閉。這個(gè)對(duì)象就像會(huì)話 cookie,也會(huì)在瀏覽器關(guān)閉后消失。
??存儲(chǔ)在 sessionStorage 中的數(shù)據(jù)可以跨越頁面刷新而存在,同時(shí)如果瀏覽器支持,瀏覽器崩潰并重啟之后依然可用(Firefox 和 WebKit 都支持,IE 則不行)。
??因?yàn)?seesionStorage 對(duì)象綁定于某個(gè)服務(wù)器會(huì)話,所以當(dāng)文件在本地運(yùn)行的時(shí)候是不可用的。
??存儲(chǔ)在 sessionStorage 中的數(shù)據(jù)只能由最初給對(duì)象存儲(chǔ)數(shù)據(jù)的頁面訪問到,所以對(duì)多頁面應(yīng)用有限制。

??由于 sessionStorage 對(duì)象其實(shí)是 Storage 的一個(gè)實(shí)例,所以可以使用 setItem() 或者直接設(shè)置新的屬性來存儲(chǔ)數(shù)據(jù)。下面是這兩種方法的例子。

// 使用方法存儲(chǔ)數(shù)據(jù)
sessionStorage.setItem("name", "Nicholas");

// 使用屬性存儲(chǔ)數(shù)據(jù)
sessionStorage.book = "Professional JavaScript";

??不同瀏覽器寫入數(shù)據(jù)方面略有不同。Firefox 和 WebKit 實(shí)現(xiàn)了同步寫入,所以添加到存儲(chǔ)空間中的數(shù)據(jù)是立刻被提交的。而 IE 的實(shí)現(xiàn)則是異步寫入數(shù)據(jù),所以在設(shè)置數(shù)據(jù)和將數(shù)據(jù)實(shí)際寫入磁盤之間可能有一些延遲。對(duì)于少量數(shù)據(jù)而言,這個(gè)差異是可以忽略的。對(duì)于大量數(shù)據(jù),你會(huì)發(fā)現(xiàn) IE 要比其他瀏覽器更快地恢復(fù)執(zhí)行,因?yàn)樗鼤?huì)跳過實(shí)際的磁盤寫入過程。
??在 IE8 中可以強(qiáng)制把數(shù)據(jù)寫入磁盤:在設(shè)置新數(shù)據(jù)之前使用 begin() 方法,并且在所有設(shè)置完成之后調(diào)用 commit() 方法。看以下例子。

// 只適用于 IE8
sessionStorage.begin();
sessionStorage.name = "Nicholas";
sessionStorage.book = "Professional JavaScript";
sessionStorage.commit();

??這段代碼確保了 name 和 book 的值在調(diào)用 commit() 之后立刻被寫入磁盤。調(diào)用 begin() 是為了確保在這段代碼執(zhí)行的時(shí)候不會(huì)發(fā)生其他磁盤寫入操作。對(duì)于少量數(shù)據(jù)而言,這個(gè)過程不是必需的;不過,對(duì)于大量數(shù)據(jù)(如文檔之類的)可能就要考慮這種事務(wù)形式的方法了。

??sessionStorage 中有數(shù)據(jù)時(shí),可以使用 getItem() 或者通過直接訪問屬性名來獲取數(shù)據(jù)。兩種方法的例子如下。

// 使用方法讀取數(shù)據(jù)
var name = sessionStorage.getItem("name");

// 使用屬性讀取數(shù)據(jù)
var book = sessionStorage.book;

??還可以通過結(jié)合 length 屬性和 key() 方法來迭代 sessionStorage 中的值,如下所示。

for (var i=0, len = sessionStorage.length; i < len; i++){
    var key = sessionStorage.key(i);
    var value = sessionStorage.getItem(key);
    console.log(key + "=" + value);
}

??它是這樣遍歷 sessionStorage 中的名值對(duì)兒的:首先通過 key() 方法獲取指定位置上的名字,然后再通過 getItem() 找出對(duì)應(yīng)該名字的值。

??還可以使用 for-in 循環(huán)來迭代 sessionStorage 中的值:

for (var key in sessionStorage){
    var value = sessionStorage.getItem(key);
    console.log(key + "=" + value);
}

??每次經(jīng)過循環(huán)的時(shí)候,key 被設(shè)置為 sessionStorage 中下一個(gè)名字,此時(shí)不會(huì)返回任何內(nèi)置方法或 length 屬性。

??要從 sessionStorage 中刪除數(shù)據(jù),可以使用 delete 操作符刪除對(duì)象屬性,也可調(diào)用 removeItem() 方法。以下是這些方法的例子。

// 使用 delete 刪除一個(gè)值——在 WebKit 中無效
delete sessionStorage.name;

// 使用方法刪除一個(gè)值
sessionStorage.removeItem("book");

??在撰寫本書時(shí),delete 操作符在 WebKit 中無法刪除數(shù)據(jù),removeItem() 則可以在各種支持的瀏覽器中正確運(yùn)行。

??sessionStorage 對(duì)象應(yīng)該主要用于僅針對(duì)會(huì)話的小段數(shù)據(jù)的存儲(chǔ)。如果需要跨越會(huì)話存儲(chǔ)數(shù)據(jù),那么 globalStorage 或者 localStorage 更為合適。

3. globalStorage 對(duì)象

??Firefox 2 中實(shí)現(xiàn)了 globalStorage 對(duì)象。作為最初的 Web Storage 規(guī)范的一部分,這個(gè)對(duì)象的目的是跨越會(huì)話存儲(chǔ)數(shù)據(jù),但有特定的訪問限制。
??要使用 globalStorage,首先要指定哪些域可以訪問該數(shù)據(jù)。可以通過方括號(hào)標(biāo)記使用屬性來實(shí)現(xiàn),如以下例子所示。

// 保存數(shù)據(jù)
globalStorage["wrox.com"].name = "Nicholas";

// 獲取數(shù)據(jù)
var name = globalStorage["wrox.com"].name;

??在這里,訪問的是針對(duì)域名 wrox.com 的存儲(chǔ)空間。

??globalStorage 對(duì)象不是 Storage 的實(shí)例,而具體的 globalStorage["wrox.com"] 才是。這個(gè)存儲(chǔ)空間對(duì)于 wrox.com 及其所有子域都是可以訪問的??梢韵裣旅孢@樣指定子域名。

// 保存數(shù)據(jù)
globalStorage["www.wrox.com"].name = "Nicholas";

// 獲取數(shù)據(jù)
var name = globalStorage["www.wrox.com"].name;

??這里所指定的存儲(chǔ)空間只能由來自 www.wrox.com 的頁面訪問,其他子域名都不行。
??某些瀏覽器允許更加寬泛的訪問限制,比如只根據(jù)頂級(jí)域名進(jìn)行限制或者允許全局訪問,如下面例子所示。

// 存儲(chǔ)數(shù)據(jù),任何人都可以訪問——不要這樣做!
globalStorage[""].name = "Nicholas";

// 存儲(chǔ)數(shù)據(jù),可以讓任何以.net 結(jié)尾的域名訪問——不要這樣做!
globalStorage["net"].name = "Nicholas";

??雖然這些也支持,但是還是要避免使用這種可寬泛訪問的數(shù)據(jù)存儲(chǔ),以防止出現(xiàn)潛在的安全問題。
??考慮到安全問題,這些功能在未來可能會(huì)被刪除或者是被更嚴(yán)格地限制,所以不應(yīng)依賴于這類功能。
??當(dāng)使用 globalStorage 的時(shí)候一定要指定一個(gè)域名。

??對(duì) globalStorage 空間的訪問,是依據(jù)發(fā)起請求的頁面的域名、協(xié)議和端口來限制的。例如,如果使用 HTTPS 協(xié)議在 wrox.com 中存儲(chǔ)了數(shù)據(jù),那么通過 HTTP 訪問的 wrox.com 的頁面就不能訪問該數(shù)據(jù)。同樣,通過 80 端口訪問的頁面則無法與同一個(gè)域同樣協(xié)議但通過 8080 端口訪問的頁面共享數(shù)據(jù)。這類似于 Ajax 請求的同源策略。

??globalStorage 的每個(gè)屬性都是 Storage 的實(shí)例。因此,可以像如下代碼中這樣使用。

globalStorage["www.wrox.com"].name = "Nicholas";
globalStorage["www.wrox.com"].book = "Professional JavaScript";

globalStorage["www.wrox.com"].removeItem("name");

var book = globalStorage["www.wrox.com"].getItem("book");

??如果你事先不能確定域名,那么使用 location.host 作為屬性名比較安全。例如:

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

??如果不使用 removeItem() 或者 delete 刪除,或者用戶未清除瀏覽器緩存,存儲(chǔ)在 globalStorage 屬性中的數(shù)據(jù)會(huì)一直保留在磁盤上。這讓 globalStorage 非常適合在客戶端存儲(chǔ)文檔或者長期保存用戶偏好設(shè)置。

4. localStorage 對(duì)象

??localStorage 對(duì)象在修訂過的 HTML 5 規(guī)范中作為持久保存客戶端數(shù)據(jù)的方案取代了 globalStorage。
??與 globalStorage 不同,不能給 localStorage 指定任何訪問規(guī)則;規(guī)則事先就設(shè)定好了。要訪問同一個(gè) localStorage 對(duì)象,頁面必須來自同一個(gè)域名(子域名無效),使用同一種協(xié)議,在同一個(gè)端口上。這相當(dāng)于globalStorage[location.host]。
??由于 localStorage 是 Storage 的實(shí)例,所以可以像使用 sessionStorage 一樣來使用它。下面是一些例子。

// 使用方法存儲(chǔ)數(shù)據(jù)
localStorage.setItem("name", "Nicholas");

// 使用屬性存儲(chǔ)數(shù)據(jù)
localStorage.book = "Professional JavaScript";

// 使用方法讀取數(shù)據(jù)
var name = localStorage.getItem("name");

// 使用屬性讀取數(shù)據(jù)
var book = localStorage.book;

??存儲(chǔ)在 localStorage 中的數(shù)據(jù)和存儲(chǔ)在 globalStorage 中的數(shù)據(jù)一樣,都遵循相同的規(guī)則:數(shù)據(jù)保留到通過 JavaScript 刪除或者是用戶清除瀏覽器緩存。

??為了兼容只支持 globalStorage 的瀏覽器,可以使用以下函數(shù)。

function getLocalStorage(){
    if (typeof localStorage == "object"){
        return localStorage;
    } else if (typeof globalStorage == "object"){
        return globalStorage[location.host];
    } else {
        throw new Error("Local storage not available.");
    }
}

??然后,像下面這樣調(diào)用一次這個(gè)函數(shù),就可以正常地讀寫數(shù)據(jù)了。

var storage = getLocalStorage();  

??在確定了使用哪個(gè) Storage 對(duì)象之后,就能在所有支持 Web Storage 的瀏覽器中使用相同的存取規(guī)則操作數(shù)據(jù)了。

5. storage 事件

??對(duì) Storage 對(duì)象進(jìn)行任何修改,都會(huì)在文檔上觸發(fā) storage 事件。當(dāng)通過屬性或 setItem() 方法保存數(shù)據(jù),使用 delete 操作符或 removeItem() 刪除數(shù)據(jù),或者調(diào)用 clear() 方法時(shí),都會(huì)發(fā)生該事件。這個(gè)事件的 event 對(duì)象有以下屬性。

  • domain:發(fā)生變化的存儲(chǔ)空間的域名。
  • key:設(shè)置或者刪除的鍵名。
  • newValue:如果是設(shè)置值,則是新值;如果是刪除鍵,則是 null。
  • oldValue:鍵被更改之前的值。

??在這四個(gè)屬性中,IE8 和 Firefox 只實(shí)現(xiàn)了 domain 屬性。在撰寫本書的時(shí)候,WebKit 尚不支持 storage 事件:以下代碼展示了如何偵聽 storage 事件:

EventUtil.addHandler(document, "storage", function(event){
    console.log("Storage changed for " + event.domain);
});

??無論對(duì) sessionStorage、globalStorage 還是 localStorage 進(jìn)行操作,都會(huì)觸發(fā) storage 事件,但不作區(qū)分。

6. 限制

??與其他客戶端數(shù)據(jù)存儲(chǔ)方案類似,Web Storage 同樣也有限制。這些限制因?yàn)g覽器而異。
??一般來說,對(duì)存儲(chǔ)空間大小的限制都是以每個(gè)來源(協(xié)議、域和端口)為單位的。換句話說,每個(gè)來源都有固定大小的空間用于保存自己的數(shù)據(jù)??紤]到這個(gè)限制,就要注意分析和控制每個(gè)來源中有多少頁面需要保存數(shù)據(jù)。

??對(duì)于 localStorage 而言,大多數(shù)桌面瀏覽器會(huì)設(shè)置每個(gè)來源 5MB 的限制。Chrome 和 Safari 對(duì)每個(gè)來源的限制是 2.5MB。而 iOS 版 Safari 和 Android 版 WebKit 的限制也是 2.5MB。

??對(duì) sessionStorage 的限制也是因?yàn)g覽器而異。有的瀏覽器對(duì) sessionStorage 的大小沒有限制,但 Chrome、Safari、iOS 版 Safari 和 Android 版 WebKit 都有限制,也都是 2.5MB。IE8+和 Opera 對(duì) sessionStorage 的限制是 5MB。

??有關(guān) Web Storage 的限制,請參考 http://dev-test.nemikor.com/web-storage/support-test/

3.4、IndexedDB

??Indexed Database API,或者簡稱為 IndexedDB,是在瀏覽器中保存結(jié)構(gòu)化數(shù)據(jù)的一種數(shù)據(jù)庫。
??IndexedDB 是為了替代目前已被廢棄的 Web SQL Database API 而出現(xiàn)的。
??IndexedDB 的思想是創(chuàng)建一套 API,方便保存和讀取 JavaScript 對(duì)象,同時(shí)還支持查詢及搜索。
??IndexedDB 設(shè)計(jì)的操作完全是異步進(jìn)行的。因此,大多數(shù)操作會(huì)以請求方式進(jìn)行,但這些操作會(huì)在后期執(zhí)行,然后如果成功則返回結(jié)果,如果失敗則返回錯(cuò)誤。
??差不多每一次 IndexedDB 操作,都需要你注冊 onerror 或 onsuccess 事件處理程序,以確保適當(dāng)?shù)靥幚斫Y(jié)果。
??在得到完整支持的情況下,IndexedDB 將是一個(gè)作為 API 宿主的全局對(duì)象。由于 API 仍然可能有變化,瀏覽器也都使用提供商前綴,因此這個(gè)對(duì)象在 IE10 中叫 msIndexedDB,在 Firefox 4 中叫 mozIndexedDB,在 Chrome 中叫 webkitIndexedDB。為了清楚起見,本節(jié)示例中將使用 IndexedDB,而實(shí)際上每個(gè)示例前面都應(yīng)該加上下面這行代碼:

var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB ||
window.webkitIndexedDB;
1. 數(shù)據(jù)庫

??IndexedDB 就是一個(gè)數(shù)據(jù)庫,與 MySQL 或 Web SQL Database 等這些你以前可能用過的數(shù)據(jù)庫類似。
??IndexedDB 最大的特色是使用對(duì)象保存數(shù)據(jù),而不是使用表來保存數(shù)據(jù)。一個(gè) IndexedDB 數(shù)據(jù)庫,就是一組位于相同命名空間下的對(duì)象的集合。
??使用 IndexedDB 的第一步是打開它,即把要打開的數(shù)據(jù)庫名傳給 indexDB.open()。如果傳入的數(shù)據(jù)庫已經(jīng)存在,就會(huì)發(fā)送一個(gè)打開它的請求;如果傳入的數(shù)據(jù)庫還不存在,就會(huì)發(fā)送一個(gè)創(chuàng)建并打開它的請求。
??總之,調(diào)用 indexDB.open() 會(huì)返回一個(gè)IDBRequest 對(duì)象,在這個(gè)對(duì)象上可以添加 onerror 和 onsuccess 事件處理程序。先來看一個(gè)例子。

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;
};

??在這兩個(gè)事件處理程序中,event.target 都指向 request 對(duì)象,因此它們可以互換使用。
??如果響應(yīng)的是 onsuccess 事件處理程序,那么 event.target.result 中將有一個(gè)數(shù)據(jù)庫實(shí)例對(duì)象(IDBDatabase),這個(gè)對(duì)象會(huì)保存在 database 變量中。
??如果發(fā)生了錯(cuò)誤,那 event.target.errorCode 中將保存一個(gè)錯(cuò)誤碼,表示問題的性質(zhì)。以下就是可能的錯(cuò)誤碼(這個(gè)錯(cuò)誤碼適合所有操作)。

  • IDBDatabaseException.UNKNOWN_ERR(1):意外錯(cuò)誤,無法歸類。
  • IDBDatabaseException.NON_TRANSIENT_ERR(2):操作不合法。
  • IDBDatabaseException.NOT_FOUND_ERR(3):未發(fā)現(xiàn)要操作的數(shù)據(jù)庫。
  • IDBDatabaseException.CONSTRAINT_ERR(4):違反了數(shù)據(jù)庫約束。
    -- IDBDatabaseException.DATA_ERR(5):提供給事務(wù)的數(shù)據(jù)不能滿足要求。
  • IDBDatabaseException.NOT_ALLOWED_ERR(6):操作不合法。
  • IDBDatabaseException.TRANSACTION_INACTIVE_ERR(7):試圖重用已完成的事務(wù)。
  • IDBDatabaseException.ABORT_ERR(8):請求中斷,未成功。
  • IDBDatabaseException.READ_ONLY_ERR(9):試圖在只讀模式下寫入或修改數(shù)據(jù)。
  • IDBDatabaseException.TIMEOUT_ERR(10):在有效時(shí)間內(nèi)未完成操作。
  • IDBDatabaseException.QUOTA_ERR(11):磁盤空間不足。

??默認(rèn)情況下,IndexedDB 數(shù)據(jù)庫是沒有版本號(hào)的,最好一開始就為數(shù)據(jù)庫指定一個(gè)版本號(hào)。為此,可以調(diào)用 setVersion() 方法,傳入以字符串形式表示的版本號(hào)。同樣,調(diào)用這個(gè)方法也會(huì)返回一個(gè)請求對(duì)象,需要你再指定事件處理程序。

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);
}

??這個(gè)例子嘗試把數(shù)據(jù)庫的版本號(hào)設(shè)置為 1.0。第一行先檢測 version 屬性,看是否已經(jīng)為數(shù)據(jù)庫設(shè)置了相應(yīng)的版本號(hào)。如果沒有,就調(diào)用 setVersion() 創(chuàng)建修改版本的請求。如果請求成功,顯示一條消息,表示版本修改成功。如果數(shù)據(jù)庫的版本號(hào)已經(jīng)被設(shè)置為 1.0,則顯示一條消息,說明數(shù)據(jù)庫已經(jīng)初始化過了。
??總之,通過這種模式,就能知道你想使用的數(shù)據(jù)庫是否已經(jīng)設(shè)置了適當(dāng)?shù)膶?duì)象存儲(chǔ)空間。在整個(gè) Web 應(yīng)用中,隨著對(duì)數(shù)據(jù)庫結(jié)構(gòu)的更新和修改,可能會(huì)產(chǎn)生很多個(gè)不同版本的數(shù)據(jù)庫。

2. 對(duì)象存儲(chǔ)空間

??在建立了與數(shù)據(jù)庫的連接之后,下一步就是使用對(duì)象存儲(chǔ)空間。
??有關(guān)系數(shù)據(jù)庫經(jīng)驗(yàn)的讀者,可以把這里的對(duì)象存儲(chǔ)空間(object storge)想象成表,而把其中保存的對(duì)象想象成表中的記錄。

??如果數(shù)據(jù)庫的版本與你傳入的版本不匹配,那可能就需要?jiǎng)?chuàng)建一個(gè)新的對(duì)象存儲(chǔ)空間。在創(chuàng)建對(duì)象存儲(chǔ)空間之前,必須要想清楚你想要保存什么數(shù)據(jù)類型。
??假設(shè)你要保存的用戶記錄由用戶名、密碼等組成,那么保存一條記錄的對(duì)象應(yīng)該類似如下所示:

var user = {
    username: "007",
    firstName: "James",
    lastName: "Bond",
    password: "foo"
};

??有了這個(gè)對(duì)象,很容易想到 username 屬性可以作為這個(gè)對(duì)象存儲(chǔ)空間的鍵。這個(gè) username 必須全局唯一,而且大多數(shù)時(shí)候都要通過這個(gè)鍵來訪問數(shù)據(jù)。這一點(diǎn)非常重要,因?yàn)樵趧?chuàng)建對(duì)象存儲(chǔ)空間時(shí),必須指定這么一個(gè)鍵。以下是就是為保存上述用戶記錄而創(chuàng)建對(duì)象存儲(chǔ)空間的示例。

var store = db.createObjectStore("users", { keyPath: "username" });

??其中第二個(gè)參數(shù)中的 keyPath 屬性,就是空間中將要保存的對(duì)象的一個(gè)屬性,而這個(gè)屬性將作為存儲(chǔ)空間的鍵來使用。

??好,現(xiàn)在有了一個(gè)對(duì)存儲(chǔ)空間的引用。接下來可以使用 add() 或 put() 方法來向其中添加數(shù)據(jù)。這兩個(gè)方法都接收一個(gè)參數(shù),即要保存的對(duì)象,然后這個(gè)對(duì)象就會(huì)被保存到存儲(chǔ)空間中。
??這兩個(gè)方法的區(qū)別在空間中已經(jīng)包含鍵值相同的對(duì)象時(shí)會(huì)體現(xiàn)出來。在這種情況下,add() 會(huì)返回錯(cuò)誤,而 put() 則會(huì)重寫原有對(duì)象。
??簡單地說,可以把 add() 想象成插入新值,把 put() 想象成更新原有的值。在初始化對(duì)象存儲(chǔ)空間時(shí),可以使用類似下面這樣的代碼。

// users 中保存著一批用戶對(duì)象
var i=0,
    len = users.length;
while(i < len){
    store.add(users[i++]);
}

??每次調(diào)用 add() 或 put() 都會(huì)創(chuàng)建一個(gè)新的針對(duì)這個(gè)對(duì)象存儲(chǔ)空間的更新請求。如果想驗(yàn)證請求是否成功完成,可以把返回的請求對(duì)象保存在一個(gè)變量中,然后再指定 onerror 或 onsuccess 事件處理程序。

// users 中保存著一批用戶對(duì)象
var i=0,
    request,
    requests = [],
    len = users.length;

while(i < len){
    request = store.add(users[i++]);
    request.onerror = function(){
        // 處理錯(cuò)誤
    };
    request.onsuccess = function(){
        // 處理成功
    };
    requests.push(request);
}

??創(chuàng)建了對(duì)象存儲(chǔ)空間并向其中添加了數(shù)據(jù)之后,就該查詢數(shù)據(jù)了。

3. 事務(wù)

??跨過創(chuàng)建對(duì)象存儲(chǔ)空間這一步之后,接下來的所有操作都是通過事務(wù)來完成的。
??在數(shù)據(jù)庫對(duì)象上調(diào)用 transaction() 方法可以創(chuàng)建事務(wù)。任何時(shí)候,只要想讀取或修改數(shù)據(jù),都要通過事務(wù)來組織所有操作。在最簡單的情況下,可以像下面這樣創(chuàng)建事務(wù)。(以下示例代碼中的 db 即前面示例代碼中的 database,正文中提到的“數(shù)據(jù)庫對(duì)象”也是指它。)

var transaction = db.transaction();

??如果沒有參數(shù),就只能通過事務(wù)來讀取數(shù)據(jù)庫中保存的對(duì)象。最常見的方式是傳入要訪問的一或多個(gè)對(duì)象存儲(chǔ)空間。

var transaction = db.transaction("users");

??這樣就能保證只加載 users 存儲(chǔ)空間中的數(shù)據(jù),以便通過事務(wù)進(jìn)行訪問。如果要訪問多個(gè)對(duì)象存儲(chǔ)空間,也可以在第一個(gè)參數(shù)的位置上傳入字符串?dāng)?shù)組。

var transaction = db.transaction(["users", "anotherStore"]);

??如前所述,這些事務(wù)都是以只讀方式訪問數(shù)據(jù)。要修改訪問方式,必須在創(chuàng)建事務(wù)時(shí)傳入第二個(gè)參數(shù),這個(gè)參數(shù)表示訪問模式,用 IDBTransaction 接口定義的如下常量表示:

  • READ_ONLY(0)表示只讀,
  • READ_WRITE(1)表示讀寫,
  • VERSION_CHANGE(2)表示改變。

??IE10+和 Firefox 4+實(shí)現(xiàn)的是 IDBTransaction,但在 Chrome 中則叫 webkitIDBTransaction,所以使用下面的代碼可以統(tǒng)一接口:

var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

??有了這行代碼,就可以更方便地為 transaction() 指定第二個(gè)參數(shù)了。

var transaction = db.transaction("users", IDBTransaction.READ_WRITE);

??這個(gè)事務(wù)能夠讀寫 users 存儲(chǔ)空間。
??取得了事務(wù)的索引后,使用 objectStore() 方法并傳入存儲(chǔ)空間的名稱,就可以訪問特定的存儲(chǔ)空間。然后,可以像以前一樣使用 add() 和 put() 方法,使用 get() 可以取得值,使用 delete() 可以刪除對(duì)象,而使用 clear() 則可以刪除所有對(duì)象。
??get() 和 delete() 方法都接收一個(gè)對(duì)象鍵作為參數(shù),而所有這 5 個(gè)方法都會(huì)返回一個(gè)新的請求對(duì)象。例如:

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"
};

??因?yàn)橐粋€(gè)事務(wù)可以完成任何多個(gè)請求,所以事務(wù)對(duì)象本身也有事件處理程序:onerror 和 oncomplete。這兩個(gè)事件可以提供事務(wù)級(jí)的狀態(tài)信息。

transaction.onerror = function(event){
    // 整個(gè)事務(wù)都被取消了
};
transaction.oncomplete = function(event){
    // 整個(gè)事務(wù)都成功完成了
};

??注意,通過 oncomplete 事件的事件對(duì)象(event)訪問不到 get() 請求返回的任何數(shù)據(jù)。必須在相應(yīng)請求的 onsuccess 事件處理程序中才能訪問到數(shù)據(jù)。

4. 使用游標(biāo)查詢

??使用事務(wù)可以直接通過已知的鍵檢索單個(gè)對(duì)象。而在需要檢索多個(gè)對(duì)象的情況下,則需要在事務(wù)內(nèi)部創(chuàng)建游標(biāo)。
??游標(biāo)就是一指向結(jié)果集的指針。與傳統(tǒng)數(shù)據(jù)庫查詢不同,游標(biāo)并不提前收集結(jié)果。游標(biāo)指針會(huì)先指向結(jié)果中的第一項(xiàng),在接到查找下一項(xiàng)的指令時(shí),才會(huì)指向下一項(xiàng)。
??在對(duì)象存儲(chǔ)空間上調(diào)用 openCursor() 方法可以創(chuàng)建游標(biāo)。與 IndexedDB 中的其他操作一樣,openCursor() 方法返回的是一個(gè)請求對(duì)象,因此必須為該對(duì)象指定 onsuccess 和 onerror 事件處理程序。例如:

var store = db.transaction("users").objectStore("users"),
    request = store.openCursor();

request.onsuccess = function(event){
    // 處理成功
};

request.onerror = function(event){
    // 處理失敗
};

??在 onsuccess 事件處理程序執(zhí)行時(shí),可以通過 event.target.result 取得存儲(chǔ)空間中的下一個(gè)對(duì)象。在結(jié)果集中有下一項(xiàng)時(shí),這個(gè)屬性中保存一個(gè) IDBCursor 的實(shí)例,在沒有下一項(xiàng)時(shí),這個(gè)屬性的值為 null。
??IDBCursor 的實(shí)例有以下幾個(gè)屬性。

  • direction:數(shù)值,表示游標(biāo)移動(dòng)的方向。默認(rèn)值為
    • IDBCursor.NEXT(0),表示下一項(xiàng)。
    • IDBCursor.NEXT_NO_DUPLICATE(1)表示下一個(gè)不重復(fù)的項(xiàng),
    • DBCursor.PREV(2)表示前一項(xiàng),
    • IDBCursor.PREV_NO_DUPLICATE 表示前一個(gè)不重復(fù)的項(xiàng)。
  • key:對(duì)象的鍵。
  • value:實(shí)際的對(duì)象。
  • primaryKey:游標(biāo)使用的鍵。可能是對(duì)象鍵,也可能是索引鍵。

??要檢索某一個(gè)結(jié)果的信息,可以像下面這樣:

request.onsuccess = function(event){
    var cursor = event.target.result;
    if (cursor){ // 必須要檢查
        console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
    }
};

??請記住,這個(gè)例子中的 cursor.value 是一個(gè)對(duì)象,這也是為什么在顯示它之前先將它轉(zhuǎn)換成 JSON 字符串的原因。

??使用游標(biāo)可以更新個(gè)別的記錄。調(diào)用 update() 方法可以用指定的對(duì)象更新當(dāng)前游標(biāo)的 value。與其他操作一樣,調(diào)用 update() 方法也會(huì)創(chuàng)建一個(gè)新請求,因此如果你想知道結(jié)果,就要為它指定 onsuccess 和 onerror 事件處理程序。

request.onsuccess = function(event){
    var cursor = event.target.result,
        value,
        updateRequest;

    if (cursor){ // 必須要檢查
        if (cursor.key == "foo"){
            value = cursor.value; // 取得當(dāng)前的值
            value.password = "magic!"; // 更新密碼

            updateRequest = cursor.update(value); // 請求保存更新
            updateRequest.onsuccess = function(){
                // 處理成功
            };
            updateReqeust.onerror = function(){
                // 處理失敗
            };
        }
    }
};

??此時(shí),如果調(diào)用 delete() 方法,就會(huì)刪除相應(yīng)的記錄。與 update() 一樣,調(diào)用 delete() 也返回一個(gè)請求。

request.onsuccess = function(event){
    var cursor = event.target.result,
        value,
        deleteRequest;

    if (cursor){ // 必須要檢查
        if (cursor.key == "foo"){
            deleteRequest = cursor.delete(); // 請求刪除當(dāng)前項(xiàng)
            deleteRequest.onsuccess = function(){
                // 處理成功
            };
            deleteRequest.onerror = function(){
                // 處理失敗
            };
        }
    }
};

??如果當(dāng)前事務(wù)沒有修改對(duì)象存儲(chǔ)空間的權(quán)限,update() 和 delete() 會(huì)拋出錯(cuò)誤。

??默認(rèn)情況下,每個(gè)游標(biāo)只發(fā)起一次請求。要想發(fā)起另一次請求,必須調(diào)用下面的一個(gè)方法。

  • continue(key):移動(dòng)到結(jié)果集中的下一項(xiàng)。參數(shù) key 是可選的,不指定這個(gè)參數(shù),游標(biāo)移動(dòng)到下一項(xiàng);指定這個(gè)參數(shù),游標(biāo)會(huì)移動(dòng)到指定鍵的位置。
  • advance(count):向前移動(dòng) count 指定的項(xiàng)數(shù)。

??這兩個(gè)方法都會(huì)導(dǎo)致游標(biāo)使用相同的請求,因此相同的 onsuccess 和 onerror 事件處理程序也會(huì)得到重用。例如,下面的例子遍歷了對(duì)象存儲(chǔ)空間中的所有項(xiàng)。

request.onsuccess = function(event){
    var cursor = event.target.result;
    if (cursor){ // 必須要檢查
       console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
       cursor.continue(); // 移動(dòng)到下一項(xiàng)
    } else {
        console.log("Done!");
    }
};

??調(diào)用 continue() 會(huì)觸發(fā)另一次請求,進(jìn)而再次調(diào)用 onsuccess 事件處理程序。在沒有更多項(xiàng)可以迭代時(shí),將最后一次調(diào)用 onsuccess 事件處理程序,此時(shí) event.target.result 的值為 null。

5. 鍵范圍

??使用游標(biāo)總讓人覺得不那么理想,因?yàn)橥ㄟ^游標(biāo)查找數(shù)據(jù)的方式太有限了。
??鍵范圍(key range)為使用游標(biāo)增添了一些靈活性。鍵范圍由 IDBKeyRange 的實(shí)例表示。支持標(biāo)準(zhǔn) IDBKeyRange 類型的瀏覽器有 IE10+ 和 Firefox 4+,Chrome 中的名字叫 webkitIDBKeyRange。與使用 IndexedDB 中的其他類型一樣,你最好先聲明一個(gè)本地的類型,同時(shí)要考慮到不同瀏覽器中的差異。

var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

??有四種定義鍵范圍的方式。第一種是使用 only() 方法,傳入你想要取得的對(duì)象的鍵。

var onlyRange = IDBKeyRange.only("007");

??這個(gè)范圍可以保證只取得鍵為"007"的對(duì)象。使用這個(gè)范圍創(chuàng)建的游標(biāo)與直接訪問存儲(chǔ)空間并調(diào)用 get("007" )差不多。

??第二種定義鍵范圍的方式是指定結(jié)果集的下界。下界表示游標(biāo)開始的位置。例如,以下鍵范圍可以保證游標(biāo)從鍵為"007"的對(duì)象開始,然后繼續(xù)向前移動(dòng),直至最后一個(gè)對(duì)象。

// 從鍵為"007"的對(duì)象開始,然后可以移動(dòng)到最后
var lowerRange = IDBKeyRange.lowerBound("007");

??如果你想忽略鍵為"007"的對(duì)象,從它的下一個(gè)對(duì)象開始,那么可以傳入第二個(gè)參數(shù) true:

// 從鍵為"007"的對(duì)象的下一個(gè)對(duì)象開始,然后可以移動(dòng)到最后
var lowerRange = IDBKeyRange.lowerBound("007", true);

??第三種定義鍵范圍的方式是指定結(jié)果集的上界,也就是指定游標(biāo)不能超越哪個(gè)鍵。指定上界使用 upperRange() 方法。下面這個(gè)鍵范圍可以保證游標(biāo)從頭開始,到取得鍵為"ace"的對(duì)象終止。

// 從頭開始,到鍵為"ace"的對(duì)象為止
var upperRange = IDBKeyRange.upperBound("ace");

??如果你不想包含鍵為指定值的對(duì)象,同樣,傳入第二個(gè)參數(shù) true:

// 從頭開始,到鍵為"ace"的對(duì)象的上一個(gè)對(duì)象為止
var upperRange = IDBKeyRange.upperBound("ace", true);

??第四種定義鍵范圍的方式——沒錯(cuò),就是同時(shí)指定上、下界,使用 bound() 方法。這個(gè)方法可以接收 4 個(gè)參數(shù):表示下界的鍵、表示上界的鍵、可選的表示是否跳過下界的布爾值和可選的表示是否跳過上界的布爾值。以下是幾個(gè)例子。

// 從鍵為"007"的對(duì)象開始,到鍵為"ace"的對(duì)象為止
var boundRange = IDBKeyRange.bound("007", "ace");

// 從鍵為"007"的對(duì)象的下一個(gè)對(duì)象開始,到鍵為"ace"的對(duì)象為止
var boundRange = IDBKeyRange.bound("007", "ace", true);

// 從鍵為"007"的對(duì)象的下一個(gè)對(duì)象開始,到鍵為"ace"的對(duì)象的上一個(gè)對(duì)象為止
var boundRange = IDBKeyRange.bound("007", "ace", true, true);

// 從鍵為"007"的對(duì)象開始,到鍵為"ace"的對(duì)象的上一個(gè)對(duì)象為止
var boundRange = IDBKeyRange.bound("007", "ace", false, true);

??無論如何,在定義鍵范圍之后,把它傳給 openCursor() 方法,就能得到一個(gè)符合相應(yīng)約束條件的游標(biāo)。

var store = db.transaction("users").objectStore("users"),
    range = IDBKeyRange.bound("007", "ace");
    request = store.openCursor(range);

request.onsuccess = function(event){
    var cursor = event.target.result;
    if (cursor){ // 必須要檢查
        console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
        cursor.continue(); // 移動(dòng)到下一項(xiàng)
    } else {
        console.log("Done!");
    }
};

??這個(gè)例子輸出的對(duì)象的鍵為"007"到"ace",比上一節(jié)最后那個(gè)例子輸出的值少一些。

6. 設(shè)定游標(biāo)方向

??實(shí)際上,openCursor() 可以接收兩個(gè)參數(shù)。第一個(gè)參數(shù)就是剛剛看到的 IDBKeyRange 的實(shí)例,第二個(gè)是表示方向的數(shù)值常量。作為第二個(gè)參數(shù)的常量是前面講查詢時(shí)介紹的 IDBCursor 中的常量。Fire fox4 +和 Chrome 的實(shí)現(xiàn)又有不同,因此第一步還是在本地消除差異:

var IDBCursor = window.IDBCursor || window.webkitIDBCursor;

??正常情況下,游標(biāo)都是從存儲(chǔ)空間的第一項(xiàng)開始,調(diào)用 continue() 或 advance() 前進(jìn)到最后一項(xiàng)。游標(biāo)的默認(rèn)方向值是 IDBCursor.NEXT。
??如果對(duì)象存儲(chǔ)空間中有重復(fù)的項(xiàng),而你想讓游標(biāo)跳過那些
重復(fù)的項(xiàng),可以為 openCursor 傳入 IDBCursor.NEXT_NO_DUPLICATE 作為第二個(gè)參數(shù):

var store = db.transaction("users").objectStore("users"),
 request = store.openCursor(null, IDBCursor.NEXT_NO_DUPLICATE);

??注意,openCursor() 的第一個(gè)參數(shù)是 null,表示使用默認(rèn)的鍵范圍,即包含所有對(duì)象。這個(gè)游標(biāo)可以從存儲(chǔ)空間中的第一個(gè)對(duì)象開始,逐個(gè)迭代到最后一個(gè)對(duì)象——但會(huì)跳過重復(fù)的對(duì)象。

??當(dāng)然,也可以創(chuàng)建一個(gè)游標(biāo),讓它在對(duì)象存儲(chǔ)空間中向后移動(dòng),即從最后一個(gè)對(duì)象開始,逐個(gè)迭代,直至第一個(gè)對(duì)象。此時(shí),要傳入的常量是 IDBCursor.PREV 和 IDBCursor.PREV_NO_DUPLICATE。例如:

var store = db.transaction("users").objectStore("users"),
 request = store.openCursor(null, IDBCursor.PREV);

??使用 IDBCursor.PREV 或 IDBCursor.PREV_NO_DUPLICATE 打開游標(biāo)時(shí),每次調(diào)用 continue() 或 advance(),都會(huì)在存儲(chǔ)空間中向后而不是向前移動(dòng)游標(biāo)。

7. 索引

??對(duì)于某些數(shù)據(jù),可能需要為一個(gè)對(duì)象存儲(chǔ)空間指定多個(gè)鍵。比如,若要通過用戶 ID 和用戶名兩種方式來保存用戶資料,就需要通過這兩個(gè)鍵來存取記錄。為此,可以考慮將用戶 ID 作為主鍵,然后為用戶名創(chuàng)建索引。
??要?jiǎng)?chuàng)建索引,首先引用對(duì)象存儲(chǔ)空間,然后調(diào)用 createIndex() 方法,如下所示。

var store = db.transaction("users").objectStore("users"),
    index = store.createIndex("username", "username", { unique: false});

??createIndex()的第一個(gè)參數(shù)是索引的名字,第二個(gè)參數(shù)是索引的屬性的名字,第三個(gè)參數(shù)是一個(gè)包含 unique 屬性的選項(xiàng)(options)對(duì)象。
??這個(gè)選項(xiàng)通常都必須指定,因?yàn)樗硎炬I在所有記錄中是否唯一。因?yàn)?username 有可能重復(fù),所以這個(gè)索引不是唯一的。

??createIndex() 的返回值是 IDBIndex 的實(shí)例。在對(duì)象存儲(chǔ)空間上調(diào)用 index() 方法也能返回同一個(gè)實(shí)例。例如,要使用一個(gè)已經(jīng)存在的名為"username"的索引,可以像下面這樣取得該索引。

var store = db.transaction("users").objectStore("users"),
 index = store.index("username");

??索引其實(shí)與對(duì)象存儲(chǔ)空間很相似。在索引上調(diào)用 openCursor() 方法也可以創(chuàng)建新的游標(biāo),除了將來會(huì)把索引鍵而非主鍵保存在 event.result.key 屬性中之外,這個(gè)游標(biāo)與在對(duì)象存儲(chǔ)空間上調(diào)用 openCursor() 返回的游標(biāo)完全一樣。來看下面的例子。

var store = db.transaction("users").objectStore("users"),
    index = store.index("username"),
    request = index.openCursor();

request.onsuccess = function(event){
    // 處理成功
};

??在索引上也能創(chuàng)建一個(gè)特殊的只返回每條記錄主鍵的游標(biāo),那就要調(diào)用 openKeyCursor() 方法。
??這個(gè)方法接收的參數(shù)與 openCursor() 相同。而最大的不同在于,這種情況下 event.result.key 中仍然保存著索引鍵,而 event.result.value 中保存的則是主鍵,而不再是整個(gè)對(duì)象。

var store = db.transaction("users").objectStore("users"),
    index = store.index("username"),
    request = index.openKeyCursor();

request.onsuccess = function(event){
    // 處理成功
    // event.result.key 中保存索引鍵,而 event.result.value 中保存主鍵
};

??同樣,使用 get() 方法能夠從索引中取得一個(gè)對(duì)象,只要傳入相應(yīng)的索引鍵即可;當(dāng)然,這個(gè)方法也將返回一個(gè)請求。

var store = db.transaction("users").objectStore("users"),
    index = store.index("username"),
    request = index.get("007");

request.onsuccess = function(event){
    // 處理成功
};
request.onerror = function(event){
    // 處理失敗
}; 

??要根據(jù)給定的索引鍵取得主鍵,可以使用 getKey() 方法。這個(gè)方法也會(huì)創(chuàng)建一個(gè)新的請求,但 event.result.value 等于主鍵的值,而不是包含整個(gè)對(duì)象。

var store = db.transaction("users").objectStore("users"),
    index = store.index("username"),
    request = index.getKey("007");

request.onsuccess = function(event){
    // 處理成功
    // event.result.key 中保存索引鍵,而 event.result.value 中保存主鍵
};

??在這個(gè)例子的 onsuccess 事件處理程序中,event.result.value 中保存的是用戶 ID。

??任何時(shí)候,通過 IDBIndex 對(duì)象的下列屬性都可以取得有關(guān)索引的相關(guān)信息。

  • name:索引的名字。
  • keyPath:傳入 createIndex() 中的屬性路徑。
  • objectStore:索引的對(duì)象存儲(chǔ)空間。
  • unique:表示索引鍵是否唯一的布爾值。

??另外,通過對(duì)象存儲(chǔ)對(duì)象的 indexName 屬性可以訪問到為該空間建立的所有索引。通過以下代碼就可以知道根據(jù)存儲(chǔ)的對(duì)象建立了哪些索引。

var store = db.transaction("users").objectStore("users"),
    indexNames = store.indexNames,
    index,
    i = 0,
    len = indexNames.length;

while(i < len){
    index = store.index(indexNames[i++]);
    console.log("Index name: " + index.name + ", KeyPath: " + index.keyPath +
                ", Unique: " + index.unique);
   }

??以上代碼遍歷了每個(gè)索引,在控制臺(tái)中輸出了它們的信息。
??在對(duì)象存儲(chǔ)空間上調(diào)用 deleteIndex() 方法并傳入索引的名字可以刪除索引。

var store = db.transaction("users").objectStore("users");
store.deleteIndex("username");

??因?yàn)閯h除索引不會(huì)影響對(duì)象存儲(chǔ)空間中的數(shù)據(jù),所以這個(gè)操作沒有任何回調(diào)函數(shù)。

8. 并發(fā)問題

??雖然網(wǎng)頁中的 IndexedDB 提供的是異步 API,但仍然存在并發(fā)操作的問題。如果瀏覽器的兩個(gè)不同的標(biāo)簽頁打開了同一個(gè)頁面,那么一個(gè)頁面試圖更新另一個(gè)頁面尚未準(zhǔn)備就緒的數(shù)據(jù)庫的問題就有可能發(fā)生。
??把數(shù)據(jù)庫設(shè)置為新版本有可能導(dǎo)致這個(gè)問題。因此,只有當(dāng)瀏覽器中僅有一個(gè)標(biāo)簽頁使用數(shù)據(jù)庫的情況下,調(diào)用 setVersion() 才能完成操作。
??剛打開數(shù)據(jù)庫時(shí),要記著指定 onversionchange 事件處理程序。當(dāng)同一個(gè)來源的另一個(gè)標(biāo)簽頁調(diào)用 setVersion() 時(shí),就會(huì)執(zhí)行這個(gè)回調(diào)函數(shù)。處理這個(gè)事件的最佳方式是立即關(guān)閉數(shù)據(jù)庫,從而保證版本更新順利完成。例如:

var request, database;

request = indexedDB.open("admin");

request.onsuccess = function(event){
    database = event.target.result;
    database.onversionchange = function(){
        database.close();
    };
};

??每次成功打開數(shù)據(jù)庫,都應(yīng)該指定 onversionchange 事件處理程序。調(diào)用 setVersion() 時(shí),指定請求的 onblocked 事件處理程序也很重要。在你想要更新數(shù)據(jù)庫的版本但另一個(gè)標(biāo)簽頁已經(jīng)打開數(shù)據(jù)庫的情況下,就會(huì)觸發(fā)這個(gè)事件處理程序。
??此時(shí),最好先通知用戶關(guān)閉其他標(biāo)簽頁,然后再重新調(diào)用 setVersion()。例如:

var request = database.setVersion("2.0");

request.onblocked = function(){
    alert("Please close all other tabs and try again.");
};
request.onsuccess = function(){
    // 處理成功,繼續(xù)
};

??請記住,其他標(biāo)簽頁中的 onversionchange 事件處理程序也會(huì)執(zhí)行。

??通過指定這些事件處理程序,就能確保你的 Web 應(yīng)用妥善地處理好 IndexedDB 的并發(fā)問題。

9. 限制

??對(duì) IndexedDB 的限制很多都與對(duì) Web Storage 的類似。首先,IndexedDB 數(shù)據(jù)庫只能由同源(相同協(xié)議、域名和端口)頁面操作,因此不能跨域共享信息。換句話說,www.wrox.comp2p.wrox.com 的數(shù)據(jù)庫是完全獨(dú)立的。
??其次,每個(gè)來源的數(shù)據(jù)庫占用的磁盤空間也有限制。Firefox 4+目前的上限是每個(gè)源 50MB,而 Chrome 的限制是 5MB。移動(dòng)設(shè)備上的 Firefox 最多允許保存 5MB,如果超過了這個(gè)配額,將會(huì)請求用戶的許可。
??Firefox 還有另外一個(gè)限制,即不允許本地文件訪問 IndexedDB。Chrome 沒有這個(gè)限制。如果你在本地運(yùn)行本書的示例,請使用 Chrome。

小結(jié)

??離線 Web 應(yīng)用和客戶端存儲(chǔ)數(shù)據(jù)的能力對(duì)未來的 Web 應(yīng)用越來越重要。瀏覽器已經(jīng)能夠檢測到用戶是否離線,并觸發(fā) JavaScript 事件以便應(yīng)用做出處理??梢灾付ㄔ趹?yīng)用緩存中保存哪些文件以便離線時(shí)使用。對(duì)于應(yīng)用緩存的狀態(tài)及變化,也有相應(yīng)的 JavaScript API 可以調(diào)用檢測。

??本節(jié)還討論了客戶端存儲(chǔ)的以下幾方面內(nèi)容。

  • 以前,這種存儲(chǔ)只能使用 cookie 完成,cookie 是一小塊可以客戶端設(shè)置也可以在服務(wù)器端設(shè)置的信息,每次發(fā)起請求時(shí)都會(huì)傳送它。
  • 在 JavaScript 中通過 document.cookie 可以訪問 cookie。
  • cookie 的限制使其可以存儲(chǔ)少量數(shù)據(jù),然而對(duì)于大量數(shù)據(jù)效率很低。

??IE 發(fā)明了一種叫做用戶數(shù)據(jù)的行為,可以應(yīng)用到頁面的某個(gè)元素上,它有以下特點(diǎn)。

  • 一旦應(yīng)用后,該元素便可以從一個(gè)命名數(shù)據(jù)空間中載入數(shù)據(jù),然后可以通過 getAttribute()、setAttribute() 和 removeAttribute() 方法訪問。
  • 數(shù)據(jù)必須明確使用 save() 方法保存到命名數(shù)據(jù)空間中,以便能在會(huì)話之間持久化數(shù)據(jù)。

??Web Storage 定義了兩種用于存儲(chǔ)數(shù)據(jù)的對(duì)象:sessionStorage 和 localStorage。前者嚴(yán)格用于在一個(gè)瀏覽器會(huì)話中存儲(chǔ)數(shù)據(jù),因?yàn)閿?shù)據(jù)在瀏覽器關(guān)閉后會(huì)立即刪除;后者用于跨會(huì)話持久化數(shù)據(jù)并遵循跨域安全策略。

??IndexedDB 是一種類似 SQL 數(shù)據(jù)庫的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)機(jī)制。但它的數(shù)據(jù)不是保存在表中,而是保存在對(duì)象存儲(chǔ)空間中。創(chuàng)建對(duì)象存儲(chǔ)空間時(shí),需要定義一個(gè)鍵,然后就可以添加數(shù)據(jù)??梢允褂糜螛?biāo)在對(duì)象存儲(chǔ)空間中查詢特定的對(duì)象。而索引則是為了提高查詢速度而基于特定的屬性創(chuàng)建的。

??有了以上這些選擇,就可以在客戶端機(jī)器上使用 JavaScript 存儲(chǔ)大量數(shù)據(jù)了。但你必須小心,不要在客戶端存儲(chǔ)敏感數(shù)據(jù),因?yàn)閿?shù)據(jù)緩存不會(huì)加密。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評(píng)論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,840評(píng)論 18 399
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,875評(píng)論 11 349
  • 這個(gè)雙十一我很為自己驕傲了一把,除了一些生活和換季的必備品之外,購物車?yán)锘旧蠜]有什么東西。 可是因?yàn)槭侵苣稍?..
    w壁立千仞閱讀 200評(píng)論 2 2
  • 01我用紙疊成翅膀帶你飛躍千山萬水一起墜落在脆弱的夢想 02我們的愛像透明的水母清澈見底 03因?yàn)榈饶愕臅r(shí)間太長我...
    蘅蕪瀟瀟閱讀 2,189評(píng)論 66 70

友情鏈接更多精彩內(nèi)容