天天看点

JS学习21(离线应用与客户端储存)离线检测应用缓存数据存储

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

要将描述文件与页面关联起来,可以使用,下面的属性。

同时,有相应的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  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  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的名称和值组成的键值对。

这些是经过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 > -){
            var cookieEnd = document.cookie.indexOf(";", cookieStart);
            if (cookieEnd == -){
                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(), 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值来储存多个名值对。最常见的格式如下:

这样对于获取到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 > -){
            cookieEnd = document.cookie.indexOf(";", cookieStart);
            if (cookieEnd == -){
                cookieEnd = document.cookie.length;
            }
            cookieValue = document.cookie.substring(cookieStart +
                cookieName.length, cookieEnd);
            if (cookieValue.length > ){
                subCookies = cookieValue.split("&");
                for (i=, len=subCookies.length; i < len; i++){
                    parts = subCookies[i].split("=");
                    result[decodeURIComponent(parts[])] =
                        decodeURIComponent(parts[]);
                }
                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 >  && subcookies.hasOwnProperty(subName)){
                subcookieParts.push(encodeURIComponent(subName) + "=" +
                    encodeURIComponent(subcookies[subName]));
            }
        }
        if (subcookieParts.length > ){
            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()).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(), 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=, 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"
};
           

继续阅读