离线应用与客户端存储

前端开发
2018年12月18日
370

支持离线Web应用开发是HTML5的另一个重点。所谓离线Web应用,就是在设备不能上网的情况下仍然可以运行的应用。

开发离线Web应用需要几个步骤。首先是确保应用知道设备是否能上网,以便下一步执行正确的操作。然后,应用还必须能访问一定的资源(图像、JavaScript、CSS等),只有这样才能正常工作。最后,必须有一块本地空间用于保存数据,无论能否上网都不妨碍读写。

离线检测

开发离线应用的第一步是要知道设备是在线还是离线,HTML5为此定义了一个navigator.onLine属性,这个属性值为true表示能上网,为false表示设备离线。在实际应用中,navigator.onLine在不同浏览器间还有些小的差异:

  • IE6+和Safari5+能够正确检测到网络已断开。
  • Firefox3+和Opera10.6+支持这个属性,但必须手工选中菜单项文件->Web开发人员(设置)->脱机工作才能让浏览器正常工作。
  • Chrome 11 及之前版本始终将这个属性的值设置为true。这个BUG在2011年10月已被修复。

由于存在上述兼容性问题,单独使用navigator.onLine属性不能确定网络是否连通。即使如此,在请求发生错误的情况下,检测这个属性仍然是管用的。

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

除了navigator.onLine属性之外,为了更好地确定网络是否可用,HTML5还定义了两个事件:onlineoffline

js
EventUtil.addHandler(window, 'online', function () { alert('Online'); }); EventUtil.addHandler(window, 'offline', function () { alert('Offline'); });

为了检测应用是否离线,在页面加载后,最好先通过navigator.onLine取得初始的状态。然后再通过上述两个事件来确定网络连接状态是否变化。

支持离线检测的浏览器有IE6+(只支持navigator.onLine属性)、Firefox3、Safari4、Opera10.6、Chrome、IOS3.2版Safari和Android版WebKit。

应用缓存

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

plain-text
CACHE MANIFEST #Comment file.js file.css

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

html
<html manifest="/offline.manifest">

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

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

应用缓存还有很多相关的事件,表示其状态的改变:

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

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

js
applicationCache.update();

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

js
EventUtil.addHandler(applicationCache, 'updateready', function () { application.swapCache(); })

支持HTML5应用缓存的浏览器有Firefox3+、Safari4+、Opera10.6、Chrome、IOS3.2版Safari及Android版WebKit。在Firefox4及之前版本中调用swapCache()会抛出错误。

数据存储

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

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

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

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

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

限制

cookie在性质上是绑定在特定的域名下的。这个限制确保了存储在cookie中的信息只能让批准的接受者访问,而无法被其他域访问。

由于cookie是存在客户端计算机上的,还加了一些限制确保cookie不会被恶意使用,同时不会占用太多磁盘空间。每个域的cookie总数是有限的:

  • IE6及更低版本:每个域名最多20个cookie。
  • IE7及之后版本:每个域名最多50个。
  • Firefox: 50个
  • Opera:30个
  • Safari和Chrome:没有硬性规定。

当超过域名限制数量之后再设置cookie,浏览器就会清除以前设置的cookie。

浏览器中对于cookie的尺寸也有限制。大多数浏览器都有大约4096B的长度限制。为了最佳的浏览器兼容性,最好将整个cookie长度限制在4095B以内。

如果你尝试创建超过最大尺寸限制的cookie,那么该cookie会被悄无声息地丢掉。

cookie的构成

cookie由浏览器保存的以下几块信息构成。

  • 名称:一个唯一确定cookie的名称,不区分大小写且必须是经过URL编码的。
  • :存储在cookie中的字符串值。必须被URL编码。
  • :cookie对哪个域有效。这个值可以包含子域,也可以不包含。如果没有明确设定,那么这个域会被认作来自设置cookie的那个域。
  • 路径:对于指定域中的哪个路径,应该向服务器发送cookie。
  • 失效时间:表示cookie何时应该被删除的时间戳。
  • 安全标志:指定后,cookie只有在使用SSL连接时才发送到服务器。

