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
要将描述文件与页面关联起来,可以使用,下面的属性。

<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"
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,561评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,218评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,162评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,470评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,550评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,806评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,951评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,712评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,166评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,510评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,643评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,306评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,930评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,745评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,983评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,351评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,509评论 2 348

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,213评论 11 349
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 村上春树的文字,让人上瘾。 他的小说里,音乐和性是不可或缺的两大元素。音乐高雅又不失稳重,引领我们体会与音乐相伴的...
    婷你说阅读 1,633评论 0 1
  • 2017年11月21号星期二哈尔滨晴-20°~-5° 学习是一种习惯,干什么都是一种习惯,从十月一号回到家里就不开...
    c0deb784fc0b阅读 234评论 0 0