離線應(yīng)用與客戶端存儲

本章內(nèi)容:進行離線檢測、使用離線緩存、在瀏覽器中保存數(shù)據(jù)

支持離線 Web 應(yīng)用開發(fā)時 HTML5 的另一個重點。HTML5 把離線應(yīng)用作為重點,主要是基于開發(fā)人員的心愿。

開發(fā)離線Web應(yīng)用程序。

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

一、離線檢測

開發(fā)離線應(yīng)用的第一步是要知道設(shè)備是在線還是離線,HTML5 為此定義了一個 navigator.onLine 屬性,這個屬性值為 true 表示設(shè)備能上網(wǎng),值為 false 表示設(shè)備離線。
navigator.onLine 在不同瀏覽器間還有些小差距

  • IE6+、Safari5+、能夠正確檢測到網(wǎng)絡(luò)已斷開,并將 navigator.onLine 的值轉(zhuǎn)換為 false
  • Firefox 3+ 和 Opera 10.6+支持 navigator.onLine 屬性,但你必須手工選中菜單項“文件-> Web開發(fā)人員(設(shè)置) -> 脫機工作” 才能讓瀏覽器正常工作

以下是檢測該屬性狀態(tài)的示例:

if (navigator.onLine) { // 正常工作
  // todo
} else { // 離線狀態(tài)
  //todo
}

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

window.ononline = function() {
  // todo
}
window.onoffline = function() {
  // todo
}

在頁面加載后,最好先通過 navigator.onLine 取得初始的狀態(tài)。然后,就是通過上述兩個事件來確定網(wǎng)絡(luò)連接狀態(tài)是否變化。當上述事件觸發(fā)時,navigator.onLine 屬性的值也會改變,不過必須手工輪詢這個屬性才能檢測到網(wǎng)絡(luò)狀態(tài)的變化。

二、應(yīng)用緩存

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

CACHE MANIFEST
# Comment

file.js
file.css

在最簡單的情況下,描述文件中列出的都是需要下載的資源,以備離線時使用


要將描述文件與頁面關(guān)聯(lián)起來,可以在<html> 中的 manifest 屬性中指定這個文件的路徑,例如:

<html manifest="/offline,manifest">

這個文件的 MIME 類型必須是 text/cache-manifest

同時有相應(yīng)的 JavaScript API 讓你知道它都在做什么。這個 API 的核心是 applicationCache 對象,這個對象有一個 status 屬性
屬性的值是常量,表示應(yī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)用緩存查找更新時觸發(fā)。
  • error:在檢測更新或下載資源期間發(fā)生錯誤時觸發(fā)
  • noupdate:在檢查描述文件發(fā)現(xiàn)文件無變化時觸發(fā)
  • downloading:在開始下載應(yīng)用緩存資源時觸發(fā)
  • progress:在文件下載應(yīng)用緩存的過程中持續(xù)不斷地觸發(fā)
  • updateready:在頁面新的應(yīng)用緩存下載完畢且可以通過 swapCache() 使用時觸發(fā)。
  • cached:在應(yīng)用緩存完整可用時觸發(fā)

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

applicationCache.update()

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

applicationCache.onupdateready = function() {
  applicationCache.swapCache()
}

三、數(shù)據(jù)存儲

隨著 Web 應(yīng)用程序的出現(xiàn),也產(chǎn)生了對于能夠直接在客戶端上存儲用戶信息能力的要求。這個問題的第一個方案是以 cookie 的形式出現(xiàn)的,cookie是原來的網(wǎng)景公司創(chuàng)造的。一份題為“Persistent Client State:HTTP Cookies”(持久客戶端狀態(tài):HTTP Cookies)的標準中對 cookie 機制進行了闡述。今天,cookie 只是在客戶端存儲數(shù)據(jù)的其中一種選項。

3.1、Cookie

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

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

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

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

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

3.1.1、限制

cookie 在性質(zhì)上時綁定在特定的域名下的,這個限制確保了儲存在 cookie 中的信息只能讓批準的接受者訪問,而無法被其他域訪問。
每個域的 cookie 總數(shù)有限,不過瀏覽器之間各有不同:

  • IE6以及更低版本限制每個域名 最多 20個 cookie
  • IE7 和之后版本每個域名最多 50個。IE7最初是支持每個域名 最大 20個 cookie,之后被微軟一個補丁更新了
  • Firefox 限制每個域最多 50個cookie
  • Opera 限制每個域最多 30個 cookie
  • Safari 和 Chrome 對于每個域的 cookie 數(shù)量限制沒有硬性規(guī)定。
    當超過單個域名限制之后還要再設(shè)置 cookie,瀏覽器就會清除以前設(shè)置的 cookie。IE 和 Opera 則會刪除最近最少使用的 cookie。Firefox 看上去好像是 隨機決定要清除哪個 cookie,所以考慮 cookie限制非常重要,以免出現(xiàn)不可預(yù)期的后果。

