JS学习21(离线应用与客户端储存)

作者: exialym | 来源:发表于2016-06-01 23:15 被阅读151次

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

    相关文章

      网友评论

        本文标题:JS学习21(离线应用与客户端储存)

        本文链接:https://www.haomeiwen.com/subject/wuohdttx.html