每一段信息都作为Set-Cookie头的一部分,使用分别加空格分隔每一段:

http
HTTP/1.1 200 OK Content-type: text/html Set-Cookie: name=value; expires=Mon, 22-Jav-07 07:10:24 GMT; domain=.humandetail.com Other-header: other-header-value

JavaScript中的cookie

在JavaScript中处理cookie有些复杂,因为其众所周知的蹩脚的接口,即BOM的document.cookie属性。这个属性的独特之处在于它会因为使用它的方式不同而表现出不同的行为。

当用来获得属性值时,document.cookie返回当前页面可用的所有cookie字符串,一系列由分号隔开的名值对。所有的名字和值都是经过URL编码的,所以必须使用docodeURIComponent()来解码。

当用于设置值的时候,document.cookie属性可以设置一个新的cookie。这个cookie字符串会被解释并添加到现在的cookie集合中。设置document.cookie并不会覆盖cookie,除非名称已存在。

js
document.cookie = 'name=Ni'; document.cookie = encodeURIComponent('name') + '=' + encodeURIComponent('Ni') + '; domain=.humandetail.com; path=/';

由于JavaScript中读写cookie不是非常直观,常常需要写一些函数来简化cookie的功能。

js
var CookieUtil = { get: function (name) { var cookieName = encodeURIComponent(name) + '#', cookieStart = document.cooke.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); } };

子cookie

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

plain-text
name=name1=value1&name2=value2

为了更好地操作子cookie,必须建立一系列方法。

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

Web存储机制

Web Storage最早是在Web超文本应用技术工作组(WHAT-WG)的Web应用1.0规范描述的。这个规范的最初的工作最终成为了HTML5的一部分。

Web Storage的两个主要目标是:

  • 提供一种在cookie之外存储会话数据的途经;
  • 提供一种存储大量可跨会话存在的数据的机制。

最初的Web Storage规范包含了两种对象的含义:sessionStorageglobalStorage。这两个对象在支持的浏览器中都是以window对象属性的形式存在的,支持这两个属性的浏览器包括IE8+、Firefox3.5+、Chrome4+和Opera10.5+。

Storage类型

Storage类型提供最大的存储空间来存储名值对。Storage的实例与其他对象类似,有以下方法:

  • clear():删除所有值,Firefox中没有实现。
  • getItem(name):根据指定的名字获取对应的值。
  • key(index):获得index位置处的值的名字。
  • removeItem(name):删除由name指定的名值对。
  • setItem:为指定的name设置一个对应的值。

还可以使用length属性来判断有多少名值对存放在Storage对象中。

sessionStorage对象

sessionStorage对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。这个对象就会cookie,也会在浏览器关闭后消失。

存储在sessionStorage中的数据可以跨越页面刷新而存在,同时如果浏览器支持,浏览器崩溃并重启之后依然可用。

因为sessionStorage对象绑定于某个服务器会话,所以当文件在本地运行的时候是不可用的。存储在sessionStorage中的数据只能由最初给对象存储数据的页面访问到,所以对多页面应用有限制。

由于sessionStorage对象其实是Storage的一个实例,所以可以使用setItem()或直接设置新的属性来存储数据:

js
// 使用方法 sessionStorage.setItem('name', 'Ni'); // 使用属性 sessionStorage.book = 'bookName';

在IE8可以使用强制把数据写入磁盘:

js
// 仅IE8 sessionStorage.begin(); sessionStorage.name = 'Ni'; sessionStorage.book = 'bookName'; sessionStorage.commit();

sessionStorage中有数据时,可以使用getItem()或者通过直接访问属性名来获得数据。

js
var name = sessionStorage.getItem('name'); var book = sessionStorage.book;

还可以通过结合length属性key()方法来迭代sessionStorage中的值:

js
for (var i = 0, len = sessionStorage.length; i < len; i++) { var key = sessionStorage.key(i); var value = sessionStorage.getItem(key); alert(key + '=' + value); }

也可以使用for-in循环来迭代:

js
for (var key in sessionStorage) { var value = sessionStorage.getItem(key); alert(key + '=' + value); }