瀏覽器中對于 cookie 的尺寸也有限制。大多數(shù)瀏覽器都有 大約 4096B(加減一)的長度限制。尺寸限制影響到一個域下面所有的 cookie,如果你嘗試創(chuàng)建超過最大尺寸限制的cookie,那么該 cookie 會被悄無聲息地丟掉。

3.1.2、cookie 的構(gòu)成

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

  • 名稱:一個唯一確定的cookie名稱。cookie 名稱是不區(qū)分大小寫的,然而,實踐中最好將 cookie 名稱看作是區(qū)分大小寫的,因為某些服務(wù)器會這樣處理cookie。cookie的名稱必須經(jīng)過 URL 編碼。
  • 值:儲存在 cookie 中的字符串值。值必須被 URL 編碼
  • 域:cookie 對于哪個域是有效的。所有項該域發(fā)送的請求中都會包含這個 cookie 信息。這個值可以包含子域,如果沒有明確設(shè)定,那么這個域會被認作來自設(shè)置 cookie的那個域。
  • 路徑:對于指定域中的那個路徑,應(yīng)該向服務(wù)器發(fā)送 cookie。
  • 失效時間:表示 cookie 何時應(yīng)該被刪除的時間戳(GMT日期格式)。默認情況下,瀏覽器會話結(jié)束時即將所有 cookie 刪除;
  • 安全標志:指定后,cookie 只有在使用 SSL 連接到時候才發(fā)送到服務(wù)器。

每一段信息都作為 Set-Cookie 頭的一部分,使用分號加空格分隔 每一段。如下所示:

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

該頭部信息指定了一個叫做 name 的 cookie,它會在格林威治事件 2007年 1月 22日 7:10:24失效,同時對于 www.wrox.com 和 wrox.com 的任何子域(如 p2p.wrox.com)都有效

secure 標志是 cookie 中唯一一個非名值對兒的部分,直接包含一個 secure 單詞。

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

因為設(shè)置了 secure 標志,這個 cookie 只能通過 SSL 連接才能傳輸。

域、路徑、失效時間、secure 標志都是服務(wù)器給瀏覽器的指示,以指定何時應(yīng)該發(fā)送 cookie。這些參數(shù)并不會作為發(fā)送服務(wù)器的 cookie 信息的一部分,只有明值對兒才會被發(fā)送。

3.1.3、JavaScript 中的 cookie

在 JavaScript 中處理 cookie 有些復(fù)雜,即 BOM的 document.cookie屬性,返回當前頁面可用的 所有 cookie 的字符串,一系列由分號隔開的名值對兒。

name1=value1;name2=value2;name3=value3

所有名字和值都是經(jīng)過URL 編碼的,所以必須使用 decodeURIComponent() 來解碼


當用于設(shè)置值的時候,document.cookie 屬性可以設(shè)置為一個新的 cookie 字符串。這個cookie 字符串會被添加到 cookie 集合中。
設(shè)置 cookie 的格式如下,和 Set-Cookie 頭中使用的格式一樣。

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

這些參數(shù)中,只有 cookie 的名字和值是必需的

document.cookie = 'name=Lee'

最好每次設(shè)置 cookie 時都想下面這個例子中一樣使用 encodeURIComponent()

document.cookie = encodeURIComponent('name') + '=' + encodeURIComponent('Lee');

?


?

由于 JavaScript 中讀寫 cookie 不是非常直觀,常常需要寫一些函數(shù)來簡化 cookie 的功能?;?cookie 操作有三種:讀取、寫入和刪除。

