JS学习笔记(第23章)(离线应用与客户端存储1)

所谓Web离线应用,就是在设备不能上网的情况下仍然可以运行的应用。开发离线Web应用需要几个步骤:
(1)确保应用知道设备是否能上网;
(2)应用还必须能访问一定的资源(图像、JavaScript、CSS等);
(3)必须有一块本地空间用于保存数据,无论能否上网都不妨碍读写。

1、离线检测

HTML5定义了一个navigator.onLine属性,这个属性值为true表示设备能上网,值为false表示设备离线。
单独使用navigator.onLine属性不能确定网络是否连通。即便如此,在请求发生错误的情况下,检测这个属性仍然是管用的。

if(navigator.onLine) {
    //正常工作
} else {
    //执行离线状态时的任务
}

除了navigator.onLine属性之外,为了更好地确定网络是否可用,HTML5还定义了两个事件:onLine和offline。当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件。这两个事件在window对象上触发。

//从离线变为在线
EventUtil.addHandler(window, "online", function() {
    alert("Online");
});
//从现在变为离线
EventUtil.addHandler(window, "offline", function() {
    alert("Offline");
});

为了检测应用是否离线,在网络加载后,最好先通过navigator.onLine取得初始的状态。然后,就是通过上述两个事件来确定网络连接状态是否变化。当上述事件触发时,navigator.onLine属性的值也会改变。

2、应用缓存

(1)HTML5的应用缓存,或者简称为appache,是专门为开发离线Web应用而设计的。Appcache就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存中保存数据,可使用一个描述文件(manifest file)列出要下载和缓存的资源。

CACHE MANIFEST
  #Comment

  file.js
  file.css

要将描述文件与页面关联起来,可以在<html>中的manifest属性中指定这个文件的路径,例如

<html manifest="/offline.manifest">

以上代码告诉页面,/offline.manifest中包含着描述文件。

(2)虽然应用缓存的意图是确保离线时资源可用,但也有相应的JavaScript API让我们知道它都在做什么,这个API的核心是applicationCache对象,这个对象有一个status属性,属性的值是常量,表示应用缓存的如下当前状态。

  • 0:无缓存,即没有与页面相关的应用缓存
  • 1:闲置,即应用缓存未得到更新
  • 2:检查中,即正在下载描述文件并检查更新
  • 3:下载中,即应用缓存正在下载描述文件中的指定资源
  • 4:更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过swapCache()来使用了
  • 5:废弃,即应用缓存的描述文件已经不存在了,因此页面无法再访问缓存。

(3)应用缓存还有很多相关的事件,表示其状态的改变。以下是这些事件:

  • checking:在浏览器为应用缓存查找更新时触发
  • error:在检查更新或下载资源其期间发生错误时触发
  • noupdate:在检查描述文件发现文件无变化时触发
  • downloading:在开始下载应用缓存资源时触发
  • progress:在文件下载应用缓存的过程中持续不断的触发
  • updateready:在页面新的应用缓存下载完毕且可以通过swapCache()使用时触发
  • cached:在应用缓存完整可用时触发

一般来讲,这些时间会随着页面加载按上述顺序依次触发,不过通过调用update()方法也可以手工干预,让应用缓存为检查更新而触发上述事件。

applicationCache.update();

update()已经一经调用,应用缓存就会去检查描述文件是否更新(触发checking事件),然后就像页面刚刚加载一样,继续执行后续操作。如果触发了cached事件,就说明应用缓存已经准备就绪,不会再发生其他操作了。如果触发了updateready事件,则说明新版本的应用缓存已经可用,而此时你需要调用swapCache()来启用新应用缓存。

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

3、数据存储

3.1 Cookie

HTTP Cookie,通常直接叫做cookie,最初是在客户端用于存储会话信息的。该标准要求服务器对任意HTTP请求发送Set-Cookies HTTP头作为响应的一部分,其中包含会话信息。例如,这种服务器响应头可能如下:

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

这个HTTP响应设置以name为名称,以value为值得一个cookie,名称和值在传送时都必须是URL编码的。浏览器会存储这样的会话信息,并在这之后,通过为每个请求添加Cookie HTTP头将信息发送回服务器。

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

发送回服务器的额外信息可以用于唯一验证客户来自于发送的哪个请求。

1、限制(绑定在特定域名下;数量限制;尺寸限制)