要从sessionStorage中删除数据,可以使用delete操作符删除对象属性,也可以调用removeItem()方法:

js
delete sessionStorage.name; sessionStorage.removeItem('book');

sessionStorage主要用于仅针对会话的小段数据的存储。如果需要跨越会话存储数据,那么globalStorage更为合适。

globalStorage对象

Firefox2中实现了globalStorage对象。这个对象的目的是跨越会话存储数据,但有特定的访问限制。

js
// 保存数据 globalStorage['humandetail.com'].name = 'Ni'; // 获取数据 var name = globalStorage['humandetail.com'].name;

在这里,访问的是针对域名humandetail.com的存储空间。globalStorage对象不是Storage的实例,而具体的globalStorage['humandetail.com']才是。这个存储空间对于humandetail.com及其所有子域都是可以访问的。

可以像下面这样指定子域名。

js
// 保存数据 globalStorage['www.humandetail.com'].name = 'Ni'; // 获取数据 var name = globalStorage['www.humandetail.com'].name;

这里所指定的存储空间只能由来自www.humandetail.com的页面访问,其他子域名都不行。

某些浏览器允许更加宽泛的访问限制,比如像下面这样:

js
// 存储数据,任何人都可以访问——不建议这样做。 globalStorage[''].name = 'Ni'; // 存储数据,可以让任何以.net结尾的域名访问——也不建议 globalStorage['.net'].name = 'Ni';

globalStorage的每个属性都是Storage的实例。因此,可以像下面这样使用:

js
// 保存数据 globalStorage['www.humandetail.com'].name = 'Ni'; // 获取数据 var name = globalStorage['www.humandetail.com'].name; // 删除数据 globalStorage['www.humandetail.com'].removeItem('name');

如果你事件不能确定域名,那么使用location.host作为属性名比较安全。

js
// 保存数据 globalStorage[location.host].name = 'Ni'; // 获取数据 var name = globalStorage[location.host].name;

如果不使用removeItem()或者delete删除,或者用户未清除浏览器缓存,存储在globalStorage中的数据会一直保留在磁盘上。

localStorage对象

localStorage对象在修订过的HTML5规范中作为持久保存客户端数据的方案取代了globaleStorage。与globalStorage不同,不能给localStorage指定任何访问规则:规则事件先设定好的。要访问同一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。这相当于globalStorage[location.host]

由于localStorage是Storage的实例,所以可以像使用sessionStorage一样来使用它:

js
// 使用方法存储数据 localStorage.setItem('name', 'Ni'); // 使用属性存储数据 localStorage.book = 'bookName'; // 使用方法读取数据 var book = localStorget.getItem('book'); // 使用属性读取数据 var name = localStorage.name;

存储在localStorage中的数据与存储在globalStorage中的数据一样,都遵循相同的规则:数据保留到通过JavaScript删除或用户清除浏览器缓存。

为了兼容只支持globalStorage的浏览器,可以使用下面的函数:

js
function getLocalStorage () { if (typeof localStorage === 'object') { return localStorage; } else if (typeof globalStorage === 'object') { return globalStorage[location.host]; } else { throw new Error('Local Storage not available.'); } } var storage = getLocalStroge();

storage事件

对Storage对象进行任何修改,都会在文档上触发storage事件。当通过属性或setItem()保存数据,使用deleteremoveItem()删除数据,或者调用clear()时,都会发生该事件。这个事件的事件对象有以下属性:

  • domain:发生变化的存储空间的域名
  • key
  • newValue
  • oldValue

在这四个属性中,IE8和Firefox只实现了domain属性

js
EventUtil.addHandler(document, 'storage', function (event)) { alert(event.domain); }

限制

对于localStronge而言,大多数桌面浏览器会设置每个来源5MB的限制。Chrome和Safari则是2.5MB。而IOS版的Safari和Android版的Webkit也是2.5MB。

对于sessionStorage而言,有些浏览器没有限制,但Chrome、Safari、IOS版的Safari和Android版的Webkit是2.5MB。IE8和Opera是5MB。