var CookieUtil = {
  get: function(name) {
    var cookieName = encodeURIComponent(name) + '=',
        cookieStart = document.cookie.indexOf(cookieName), // 查找 查詢鍵的位置
        cookieValue = null

    if (cookieStart > -1) { // 存在對應(yīng) 鍵
      var cookieEnd = document.cookie.indexOf(';', cookieStart) // 查找對應(yīng)鍵 的值結(jié)束位置
      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, doamin, secure) {
    this.set(name, '', new Date(0), path, domain, secure) //設(shè)置過期時間
  }
}

可以像下面這樣使用上述方法

// 設(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)
3.1.4、子 cookie

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

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

子 cookie 一般也以查詢字符串的格式進行格式化。然后這些值可以使用 單個 cookie 進行存儲和訪問,而非對每個名稱-值對兒使用不同的 cookie 存儲。最后網(wǎng)站或者 Web 應(yīng)用程序可以達到單域名 cookie 上限也可以存儲更加結(jié)構(gòu)化的數(shù)據(jù)。


?

為了更好地操作子 cookie,必須建立一系列新方法。

var SubCookieUtil = {

  get: function(name, subName) {
    var subCookies = this.getAll(name) // 獲取子 cookie 對象
    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,
        len,
        parts,
        result = {}

    if (cookieStart > -1) { // 存在 子 cookie
      cookieEnd = document.cookie.indexOf(';', cookieStart) // 子 cookie 結(jié)束的位置

      if (cookieEnd == -1) { // 父cookie 是最后一對鍵值 
        cookieEnd = document.cookie.length // 子 cookie 結(jié)束的位置即為 cookie的長度
      }
      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
    }
  }
}

可以像下面這樣使用 上述方法:

// 假設(shè) cookie 為以下值
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

// 逐個獲取 子 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) || {} // 獲取 cookie 鍵值對象
    subcookies[subName] = value // 添加 
    this.setAll(name, subcookies, expires, path, domain, secure) // 設(shè)置
  },

  setAll: function(name, subcookies, expires, path, domain, secure) {
    var cookieText = encodeURIComponent(name) + '=', 
        subcookieParts = new Array(),
        subName  
    
    for (subName in  subcookies) { // 遍歷 子 cookie 對象
      if (subName.length > 0 && subcookies.hasOwnProperty(subName)) {
        // 以鍵值對字符串的形式 保存在數(shù)組中
        subcookieParts.push(encodeURIComponent(subName) +'='+ encodeURIComponent(subcookies[subName]))
      }
    }

    if (subcookieParts.length > 0) { // 以 & 為界定符 將字符數(shù)組轉(zhuǎn)換為字符串。
      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
  }

// ...省略了更多代碼
}

可以按照以下方式 來使用 set() 和 setAll() 方法

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

// 設(shè)置兩個 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, 2020'))

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

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

var SubCookieUtil = {
 // ...省略了更多代碼
  unset: function(name, subName, path, domain, secure) {
    var subCookies = this.getAll(name) // 獲取 子 cookie

    if (subCookies) {
      delete subCookies[subName] // 刪除

      this.setAll(name, subCookies, null, path, domain, secure) // 保存
    }
  },

  unsetAll: function(name, path, domain, secure) { // 設(shè)置過期時間
    this.setAll(name, null, new Date(0), path, domain, secure)
  }
}

這兩個方法可以像下面這樣使用

// 僅刪除名為 name 的子 cookie
SubCoolieUtil.unset('data', 'name')

// 刪除整個 cookie
SubCookieUtil.unsetAll('data')

?
?

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

3.1.5、關(guān)于 cookie 的思考

由于 所有的 cookie 都會有瀏覽器作為 請求頭發(fā)送,所以在 cookie 中存儲大量信息會影響到特定域的 請求性能。cookie 的性質(zhì)和它的局限使得其并不能作為存儲大量信息的理想手段。

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

在 IE5.0 中,微軟通過一個自定義行為引入了持久化用戶數(shù)據(jù)的概念。用戶數(shù)據(jù)允許每個文檔最多 128KB 數(shù)據(jù),每個域名最多 1MB數(shù)據(jù)。

要使用持久化用戶數(shù)據(jù),首先必須如下所示,使用 CSS 在某個元素上指定 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')

?
在下一次頁面載入之后,可以使用 load() 方法指定 同樣的數(shù)據(jù)空間名稱來獲取數(shù)據(jù)

dataStore.load('BookInfo')
console.log(dataStore.getAttribute('name')) // Nicholas
console.log(dataStore.getAttribute('book')) // Prefessional JavaScript

你可以通過 removeAttribute() 方法明確指定要刪除某些元素數(shù)據(jù),只要指定屬性名稱。
刪除之后,必須像下面這樣再次調(diào)用 save() 來提交更改

dataStore.removeAttribute('name')
dataStore.save('BookInfo')

對 IE 用戶數(shù)據(jù)的訪問限制和對 cookie 的限制類似。要訪問某個數(shù)據(jù)空間,腳本運行的頁面必將來自用一個域名,在同一個路徑下,并使用與進行存儲的腳本同樣的協(xié)議。和 cookie 不同的是:

  • 你無法將 用戶數(shù)據(jù)訪問限制擴展到更多的客戶。
  • 用戶數(shù)據(jù)默認是可以跨越會話持久存在的,同時也不會過期; 數(shù)據(jù)需要通過 removeAttribute() 方法專門進行 刪除以釋放空間。

3.3、Web 存儲機制

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

Web Storage 的兩個主要目標是:

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

最初 的 Web Storage 規(guī)范包含了兩種對象的定義:sessionStorageglobalStorage。這兩個對象再支持的瀏覽器中都是以 window 對象屬性的形式存在的。

3.3.1、Storage 類型

Storage 類型提供最大的存儲空間(因瀏覽器而異)來存儲名值對兒。Storage 的實例與其他對象類似,有如下方法。

  • clear():刪除所有值;
  • getItem(name):根據(jù)指定的名字 那么獲取對應(yīng)的值。
  • key(index):獲得 index 位置處的值的名字
  • removeItem(name):刪除有 name 指定的明值對兒。
  • setItem(name, value):為指定的 name 設(shè)置一個對應(yīng)的值。