(1)cookie在性质上是绑定在特定的域名下的。当设定了一个cookie后,再给创建它的域名发送请求时,都会包含这个cookie。这个限制确保里存储在cookie中的信息只能让批准的接受者访问,而无法被其他域访问。
(2)每个域的cookie总数总是有限的,当超过单个域名限制之后还要再设置cookie,浏览器就会清除以前设置的cookie。IE和Opera会删除最近最少使用过的cookie,腾出空间给新设置的cookie。Firefox看上去好像是随机决定要清除哪个cookie,所以考虑cookie限制非常重要,以免出现不可预期的后果。
(3)浏览器中对于cookie的尺寸也有限制。尺寸限制影响到一个域下所有的cookie,而并非每个cookie单独限制。如果你尝试创建超过最大尺寸限制的cookie,那么该cookie会被悄无声息地丢掉。

2、cookie的构成

cookie由浏览器保存的一下几块信息构成。(名称、值、域、路径、失效时间、安全标志)

  • 名称:一个唯一确定cookie的名称。cookie名称是不区分大小写的,但是实践中最好将cookie名称看作是区分大小写的。cookie的名称必须是经过URL编码的。
  • 值:存储在cookie中的字符串值。值必须被URL编码。
  • 域:cookie对于哪个域是有效的。所有向该域发送的请求都会包含这个cookie信息。这个值可以包含子域(subdomain,如www.wrox.com),也可以不包含它(如.wrox.com,则对于wrox.com的所有子域都有效)。如果没有明确设定,那么这个域会被认作来自设置cookie的那个域。
  • 路径:对于指定域中的那个路径,应该向服务器发送cookie。例如,你可以指定cookie只有从http://www.wrox.com/books/ 中才能访问,那么http://www.wrox.com 的页面就不会发送cookie信息,即使请求都是来自于同一个域的。
  • 失效时间:表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。默认情况下,浏览器会话结束时即将所有cookie删除;不过也可以自己设置删除时间。这个值是个GMT格式的日期,用于指定应该删除cookie的准确时间。cookie可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则cookie会被立即删除。
  • 安全标志:指定后,cookie只有在使用SSL连接的时候才发送到服务器。例如,cookie信息只能发送给https://www.wrox.com,而 http://www.wrox.com 的请求则不能发送。

每一段信息都作为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

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

这里创建了一个对于所有wrox.com的子域和域名下(由path参数指定的)所有页面都是有效的cookie。因为设置了secure标志,这个cookie只能通过SSL连接才能传输。

尤其要注意,域、路径、失效时间和secure标志都是服务器给浏览器的指示,以指定何时应该发送cookie。这些参数并不会作为发送到服务器的cookie信息的一部分,只有名值对儿才会被发送。

3、JavaScript中的cookie

JavaScript中处理cookie有些复杂,因为BOM的document.cookie属性比较独特,它会因为使用它的不同而表现出不同的行为。

当用来获取属性值时,document.cookie返回当前页面可用的(根据cookie的域、路径、失效时间和安全设置)所有cookie的字符串,一系列由分号隔开的名值对儿。

当用于设置值的时候,document.cookie属性可以设置为一个新的cookie字符串。这个cookie字符串会被解释并添加到现有的cookie集合中。设置document.cookie并不会覆盖cookie,除非设置cookie的名称已经存在。设置cookie的格式如下,和Set-Cookie头中使用的格式一样。

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

这些参数中,只有cookie的名字和值是必需的。如:最好每次设置cookie时都像下面这样使用encodeURI-Component();

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

要给被创建的cookie指定额外的信息,只要将参数追加到该字符串,和Set-Cookie头中的格式一样,如下所示:

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

由于JavaScript中读写cookie不是非常直观,常常需要写一些函数来简化cookie功能。基本的cookie操作有3种:读取、写入和删除。

所有名字和值都是经过URL编码的,所有必须使用decodeURIComponent()来解码

var CookieUtil = {
    get: function(name){
    //查找cookie名加上等于号的位置。如果找到了,那么使用indexOf()查找该位置之后的第一个分号(表示了该cookie的结束位置)。如果没有找到分号,则表示该cookie是字符串中的最后一个,则余下的字符串都是cookie的值。该值使用decodeURIComponent()进行解码并最后返回。如果没有发现cookie,则返回null。
        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;
    },
    //没有删除已有cookie的直接方法。所以,需要使用相同的路径、域和安全选项再次设置cookie,并将失效时间设置为过去的时间。
    unset: function (name, path, domain, secure) {
        this.set(name, "", new Date(0), path, domain, secure);
    }
};

然后就可以像下面这样使用上述方法

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

//读取cookie的值
alert(CookieUtil.get("name")); //"Nicholas"
alert(CookieUtil.get("book")); //"professional JavaScript" 
            
//删除cookie
CookieUtil.unset("name");
CookieUtil.unset("book");

