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