其中,getItem()、removeItem()、setItem() 可以直接調(diào)用,也可以通過 Storage對象訪問。不過,建議使用方法而不是屬性來訪問數(shù)據(jù),以免某個鍵會以外重寫該對象上已經(jīng)存在的成員。
還可以使用 length 屬性來判斷有多少名值對兒存放在 Storage 對象中。

3.3.2、sessionStorage 對象

sessionStorage 對象存儲特定于某個會話的數(shù)據(jù),也就是該數(shù)據(jù)只保持到瀏覽器關(guān)閉。存儲在 sessionStorage 中的數(shù)據(jù)可以跨越頁面刷新而存在,同時如果瀏覽器支持,瀏覽器奔潰并重啟之后依然可用

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

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

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

不同瀏覽器寫入數(shù)據(jù)方式略有不同。Firefox 和 WebKit 實現(xiàn)了同步寫入,而IE 的實現(xiàn)則是異步寫入數(shù)據(jù),對于少量數(shù)據(jù)而言,這個差異是可以忽略的


sessionStorage 中有數(shù)據(jù)時,可以使用 getItem() 或者通過直接訪問屬性來獲取
數(shù)據(jù)

// 使用方法讀取數(shù)據(jù)
console.log(sessionStorage.getItem('name'))

// 使用屬性讀取數(shù)據(jù)
console.log(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)
}

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

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

?


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

// 使用 delete 刪除一個值
delete sessionStorage.name

// 使用方法刪除一個值
sessionStorage.removeItem('book')

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

3.3.3、globalStorage 對象

要使用 globalStorage,首先要指定 哪些域可以訪問該數(shù)據(jù)??梢酝ㄟ^方括號標記使用屬性來實現(xiàn),

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

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

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

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

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

這里所指定的存儲空間只能來自 www.wrox.com 的頁面訪問,其他子域名都不行。


某些瀏覽器允許更加寬泛的訪問限制,比如只根據(jù)頂級域名進行限制或者允許全局訪問。

// 存儲數(shù)據(jù),任何人都可以訪問——不建議這樣做
globalStorage[''].name = 'Nicholas'

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

雖然這些也支持,但是還是要避免使用這種可寬泛訪問的數(shù)據(jù)存儲,以防止出現(xiàn)潛在的安全問題。

對 globalStorage 空間的訪問,是依據(jù)發(fā)起請求的頁面的域名、協(xié)議、端口來限制的。這類似于 Ajax 的同源策略。


globalStorage 的每個屬性都是 Storage 的實例。因此,可以像如下代碼中這樣使用。

globalStorage['www.wrox.com'].name = 'Nicholas'
globalStorage['www.wrox.com'].book= 'Professional Javascript'

globalStorage['www.wrox.com'].removeItem('name')

console.log(globalStorage['www.wrox.com'].getItem('book'))

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

globalStorage[location.host].name = 'Nicholas'
console.log(globalStorage[location.host].getItem('book'))

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

3.3.4、localStorage 對象

localStorage 對象在 修訂過的 HTML5 規(guī)范中作為持久保存客戶端數(shù)據(jù)的方案取代了 globalStorage。

要訪問一個 localStorage對象,頁面必須來自同一個域名(子域名無效),使用同一種協(xié)議,在同一個端口上。這相當于 globalStorage[location.host]
由于 localStorage 是 Storage 的實例,所以可以像使用 sessionStorage 一樣來使用它。

// 使用 方法存儲數(shù)據(jù)
localStorage.setItem('name', 'Nicholas')
// 使用 屬性存儲數(shù)據(jù)
localStorage.book = 'Professional JavaScript'
// 使用方法讀取數(shù)據(jù)
console.log(localStorage.getItem('name'))
// 使用屬性讀取數(shù)據(jù)
console.log(localStorage.book)

存儲在 localStorage 中的數(shù)據(jù)和存儲在 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)用這個函數(shù),就可以正常地讀寫數(shù)據(jù)了

var storage = getLocalStorage()
3.3.5、storage 事件

對 Storage 對象進行任何修改,都會在文檔上觸發(fā) storage 事件
這個事件的 event 對象有以下屬性:

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

以下代碼展示了如何偵聽 storage 事件

document.onstorage = function(event) {
  console.log('Storage changed for ' + event.domain)
}

無論是 sessionStorage、globalStorage、localStorage 進行操作,都會觸發(fā) storage 事件,但不做區(qū)分。

3.3.6、限制

Web Storage 也有限制。這些限制因瀏覽器而異。對存儲空間大小的限制都是以每個來源(協(xié)議、域、端口)為單位的??紤]到這個限制,就要注意分析和控制每個來源中有多少頁面需要保持數(shù)據(jù)。
對于 localStorage 而言,大多數(shù)桌面瀏覽器會設(shè)置每個來源 5MB的限制。Chrome 和 Safari 對每個來源的限制時 2.5MB。而 iOS 版 Safari 和 Android 版 Webkit 的限制也是 2.5MB