4、子cookie
为了绕开浏览器的单域名下的cookie数限制,一些开发人员使用了一种称为子cookie(subcookie)的概念。子cookie是存放在的那个cookie中的更小段的数据。也就是使用cookie值来存储多个名称值对儿。子cookie最常见的格式如下

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

子cookie一般也以查询字符串否认格式进行格式化。然后这些值可以使用单个cookie进行存储和访问,而非对每个名称-值对儿使用不同的cookie存储。最后网站或者微博应用程序可以无需达到单域名cookie上限也可以存储更加结构化的数据。

为了更好地操作子cookie,必须建立一系列新的方法。子cookie的解析序列和序列化会因子cookie的期望用途而略有不同并更加复杂些。

获取子cookie的方法有两个:get()和getAll()。其中get()获取单个子cookie的值,getAll()获取所有子cookie并将它们放入一个对象中返回,对象的属性为子cookie的名称,对应值为子cookie对应的值。get()方法接收两个参数:cookie的名字和子cookie的名字。它其实就是调用getAll()获取所有的子cookie,然后只返回所需的那一个(如果cookie不存在则返回null)。

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,也有两种方法:set()和setAll().为了在同一个cookie中存储多个子cookie,路径、域和secure标志必须一致;针对整个cookie的失效日期则可以在任何一个单独的子cookie写入的时候同时设置。在这个set()方法中,第一步是获取指定cookie名称对应的所有子cookie。逻辑或操作符"||"用于当getAll()返回null时将subcookies设置为一个新对象。然后,在subcookies对象上设置好子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);

    },
    
    setAll: function(name, subcookies, expires, path, domain, secure){
    
        var cookieText = encodeURIComponent(name) + "=",
            subcookieParts = new Array(),
            subName;
        
        for (subName in subcookies){
            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中的所有子cookie,然后再将余下的子cookie的值保存为cookie的值。

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

整个SubCookieUtil部分代码如下:

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;
    },
    
    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) + "=",
            subcookieParts = new Array(),
            subName;
        
        for (subName in subcookies){
            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;        
    
    },
    
    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);
    }

};

可以像下面这样使用上述方法:

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

 //逐个获取子cookie
 alert(SubCookieUtil.get("data","name")); //"Nicholas"
 alert(SubCookieUtil.get("data","book")); //"Professional JavaScript"

  //设置两个cookie
  SubCookieUtil.set("data", "name", "Nicholas");
  SubCookieUtil.set("data", "book", "Professional JavaScript");

  //设置全部子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"));

5、关于cookie的思考
还有一类cookie被称为“HTTP专有cookie”。HTTP专有cookie可以从浏览器或者服务器设置,但是只能从服务器端读取。由于所有的cookie都会由浏览器作为请求头发送,所以在cookie中存储大量信息会影响到特定域请求性能。cookie信息越大,完成对服务器请求的时间也就越长。尽管浏览器对cookie进行了大小限制,不过最好还是尽可能在cookie中少储存信息,以避免影响性能。

cookie的性质和它的局限使得并不能作为存储大量信息的理想手段。
一定不要在cookie中存储重要和敏感的数据。

3.2 IE用户数据

微软通过一个自定义行为引入了持久化用户数据的概念。要使用持久化用户数据,首先必须如下所示,使用CSS在某个元素上指定userData行为:

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

一旦该元素使用了userDate行为,那么就可以(1)使用setAttribute()方法在上面保存数据了。为了将数据提交到浏览器缓存中,还必须(2)调用save()方法并告诉它要保存到的数据空间的名字。数据空间名字可以完全任意,仅用于区分不同数据集。(3)下一次页面载入之后,可以使用load()方法指定同样的数据空间名称来获取数据。

var dataStore = document.getElementById("dataStore");
dataStore.setAttribute("name", "Nicholas");
dataStore.setAttribute("book","Professional JavaScript");
datastore.save("BookInfo");  //指定了数据空间的名称为BookInfo
dataStore.load("BookInfo");
alert(dataStore.getAttribute("name")); //"Nicholas"
alert(dataStore.getAttribute("book")); //"Professional JavaScript"

对load()的调用获取了BookInfo数据空间中的所有信息,并且使数据可以通过元素访问;只有到载入确切完成之后数据方能使用。如果getAttribute()调用了不存在的名称或者是尚未载入的名称,则返回null。

通过removeAttribute()方法明确指定要删除某元素数据,只要指定属性名称。删除之后,必须像下面这样再次调用save()来提交更改。

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

和cookie一样,IE用户数据并非安全的,所以不能存放敏感信息。

3.3 Web存储机制

3.4 IndexedDB

相关推荐