Web應用與傳統客戶端最大的區別就是需要連接網絡,沒有網絡整個應用就無法運行,這個一直是Web應用最大的痛點之一。
HTML5為了解決這個問題添加了對離線應用的支持。開發離線Web應用有幾個關鍵點。確保應用知道設備是否能上網以便下一步執行正確的操作,然后應用還必須能訪問一定的資源。最后必須有一塊本地空間用于保存數據,無論是否能上網都能讀寫數據。
離線檢測
為檢測設備是離線還是在線,HTML5定義了navigator.onLine這個屬性值為true表示設備可以上網。
還有兩個事件:online和offline,這兩個事件會在網絡狀態變化時在window對象上觸發。
EventUtil.addHandler(window, "online", function(){
alert("online");
});
EventUtil.addHandler(window, "offline", function(){
alert("Offline");
});
支持離線檢測的瀏覽器有IE6+、navigator.onLine、Firefox3、Safari4、Opera10.6、Chrome、iOS 3.2版Safari、Android 版 WebKit。
應用緩存
application cache,這是專門為了開發離線Web應用而設計的,它從瀏覽器緩存中分出來一部分,想要在這個部分中保存數據,可以使用一個描述文件,列出要下載和緩存的資源。這里有個簡單的例子:
CACHE MANIFEST
#Comment
file.js
file.css
這里僅簡單的列出了要下載的文件。描述文件的選項非常多,想要進一步了解的話給大家一個網址咯。Go offline with application cache
要將描述文件與頁面關聯起來,可以使用,下面的屬性。
<html manifest="/offline.manifest">
同時,有相應的JS API提供給開發者來獲取其狀態。
這個API的核心是applicationCache對象,這個對象有一個status屬性,表示應用緩存的狀態:
- 0:無緩存
- 1:閑置,應用緩存未更新
- 2:檢查中,正在下載描述文件并檢查更新
- 3:下載中,應用緩存正在下載描述文件中指定的資源
- 4:更新完成,應用緩存已經更新了資源,且所有資源下載完畢,可以通過swapCache()來使用了
- 5:廢棄,應用緩存的描述文件已經不存在了,頁面無法再訪問應用緩存
同時針對上面的狀態也有相應的事件:
- checking,查找更新時觸發
- error,檢查更新或下載資源期間發生錯誤時觸發
- noupdate,檢查描述文件發現沒有更新時觸發
- downloading,開始下載資源時觸發
- progress,下載的過程中不斷觸發
- updateready,下載完畢且可以使用swapCache()時觸發
- cached,應用緩存完整可用時觸發
在頁面剛剛加載時,會自動檢查有沒有描述文件是否更新,并根據具體情況觸發上述事件。
有用的方法有兩個:
- update(),會去檢查描述文件是否更新,就像頁面剛剛加載那樣,并觸發相應的事件
- swapCache(),在新緩存可用時可以調用這個方法來啟用新應用緩存
EventUtil.addHandler(applicationCache, "updateready", function(){
applicationCache.swapCache();
});
數據存儲
隨著Web應用的出現,也產生了應該直接在客戶端上儲存用戶信息能力的要求,包括用戶的登陸信息,偏好設置或其他數據。最最開始解決這個問題的方案是cookie。
Cookie
Cookie最初是用來在客戶端儲存會話信息的。該標準要求服務器對任意HTTP請求發送Set-Cookie HTTP頭作為相應的一部分,其中包含會話的信息。例如:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value
這里就設置了一個以name為名稱,value為值的一個cookie。
瀏覽器會儲存這樣的會話信息。并在這之后通過為每一個請求添加Cookie HTTP頭部將信息發送回服務器:
GET /index.html HTTP/1.1
Cookie: name=value
Other-header: other-header-value
這個信息對于服務器來說就可以唯一驗證請求的身份
限制
cookie在性質上是綁定在特定域名下的。當設定了一個cookie,再給創建它的域名發送請求時都會包含這個cookie,而發向其他域的請求中并不會包含這個cookie。這個限制保證了cookie只能讓批準的接受者訪問。
每個域的cookie總數是有限的,各瀏覽器不同,最小的規定一個域有30個cookie,大小一般不超過4095B。
cookie的構成
cookie由瀏覽器保存的一下幾塊信息構成:
- 名稱:一個唯一確定cookie的名稱
- 值:儲存在cookie中的字符串值
- 域:這個cookie對哪個域有效,如果這個域包含子域,那對子域同樣有有效。如果設定是沒有明確指定,這個值會被認為是設置cookie的那個域
- 路徑:用于指定向域中的哪個路徑發送cookie,例如,你可以指定cookie只發送到www.baidu.com/img,那再訪問www.baidu.com時就不會發送cookie。及時它們同域
- 失效時間:cookie應該被刪除的時間戳,默認瀏覽器會話結束就刪除
- 安全標志:指定后,cookie只有在使用SSL連接時才會發送到服務器
設置時像這樣:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com path=/; secure
Other-header: other-header-value
JS中的cookie
JS中訪問cookie的接口是BOM的document.cookie。
獲取時,這個屬性返回字符串,包括當前頁面可用的(根據cookie的域,路徑,失效時間等等)所有cookie的名稱和值組成的鍵值對。
name1=value1;name2=value2;name3=value3
這些是經過URI編碼的值。要使用decodeURIComponent()解碼。
設置時和使用HTTP頭部設置一樣:
document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nicholas") + "; domain=.wrox.com; path=/";
由于在JS中使用cookie不是很直觀,寫個工具類比較好。
var CookieUtil = {
get: function (name){
var cookieName = encodeURIComponent(name) + "=",
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null;
if (cookieStart > -1){
var cookieEnd = document.cookie.indexOf(";", cookieStart);
if (cookieEnd == -1){
cookieEnd = document.cookie.length;
}
cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd));
}
return cookieValue;
},
set: function (name, value, expires, path, domain, secure) {
var cookieText = encodeURIComponent(name) + "=" +
encodeURIComponent(value);
if (expires instanceof Date) {
cookieText += "; expires=" + expires.toGMTString();
}
if (path) {
cookieText += "; path=" + path;
}
if (domain) {
cookieText += "; domain=" + domain;
}
if (secure) {
cookieText += "; secure";
}
document.cookie = cookieText;
},
unset: function (name, path, domain, secure){
this.set(name, "", new Date(0), path, domain, secure);
}
};
CookieUtil.set("book", "Professional JavaScript");
alert(CookieUtil.get("book"));
CookieUtil.unset("book");
alert(CookieUtil.get("book"));
子cookie
因為cookie有單域名下數量的限制,一些開發人員使用了一種稱為子cookie的概念,子cookie是存放在單個cookie中更小段的數據,也就是使用cookie值來儲存多個名值對。最常見的格式如下:
name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5
這樣對于獲取到cookie就又多了一層障礙,為了更好的操縱cookie當然要在來個工具類咯~
var SubCookieUtil = {
//獲取這個name的cookie中所有的子cookie并放入一個對象中
getAll: function(name){
var cookieName = encodeURIComponent(name) + "=",
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null,
cookieEnd,
subCookies,
len,
i,
parts,
result = {};
if (cookieStart > -1){
cookieEnd = document.cookie.indexOf(";", cookieStart);
if (cookieEnd == -1){
cookieEnd = document.cookie.length;
}
cookieValue = document.cookie.substring(cookieStart +
cookieName.length, cookieEnd);
if (cookieValue.length > 0){
subCookies = cookieValue.split("&");
for (i=0, len=subCookies.length; i < len; i++){
parts = subCookies[i].split("=");
result[decodeURIComponent(parts[0])] =
decodeURIComponent(parts[1]);
}
return result;
}
}
return null;
},
//使用getAll方法返回的子cookie的對象,找到想要的子cookie
get: function (name, subName){
var subCookies = this.getAll(name);
if (subCookies){
return subCookies[subName];
} else {
return null;
}
},
//將所有子cookie和相應參數序列化存進cookie里
setAll: function(name, subcookies, expires, path, domain, secure){
var cookieText = encodeURIComponent(name) + "=",
subcookieParts = new Array(),
subName;
for (subName in subcookies){
//這里使用hasOwnProperty來確定不會循環到原型鏈中其實不是子cookie的屬性
if (subName.length > 0 && subcookies.hasOwnProperty(subName)){
subcookieParts.push(encodeURIComponent(subName) + "=" +
encodeURIComponent(subcookies[subName]));
}
}
if (subcookieParts.length > 0){
cookieText += subcookieParts.join("&");
if (expires instanceof Date) {
cookieText += "; expires=" + expires.toGMTString();
}
if (path) {
cookieText += "; path=" + path;
}
if (domain) {
cookieText += "; domain=" + domain;
}
if (secure) {
cookieText += "; secure";
}
} else {
cookieText += "; expires=" + (new Date(0)).toGMTString();
}
document.cookie = cookieText;
},
//更改一個子cookie,這個方法會先找到這個子cookie所在的cookie中所有的子cookie,將這個新子cookie放到存著所有子cookie的對象中
//再調用setAll方法保存
set: function (name, subName, value, expires, path, domain, secure) {
var subcookies = this.getAll(name) || {};
subcookies[subName] = value;
this.setAll(name,subcookies,expires, path, domain, secure);
},
unset: function (name, subName, path, domain, secure){
var subcookies = this.getAll(name);
if (subcookies){
delete subcookies[subName];
this.setAll(name, subcookies, null, path, domain, secure);
}
},
unsetAll: function(name, path, domain, secure){
this.setAll(name, null, new Date(0), path, domain, secure);
}
};
document.cookie="data=name=Nicholas&book=Professional%20JavaScript";
var data = SubCookieUtil.getAll("data");
alert(data);
alert(data.name); //"Nicholas"
alert(data.book); //"Professional JavaScript"
alert(SubCookieUtil.get("data", "name")); //"Nicholas"
alert(SubCookieUtil.get("data", "book"));
alert(SubCookieUtil.get("data", "class"));
SubCookieUtil.set("data", "class", "how to kill people");
alert(SubCookieUtil.get("data", "class"));
JS不能訪問的cookie
為了保證cookie的安全,有的cookie會不允許JS獲取
Web儲存機制
Web Storage的目標是克服cookie的限制,當數據需要被嚴格控制在客戶端上時,無需持續的將數據發回到服務器。其兩個主要目標是:
- 提供一種在cookie之外儲存會話數據的途徑
- 提供一種儲存大量可以夸會話存在的數據的機制
Storage類型
可以以名值對的方式儲存字符串值,有如下方法:
- clear()
- getItem(name)
- key(index)
- removeItem(name)
- setItem(name, value)
sessionStorage對象
這個對象儲存特定于某個會話的數據,這也就意味著這里的數據只保存到瀏覽器關閉。不過刷新頁面時這里的數據是可以存住的。存在這里的數據只能由最初儲存數據的頁面訪問。sessionStorage 其實是Storage的一個實例,所以上面的方法同樣可用。
sessionStorage.setItem("name", "Nicholas");
sessionStorage.book = "Professional JavaScript";
for (var i=0, len = sessionStorage.length; i < len; i++){
var key = sessionStorage.key(i);
var value = sessionStorage.getItem(key);
alert(key + "=" + value);
}
sessionStorage.removeItem("book");
globalStorage對象
這個對象的目的是跨會話存儲數據,但是有域的限制,在儲存數據時,首先要指定的就是域:
globalStorage["wrox.com"].name = "Nicholas";
var name = globalStorage["wrox.com"].name;
globalStorage不是Storage的實例globalStorage["wrox.com"]才是哦。
這個域名下的所有子域可以訪問這里的數據。
對每個空間訪問的限制是根據域名,協議,端口來限制的,同一個域名。使用HTTP訪問就訪問不到HTTPS時存的數據。端口號不同也是一樣。
globalStorage[location.host ].name = "Nicholas";
globalStorage[location.host ].book = "Professional JavaScript";
globalStorage[location.host ].removeItem("name");
var book = globalStorage[location.host ].getItem("book");
localStorage對象
這個對象是為了取代globalStorage而存在的。這個也是跨會話的,不需要指定域名,只有完全相同的域名才能訪問,子域名都不行。
localStorage.setItem("name", "Nicholas");
localStorage.book = "Professional JavaScript";
var name = localStorage.getItem("name");
var book = localStorage.book;
alert(name);
Storage事件
對storage對象進行的任何修改都會觸發storage事件,這個事件的event有如下屬性:
- domain
- key
- newValue
- oldValue
各瀏覽器對這個事件的支持并不全面
大小限制
各瀏覽器對Storage大小的限制并不相同,不過都是根據域名來區分的。
IndexedDB
Indexed Database API,是用來在瀏覽器中保存結構化數據的一種數據庫。它的思想是創建一套API,方便保存和讀取JavaScript對象,同時還支持查詢及搜索。
IndexedDB設計的操作完全是異步進行的。每次對數據庫的操作都會返回一個相應的IDBRequest對象的實例來代表這次請求。在這個實例上可以設置事件,等待成功或失敗事件被觸發,在里面做相應的操作。IndexedDB是全局對象。API不穩定,有的瀏覽器為其加了前綴。
var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;
數據庫
打開數據庫,把數據庫名傳入,存在會打開,不存在會創建并打開。打開的數據庫的請求是一個IDBRequest對象,通過事件來觀察請求是否成功。成功就會返回一個IDBDatabase對象。
可以給database設置一個版本號,同樣是返回一個IDBRequest,同樣的操作模式。
var request, database;
request = indexedDB.open("admin");
request.onerror = function(event){
alert("Something bad happened while trying to open: " +
event.target.errorCode);
};
request.onsuccess = function(event){
database = event.target.result;
setVersion();
};
function setVersion() {
if (database.version != "1.0"){
request = database.setVersion("1.0");
request.onerror = function(event){
alert("Something bad happened while trying to set version: " +
event.target.errorCode);
};
request.onsuccess = function(event){
alert("Database initialization complete. Database name: " + database.name + ", Version: " + database.version);
};
} else {
alert("Database already initialized. Database name: " + database.name + ", Version: " + database.version);
}
}
對象儲存空間
建立了與數據庫的連接后,就可以使用對象儲存空間(相當于表,其中的對象相當于表中的紀錄)。
創建對象儲存空間,需要兩個信息,這個空間的名字,以及Key Path和Key Generator,這兩個值確定了這個空間中儲存的每個記錄以什么來標識。
- No No:This object store can hold any kind of value, even primitive values like numbers and strings. You must supply a separate key argument whenever you want to add a new value.
- Yes No:This object store can only hold JavaScript objects. The objects must have a property with the same name as the key path.
- No Yes:This object store can hold any kind of value. The key is generated for you automatically, or you can supply a separate key argument if you want to use a specific key.
- Yes Yes:This object store can only hold JavaScript objects. Usually a key is generated and the value of the generated key is stored in the object in a property with the same name as the key path. However, if such a property already exists, the value of that property is used as key rather than generating a new key.
創建空間需要在打開數據庫時返回的IDBRequest上的onupgradeneeded事件中進行,否則會報錯的。這個事件會在新創建數據庫或更新數據庫版本號時(open()時傳入更高的版本號,數據庫的版本就會自己更新)被觸發。
request.onupgradeneeded = function (event) {
var db = event.target.result;
// Create an objectStore to hold information about our customers. We're
// going to use "ssn" as our key path because it's guaranteed to be
// unique - or at least that's what I was told during the kickoff meeting.
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
// Create an index to search customers by name. We may have duplicates
// so we can't use a unique index.
objectStore.createIndex("name", "name", { unique: false });
// Create an index to search customers by email. We want to ensure that
// no two customers have the same email, so use a unique index.
objectStore.createIndex("email", "email", { unique: true });
// Use transaction oncomplete to make sure the objectStore creation is
// finished before adding data into it.
objectStore.transaction.oncomplete = function(event) {
// Store values in the newly created objectStore.
var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
for (var i in customerData) {
customerObjectStore.add(customerData[i]);
}
};
};
在創建了空間后,就可以用add()或put()來向其中添加要保存的對象。對于添加唯一標識已經存在的對象,add會報錯,put則會更新原有值。
事務
在創建好空間后,就相當于數據庫的結構已經確定了,在這之后對數據的操作就要通過事務了。
創建事務需要指定針對哪個儲存空間以及讀寫模式,可以一下打開多個儲存空間。
var transaction = db.transaction(["customers"], "readwrite");
取得事務索引后使用objectStore()訪問儲存空間,然后就可以使用add()、put()、get()、delete()、clear()。這五個方法都會返回一個請求對象,通過事件來操作
var request = db.transaction("users").objectStore("users").get("007");
request.onerror = function(event){
alert("Did not get the object!");
};
request.onsuccess = function(event){
var result = event.target.result;
alert(result.firstName); //"James"
};