對 sessionStorage 的限制也是因瀏覽器而異。有的瀏覽器對 sessionStorage 的大小沒有限制 但Chrome、Safari、iOS 版的Safari 和 Android版 WebKit 都有限制,也都是 2.5MB。IE8+ 和 Opera 對象 sessionStorage 的限制時 5MB。

3.4、IndexedDB

Indexed Database API, 是在瀏覽器中保存結(jié)構(gòu)化數(shù)據(jù)的一種數(shù)據(jù)庫。IndexedDB 的思想是創(chuàng)建一套 API,方便保存 和 讀取 JavaScript 對象,同時還支持查詢及搜索。
indexDB設(shè)計的操作完全是異步進行的。差不多每一次 indexedDB 操作,都需要你注冊 onerror 或 onsuccess事件處理程序,以確保適當處理結(jié)果

在得到完整支持的情況下,IndexDB 將是一個作為API 宿主的全局對象。由于 API 仍然可能有變化,瀏覽器也都使用提供商前綴,因為這個對象在 IE10 中叫 msIndexedDB,在 Firefox 4中叫 mozIndexedDB,在 Chrome 中叫 webkitIndexDB。

var indexedDB = window.indexDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB
3.4.1、數(shù)據(jù)庫

IndexedDB 就是一個數(shù)據(jù)庫,最大的特色是使用對象保存數(shù)據(jù),而不是使用表保持數(shù)據(jù)。一個IndexedDB 數(shù)據(jù)庫,就是一組位于相同命名空間下的對象的集合

使用 indexedDB 的第一步是打開它,即把要打開的數(shù)據(jù)庫名傳給 indexedDB.open()。如果傳入的數(shù)據(jù)庫已經(jīng)存在,就會發(fā)送一個打開它的請求;如果傳入的數(shù)據(jù)庫還不存在,就會發(fā)送一個創(chuàng)建并打開它的請求。調(diào)用 indexedDB.open() 會返回一個 IDBRequest 對象,在這個對象上可以添加 onerror 和 onsuccess 事件處理程序。

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

var request = indexedDB.open('admin')

request.onerror = function(event) {
  console.log('Error: ' + event.target.errorCode) // 發(fā)生錯了,保存一個錯誤碼
}
request.onsuccess = function(event) {
  var database = event.target.result // 響應(yīng)成功 數(shù)據(jù)庫實例對象
  // console.log(database) // IDBDatabase
}

在這兩個事件處理程序中, event.target 都指向 request 對象。

以下就是 error 中 errrorCode 可能出現(xiàn)的 錯誤碼(這個錯誤碼適合所有操作):

  • IDBDatabaseException.UNKNOWN_ERR(1):意外錯誤,無法歸類
  • 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):在有效時間內(nèi)未完成操作
  • IDBDatabaseException.QUOTA_ERR(11):磁盤空間不足

?


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

if (database.version != '1.0') request = database.setVersion('1.0')

3.4.2、對象存儲空間

在建立了與數(shù)據(jù)庫的連接之后下一步就是使用對象存儲空間。如果數(shù)據(jù)庫的版本與你傳入的版本不匹配,那可能就需要創(chuàng)建一個新的對象存儲空間。在創(chuàng)建對象存儲空間之前,必須要想清楚你想要保存什么類型數(shù)據(jù)。

保存一條記錄的對象應(yīng)該類似如下所示:

var user = {
  id: '00,
  username: 'momo11123',
  password: 'foo'
}

有了這個對象,很容易想到 username 屬性可以作為這個對象存儲空間的鍵。這個 username 必須全局唯一,而且大多數(shù)時候都要通過這個鍵來訪問數(shù)據(jù),這一點非常重要,因為在創(chuàng)建對象存儲空間時,必須指定這個一個鍵。

以下是為了保存上述用戶記錄而創(chuàng)建對象存儲空間的示例:

request.onupgradeneeded = function(event) { // 必須在 upgradeneeded 事件中進行創(chuàng)建
  var db = event.target.result;
  var objectStore = db.createObjectStore("users", { keyPath: "id" });
  console.log('數(shù)據(jù)庫版本更改為: ' +  db.version);
}

第一次打開成功后或者版本有變化時會觸發(fā)這個事件:一般用于初始化數(shù)據(jù)庫。其中,第二個參數(shù)中的keyPath 屬性,就是空間中將有保存的對象的一個屬性,而這個屬性將作為存儲空間的 鍵來使用

接下來可以使用 add() 或 put() 方法來向其中添加數(shù)據(jù)。這兩個方法都接受一個參數(shù),即要保存的對象。區(qū)別在于,在空間中已經(jīng)包含鍵值相同的對象時,add() 會返回錯誤,而 put() 會重寫原有對象。

