Web應(yīng)用與傳統(tǒng)客戶端最大的區(qū)別就是需要連接網(wǎng)絡(luò)弟塞,沒有網(wǎng)絡(luò)整個應(yīng)用就無法運行,這個一直是Web應(yīng)用最大的痛點之一。
HTML5為了解決這個問題添加了對離線應(yīng)用的支持泳叠。開發(fā)離線Web應(yīng)用有幾個關(guān)鍵點。確保應(yīng)用知道設(shè)備是否能上網(wǎng)以便下一步執(zhí)行正確的操作茶宵,然后應(yīng)用還必須能訪問一定的資源危纫。最后必須有一塊本地空間用于保存數(shù)據(jù),無論是否能上網(wǎng)都能讀寫數(shù)據(jù)乌庶。
離線檢測
為檢測設(shè)備是離線還是在線种蝶,HTML5定義了navigator.onLine這個屬性值為true表示設(shè)備可以上網(wǎng)。
還有兩個事件:online和offline瞒大,這兩個事件會在網(wǎng)絡(luò)狀態(tài)變化時在window對象上觸發(fā)螃征。
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顾瞻。
應(yīng)用緩存
application cache泼疑,這是專門為了開發(fā)離線Web應(yīng)用而設(shè)計的,它從瀏覽器緩存中分出來一部分荷荤,想要在這個部分中保存數(shù)據(jù)退渗,可以使用一個描述文件,列出要下載和緩存的資源蕴纳。這里有個簡單的例子:
CACHE MANIFEST
#Comment
file.js
file.css
這里僅簡單的列出了要下載的文件会油。描述文件的選項非常多,想要進一步了解的話給大家一個網(wǎng)址咯古毛。Go offline with application cache
要將描述文件與頁面關(guān)聯(lián)起來翻翩,可以使用都许,下面的屬性。
<html manifest="/offline.manifest">
同時嫂冻,有相應(yīng)的JS API提供給開發(fā)者來獲取其狀態(tài)胶征。
這個API的核心是applicationCache對象,這個對象有一個status屬性桨仿,表示應(yīng)用緩存的狀態(tài):
- 0:無緩存
- 1:閑置睛低,應(yīng)用緩存未更新
- 2:檢查中,正在下載描述文件并檢查更新
- 3:下載中服傍,應(yīng)用緩存正在下載描述文件中指定的資源
- 4:更新完成钱雷,應(yīng)用緩存已經(jīng)更新了資源,且所有資源下載完畢吹零,可以通過swapCache()來使用了
- 5:廢棄罩抗,應(yīng)用緩存的描述文件已經(jīng)不存在了,頁面無法再訪問應(yīng)用緩存
同時針對上面的狀態(tài)也有相應(yīng)的事件:
- checking灿椅,查找更新時觸發(fā)
- error澄暮,檢查更新或下載資源期間發(fā)生錯誤時觸發(fā)
- noupdate,檢查描述文件發(fā)現(xiàn)沒有更新時觸發(fā)
- downloading阱扬,開始下載資源時觸發(fā)
- progress,下載的過程中不斷觸發(fā)
- updateready伸辟,下載完畢且可以使用swapCache()時觸發(fā)
- cached麻惶,應(yīng)用緩存完整可用時觸發(fā)
在頁面剛剛加載時,會自動檢查有沒有描述文件是否更新信夫,并根據(jù)具體情況觸發(fā)上述事件窃蹋。
有用的方法有兩個:
- update(),會去檢查描述文件是否更新静稻,就像頁面剛剛加載那樣警没,并觸發(fā)相應(yīng)的事件
- swapCache(),在新緩存可用時可以調(diào)用這個方法來啟用新應(yīng)用緩存
EventUtil.addHandler(applicationCache, "updateready", function(){
applicationCache.swapCache();
});
數(shù)據(jù)存儲
隨著Web應(yīng)用的出現(xiàn)振湾,也產(chǎn)生了應(yīng)該直接在客戶端上儲存用戶信息能力的要求杀迹,包括用戶的登陸信息,偏好設(shè)置或其他數(shù)據(jù)押搪。最最開始解決這個問題的方案是cookie树酪。
Cookie
Cookie最初是用來在客戶端儲存會話信息的。該標準要求服務(wù)器對任意HTTP請求發(fā)送Set-Cookie HTTP頭作為相應(yīng)的一部分大州,其中包含會話的信息续语。例如:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value
這里就設(shè)置了一個以name為名稱,value為值的一個cookie厦画。
瀏覽器會儲存這樣的會話信息疮茄。并在這之后通過為每一個請求添加Cookie HTTP頭部將信息發(fā)送回服務(wù)器:
GET /index.html HTTP/1.1
Cookie: name=value
Other-header: other-header-value
這個信息對于服務(wù)器來說就可以唯一驗證請求的身份
限制
cookie在性質(zhì)上是綁定在特定域名下的。當設(shè)定了一個cookie,再給創(chuàng)建它的域名發(fā)送請求時都會包含這個cookie力试,而發(fā)向其他域的請求中并不會包含這個cookie徙邻。這個限制保證了cookie只能讓批準的接受者訪問。
每個域的cookie總數(shù)是有限的懂版,各瀏覽器不同鹃栽,最小的規(guī)定一個域有30個cookie,大小一般不超過4095B躯畴。
cookie的構(gòu)成
cookie由瀏覽器保存的一下幾塊信息構(gòu)成:
- 名稱:一個唯一確定cookie的名稱
- 值:儲存在cookie中的字符串值
- 域:這個cookie對哪個域有效民鼓,如果這個域包含子域,那對子域同樣有有效蓬抄。如果設(shè)定是沒有明確指定丰嘉,這個值會被認為是設(shè)置cookie的那個域
- 路徑:用于指定向域中的哪個路徑發(fā)送cookie,例如嚷缭,你可以指定cookie只發(fā)送到www.baidu.com/img饮亏,那再訪問www.baidu.com時就不會發(fā)送cookie。及時它們同域
- 失效時間:cookie應(yīng)該被刪除的時間戳阅爽,默認瀏覽器會話結(jié)束就刪除
- 安全標志:指定后路幸,cookie只有在使用SSL連接時才會發(fā)送到服務(wù)器
設(shè)置時像這樣:
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。
獲取時付翁,這個屬性返回字符串简肴,包括當前頁面可用的(根據(jù)cookie的域,路徑百侧,失效時間等等)所有cookie的名稱和值組成的鍵值對砰识。
name1=value1;name2=value2;name3=value3
這些是經(jīng)過URI編碼的值。要使用decodeURIComponent()解碼佣渴。
設(shè)置時和使用HTTP頭部設(shè)置一樣:
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有單域名下數(shù)量的限制辛润,一些開發(fā)人員使用了一種稱為子cookie的概念膨处,子cookie是存放在單個cookie中更小段的數(shù)據(jù),也就是使用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和相應(yīng)參數(shù)序列化存進cookie里
setAll: function(name, subcookies, expires, path, domain, secure){
var cookieText = encodeURIComponent(name) + "=",
subcookieParts = new Array(),
subName;
for (subName in subcookies){
//這里使用hasOwnProperty來確定不會循環(huán)到原型鏈中其實不是子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的對象中
//再調(diào)用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的限制晦溪,當數(shù)據(jù)需要被嚴格控制在客戶端上時瀑粥,無需持續(xù)的將數(shù)據(jù)發(fā)回到服務(wù)器。其兩個主要目標是:
- 提供一種在cookie之外儲存會話數(shù)據(jù)的途徑
- 提供一種儲存大量可以夸會話存在的數(shù)據(jù)的機制
Storage類型
可以以名值對的方式儲存字符串值三圆,有如下方法:
- clear()
- getItem(name)
- key(index)
- removeItem(name)
- setItem(name, value)
sessionStorage對象
這個對象儲存特定于某個會話的數(shù)據(jù)狞换,這也就意味著這里的數(shù)據(jù)只保存到瀏覽器關(guān)閉避咆。不過刷新頁面時這里的數(shù)據(jù)是可以存住的。存在這里的數(shù)據(jù)只能由最初儲存數(shù)據(jù)的頁面訪問修噪。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對象
這個對象的目的是跨會話存儲數(shù)據(jù)黄琼,但是有域的限制樊销,在儲存數(shù)據(jù)時,首先要指定的就是域:
globalStorage["wrox.com"].name = "Nicholas";
var name = globalStorage["wrox.com"].name;
globalStorage不是Storage的實例globalStorage["wrox.com"]才是哦脏款。
這個域名下的所有子域可以訪問這里的數(shù)據(jù)围苫。
對每個空間訪問的限制是根據(jù)域名,協(xié)議撤师,端口來限制的剂府,同一個域名。使用HTTP訪問就訪問不到HTTPS時存的數(shù)據(jù)剃盾。端口號不同也是一樣腺占。
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對象進行的任何修改都會觸發(fā)storage事件嚎研,這個事件的event有如下屬性:
- domain
- key
- newValue
- oldValue
各瀏覽器對這個事件的支持并不全面
大小限制
各瀏覽器對Storage大小的限制并不相同,不過都是根據(jù)域名來區(qū)分的库倘。
IndexedDB
Indexed Database API,是用來在瀏覽器中保存結(jié)構(gòu)化數(shù)據(jù)的一種數(shù)據(jù)庫论矾。它的思想是創(chuàng)建一套API教翩,方便保存和讀取JavaScript對象,同時還支持查詢及搜索贪壳。
IndexedDB設(shè)計的操作完全是異步進行的饱亿。每次對數(shù)據(jù)庫的操作都會返回一個相應(yīng)的IDBRequest對象的實例來代表這次請求。在這個實例上可以設(shè)置事件闰靴,等待成功或失敗事件被觸發(fā)彪笼,在里面做相應(yīng)的操作。IndexedDB是全局對象蚂且。API不穩(wěn)定配猫,有的瀏覽器為其加了前綴。
var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;
數(shù)據(jù)庫
打開數(shù)據(jù)庫杏死,把數(shù)據(jù)庫名傳入泵肄,存在會打開捆交,不存在會創(chuàng)建并打開。打開的數(shù)據(jù)庫的請求是一個IDBRequest對象腐巢,通過事件來觀察請求是否成功品追。成功就會返回一個IDBDatabase對象。
可以給database設(shè)置一個版本號冯丙,同樣是返回一個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);
}
}
對象儲存空間
建立了與數(shù)據(jù)庫的連接后胃惜,就可以使用對象儲存空間(相當于表泞莉,其中的對象相當于表中的紀錄)。
創(chuàng)建對象儲存空間蛹疯,需要兩個信息戒财,這個空間的名字,以及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.
創(chuàng)建空間需要在打開數(shù)據(jù)庫時返回的IDBRequest上的onupgradeneeded事件中進行,否則會報錯的列吼。這個事件會在新創(chuàng)建數(shù)據(jù)庫或更新數(shù)據(jù)庫版本號時(open()時傳入更高的版本號幽崩,數(shù)據(jù)庫的版本就會自己更新)被觸發(fā)。
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]);
}
};
};
在創(chuàng)建了空間后寞钥,就可以用add()或put()來向其中添加要保存的對象慌申。對于添加唯一標識已經(jīng)存在的對象,add會報錯理郑,put則會更新原有值蹄溉。
事務(wù)
在創(chuàng)建好空間后,就相當于數(shù)據(jù)庫的結(jié)構(gòu)已經(jīng)確定了您炉,在這之后對數(shù)據(jù)的操作就要通過事務(wù)了柒爵。
創(chuàng)建事務(wù)需要指定針對哪個儲存空間以及讀寫模式,可以一下打開多個儲存空間赚爵。
var transaction = db.transaction(["customers"], "readwrite");
取得事務(wù)索引后使用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"
};