// users 中保存著一批用戶對象
var users = [
  {
    id: '001',
    username: 'lwx',
    password: 'mom1123'
  },
  {
    id: '002',
    username: 'smlz',
    password: 'momo123'
  },
]


var i = 0, len = users.length

while(i < len) {
  store.add(users[i++])
}

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

// users(數(shù)組) 中保存著一批 用戶對象 
var users = [
  {
    id: '001',
    username: 'lwx',
    password: 'mom1123'
  },
  {
    id: '002',
    username: 'smlz',
    password: 'momo123'
  },
]


var i = 0,
      request,
      requests = [],
      len = users.length

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

  request.push(request)
}
3.4.3、事務(wù)

跨越創(chuàng)建對象存儲空間這一步后,接下來的所有操作都是通過事務(wù)來完成的。在數(shù)據(jù)庫對象上調(diào)用 transaction() 方法可以創(chuàng)建事務(wù)。任何時候,只要想讀取或修改數(shù)據(jù),都要通過事務(wù)來組織所有操作。在最簡單的情況下,可以像下面這個創(chuàng)建事務(wù)。

var transaction = db.transaction('users')

如果要訪問多個對象存儲空間,也可以在第一個參數(shù)的位置上傳入字符串數(shù)組

var transaction = db.transaction(['users', 'anothersStore'])

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

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

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

var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction

如此,就可以更方便的為 transaction() 指定第二個參數(shù)了

var transaction = db.transaction('users', IDBTransaction.READ_WRITE)

取得了事務(wù)的索引后,使用 objectStore() 方法并傳入存儲空間的名稱,就可以訪問特定的存儲空間。然后,可以像以前一樣使用 add() 和 put() 方法,此外:

  • get() 用來獲取值 —— 接受一個對象鍵作為參數(shù)
  • delete() 用來刪除對象 —— 接受一個對象鍵作為參數(shù)
  • clear() 則可以刪除所有對象
var transaction = db.transaction('users', IDBTransaction.READ_WRITE)
  var request = transaction.objectStore('users').get('001')

  request.onerror = function(event) {
    console.error('Error ' + event.error.message)
  }
  request.onsuccess = function(event) {
    var result = event.target.result
    console.log(result.username) // lwx
  }

因為 一個事務(wù) 可以完成任何多個請求,所以事務(wù)對象本身也有事件處理程序:onerror 和 oncomplete。這兩個事件可以提供事務(wù)級的狀態(tài)信息。

transaction.onerror = function(event) {
  // 整個事務(wù)都被取消了
}

transaction.oncomplete = function(event) {
  // 整個事務(wù)都成功完成了
}

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

3.4.4、使用游標查詢

使用 事務(wù)可以直接通過已知的鍵檢索單個對象。而在需要檢索多個對象的情況下,則需要在事務(wù)內(nèi)部創(chuàng)建游標。游標就是一指向結(jié)果集的指針。游標并不提前收集結(jié)果。游標指針會先指向結(jié)果中的第一項,在接到查找下一項指令是,才會指向下一項。

在對象存儲空間上調(diào)用 openCursor() 方法可以創(chuàng)建游標。與 indexedDB 中的其他操作一樣,openCursor() 方法返回的是一個請求對象,因此必須為該對象指定 onsuccess 和 onerror 事件處理程序。

var store = db.transaction('users').objectStore('users')
var request = store.openCursor() // 創(chuàng)建游標

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

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

  • direction:數(shù)值,表示游標移動的方向。
    • 默認值為 IDBCursor.NEXT(0),表示下一項
    • IDBCursor.NEXT_NO_DUPLICATE(1),表示下一個不重復(fù)的項
    • DBCursor.PREV(2),表示前一項
    • IDBCursor.PREV_NO_DUPLICATE 表示前一個不重復(fù)的項。
  • key:對象的鍵
  • value:實際的對象
  • primaryKey:游標使用的鍵??赡苁菍ο箧I,也可能是索引鍵

檢索某一個結(jié)果的信息:

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

?


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

 var store = db.transaction('users', 'readwrite').objectStore('users') // 更新需要在讀寫模式下進行
  var request = store.openCursor() // 創(chuàng)建游標

  request.onerror = function(event) {
    console.error('Error ' + event.error.message)
  }
  request.onsuccess = function(event) {
    var cursor = event.target.result
    
    if (cursor) { // 必須要檢查
      if (cursor.key == '001') {
        var value = cursor.value
        value.password = 'MOMO_)GG'

        var updateRequest = cursor.update(value)

        updateRequest.onsuccess = function(event) {

          console.log('處理成功')
        }
        updateRequest.onerror = function(event) {

          console.error('Error ' + event.taregt.error.message)
        }
      }
    }
    
  }

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

  request.onsuccess = function(event) {
    var cursor = event.target.result
    console.log(cursor)
    if (cursor) { // 必須要檢查
      if (cursor.key == '001') {

        vardeleteRequest = cursor.delete()

       deleteRequest.onsuccess = function(event) {

          console.log('處理成功')
        }
       deleteRequest.onerror = function(event) {

          console.error('Error ' + event.taregt.error.message)
        }
      }
    }
    
  }

如果當前事務(wù)沒有修改 對象存儲空間的權(quán)限,update() 和 delete() 會拋出錯誤
?


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

  • continue([key]):移動到結(jié)果集中的下一項。參數(shù) key 是可選的,不指定這個參數(shù),游標移動到下一項;指定這個參數(shù),游標會移動到指定位置。
  • advance(count):向前移動 count 指定的項數(shù)

著兩個方法都會導(dǎo)致游標使用相同的請求,因此同樣的 onsuccess 和 onerror 使勁按處理程序也會得到重用。例如,下面的例子遍歷了對象存儲空間中的所有項。

  request.onsuccess = function(event) {
    var cursor = event.target.result
    console.log(cursor)
    if (cursor) { // 必須要檢查
      cursor.continue() // 移動到下一項
    } else {
      console.log('Done!')
    }
  }

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

3.4.5、鍵范圍

鍵范圍(key range) 為適用游標增添了一些靈活性。鍵范圍有 IDBKeyRange 的實例表示。

支持標準 IDBKeyRange 類型的瀏覽器有 IE10+、Firefox4+,Chrome 中的名字叫 webkitIDBKeyRange。與使用 IndexedDB 中的其他類型一樣,你最好先聲明一個本地的類型,同時要考慮到不同瀏覽器中的差異。

var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange

有四種定義鍵范圍的方式:

  1. 使用 only() 方法,傳入你想要取得的對象的鍵
var onlyRange = IDBKeyRange.only('001')

這個范圍可以保證只取得鍵為 ‘007’ 的對象。使用這個范圍創(chuàng)建的游標與直接訪問存儲空間并調(diào)用 get('001')差不多

  1. 指定結(jié)果集的下界,lowerBound()。下界表示游標開始的位置。例如,以下鍵范圍可以保證游標從鍵為‘001’的對象開始,然后繼續(xù)向西移動,直至最后一個對象
// 從鍵為 001  的對象開始,然后可以移動到最后
var lowerRange = IDBKeyRange.lowerBound('001')

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

var lowerRange = IDBKeyRange.lowerBound('001', true)
  1. 指定結(jié)果集上界,也就是指定游標不能超越哪個鍵。指定上界使用 upperRange() 方法
// 從頭開始,到鍵為 002 的對象為止
var upperRange = IDBKeyRange.upperBound('002')

如果不想包含鍵為指定值對象,同樣,傳入第二個參數(shù) true

var upperRange = IDBKeyRange.upperBound('002', true)
  1. 同時指定上、下界,使用 bound() 方法。這個方法可以接收 4個參數(shù):表示下界的鍵、表示上界的鍵、可選的表示是否跳過下界的布爾值、可選的表示是否跳過下界的布爾值和可選的表示是否跳過上界的布爾值。
// 從鍵為 001 的對象開始,到鍵為007的對象
var boundRange = IDBKeyRange.bound('001', '007')

// 從鍵為 001 的對象的下一個對象開始,到鍵為  007 的對象為止
var boundRange = IDBKeyRange.bound('001', '007', true)

// 從鍵為 001 的對象的下一個對象開始,到鍵為 007 的對象的上一個為止
var boundRange = IDBKeyRange.bound('001', '007', true, true)

// 從鍵為 001 的對象開始,到鍵為  007 的對象的上一個對象為止
var boundRange = IDBKeyRange.bound('001', '007', false, true)

?


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

 var store = db.transaction('users', 'readwrite').objectStore('users') // 更新需要在讀寫模式下進行
  var range = IDBKeyRange.bound('001', '007')
  var request = store.openCursor(range) // 創(chuàng)建游標

  request.onerror = function(event) {
    console.error('Error ' + event.error.message)
  }
  request.onsuccess = function(event) {
   var cursor = event.target.result

    if (cursor) { // 必須要檢查
      console.log('key: ' + cursor.key + ': ' + JSON.stringify(cursor.value))
      cursor.continue() // 移動到下一項
    } else {
      console.log('done!')
    } 
  }
3.4.6、設(shè)定游標方向

實際上,openCursor() 可以接收兩個參數(shù)

  • 第一個參數(shù)是剛剛上面演示的 IDBKeyRange 的實例。
  • 第二個是表示方向的數(shù)值常量。

首先還是本地消除差異:

var IDBCursor = window.IDBCursor || window.webkitIDBCursor

游標的默認方向值是 IDBCursor.NEXT。如果對象存儲空間中有重復(fù)的項,而你想讓游標跳過那些重復(fù)的項,可以為 openCursor 傳入 IDBCursor.NEXT_NO_DUPLICATE 作為第二個參數(shù)

var store = db.transcation('users').objectStore('users')
var request = store.openCursor(null, IDBCursor.NEXT_NO_DUPLICATE)

當然,也可以創(chuàng)建一個游標,讓他在對象存儲空間中向后移動,即從最后一個對象開始,逐個迭代,直至第一個對象。此時,要傳入的常量是 IDBCursor.PREV 和 IDBCursor.PREV_NO_DUPLICATE。

var store = db.transcation('users').objectStore('users')
var request = store.openCursor(null, IDBCursor.PREV)

使用 上述兩種參數(shù) 打開游標時,每次調(diào)用 continue() 或 advance(),都會在存儲空間中向后而不是向前移動游標

3.4.7、索引

對于某些數(shù)據(jù),可能需要為一個對象存儲空間指定多個鍵,除主鍵外,還可以創(chuàng)建索引。
首先,引用對象存儲空間,然后調(diào)用 createIndex() 方法

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

createIndex() 三個參數(shù)分別表示:

  • 第一個參數(shù)是索引的名字
  • 第二個參數(shù)是索引的屬性的名字
  • 第三個參數(shù)是一個包含 unique 屬性的選項(options)對象。這個選項通常都必須指定,因為它表示鍵所在記錄中是否唯一。因為 username 有可能重復(fù),所以這里的值為 false

createIndex() 的返回值是 IDBIndex 的實例。在對象存儲空間上 調(diào)用 index() 方法也能返回同一個實例。

var store = db.transaction('users').objectStore('users'),
      index = store.index('username') // 獲取索引

在索引上調(diào)用 openCursor() 方法也可以創(chuàng)建新的游標,除了將來會把索引鍵而非主鍵保存在 event.result.key 屬性中之外,這個游標與在對象存儲空間上調(diào)用 openCursor() 返回的游標完全一樣。

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

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

在索引上也能創(chuàng)建一個特殊的只返回每條記錄主鍵的游標,那就要調(diào)用 openKeyCursor() 方法。其中 event.result.key 中保存著索引鍵,而 event.result.value 中保存的則是主鍵

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

request.onsuccess = function(event) {
  // 處理成功
  console.log(event.result)
}

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

  var store = db.transaction('users', 'readwrite').objectStore('users'),
      index = store.index('id'),
      request = index.openKeyCursor('007')

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

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

  var store = db.transaction('users', 'readwrite').objectStore('users'),
      index = store.index('id'),
      request = index.getKey('007')

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

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

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

另外,通過對象存儲對象的 indexName 屬性可以訪問到位該空間建立的所有索引。
根據(jù)存儲的對象建立了哪些索引

var store = db.trasaction('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)
}

在 對象存儲空間上調(diào)用 deleteIndex() 方法 并傳入索引的名字可以刪除索引。

var store = db.transaction('users').objectStore('users')
store.deleteIndex('username')
3.4.8、并發(fā)問題

雖然網(wǎng)頁中的 IndexedDB 提供的是異步 API,但任然存在并發(fā)操作的問題。如果瀏覽器的兩個不同的標簽打開了同一個頁面,那么一個頁面視圖更新另一個頁面尚未準備就緒的數(shù)據(jù)庫的問題就有可能發(fā)生。把數(shù)據(jù)庫設(shè)置為新版本有可能導(dǎo)致這個問題。因此,只有當瀏覽器中僅有一個標簽頁使用數(shù)據(jù)庫的情況下,調(diào)用 setVersion() 才能完成操作。

剛打開數(shù)據(jù)庫時,要記著指定 onversionchange 事件處理程序,當同一個來源的另一個標簽頁調(diào)用 setVersion() 時,就會執(zhí)行這個回調(diào)函數(shù)。處理這個事件的最佳方式是關(guān)閉數(shù)據(jù)庫,從而保證版本更新順利完成。

var request, database

request = indexedDB.open('admin')
request.onsuccess = function(event) {
  database = event.target.result

  database.onversionchange = function() {
    database.close() // 關(guān)閉數(shù)據(jù)庫
  }
}

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

var request = database.setVersion('2.0')

request.onblocked = function() {
  alert('Please c;pse all other tabs any try again.')
}
request.onsuccess = function(event) {
  // 處理成功...
}

請記住,其他標簽頁中的 onversionchange 事件處理程序也會執(zhí)行

3.4.9、限制

對 IndexedDB 的限制 很多斗魚 對 Web Storage 的類似。

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

四、小結(jié)

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

客戶端存儲

  • cookie 是一小塊可以客戶端設(shè)置 也可以在 服務(wù)器端設(shè)置的信息,每次請求時都會傳送它。
  • 在JavaScript中 通過 document.cookie 進行訪問 cookie
  • cookie 的限制使其可以存儲少量數(shù)據(jù),對于大量數(shù)據(jù)效率很低。

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

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

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

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

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