HTML5 Web 存储的本质
Web Storage 是一种全新的存储 HTTP 状态数据机制,它在 HTML5 中被引入以取代 cookie。我将深入探讨其客户端存储机制以及与 cookie 相比的优势,同时描述 JavaScript 如何操作它以及浏览器如何存储 localStorage。
背景
Web Storage (也称为 DOM Storage) 带来了两个全新的对象:sessionStorage
和 localStorage
。在 Web Storage 规范中,Ian Hickson 曾指出 Web Storage 旨在解决 Cookies“处理不当”的两种情况。它们是:
- 第一种是为用户正在进行单次事务的场景设计的,但用户可能同时在不同窗口进行多次事务。
- 第二种存储机制是为跨多个窗口且持续时间超过当前会话的存储而设计的。特别是,Web 应用程序可能希望出于性能原因将用户数据(如整个用户创建的文档或用户的邮箱)存储在客户端,以兆字节计。
sessionStorage
和 localStorage
由 W3 在 HTML5 中引入以解决上述问题,让我们首先回顾一下 Cookie 当前的工作方式。
当前 HTTP 状态存储 – Cookie
机制
HTTP Cookie 是一组键/值对,HTTP Web 服务器要求具体的客户端浏览器(即用户代理 - UA)在客户端硬盘上存储。当 UA 再次向 Web 服务器发出请求时,只要满足以下所有条件,UA 就会将所有 Cookie 数据包含在 HTTP 请求头中:
- Cookie 数据尚未过期。
- 请求的域名与存储 Cookie 时的域名完全相同。
- 域下的请求路径与存储 Cookie 时的路径相同。
一旦 Cookie 过期,浏览器就会将其从本地硬盘删除。
一个常见的 Cookie 示例
- 最终用户在浏览器的导航栏中输入 www.foo.net 并按回车键。
-
www.foo.net 返回以下 HTTP 状态 200 消息(在绿色部分,Web 服务器期望客户端浏览器存储两个 Cookie 键/值对:
WayneKey1=value1; WayneKey2=Value2
,它们可从整个域访问,并且将在 1 年后过期)。HTTP/1.1 200 OK Content-type: text/html Set-Cookie: WayneKey1=value1;WayneKey2=Value2;domain=www.foo.net; expires=Tue, 24-Feb-2012 07:13:22 GMT; path=/; HttpOnly (Response HTTP body)
-
浏览器接收到 HTTP 响应,并将 Cookie 键/值以明文形式存储在用户的硬盘上。
-
用户进行了一些交互并再次请求 www.foo.net,浏览器会将 Cookie 数据包含在请求 HTTP 头中。
GET /Index.aspx HTTP 1.1 HOST: www.foo.net Accept: *.* Cookie: WayneKey1=value1;WayneKey2=Value2; (Request HTTP body)
注意:在上面的 #4 中,如果客户端浏览器不支持 Cookie 或用户禁用了 Cookie,HTTP Web 服务器可能会实现一个后备机制,通常可以将 Cookie 数据存储在 URL 中(例如,ASP.NET 可通过在web.config 中设置
cookieless="UseUri"
来支持在 URL 中存储 Cookie,参考:无 Cookie 的 ASP.NET),或者,Web 服务器将不允许客户端使用其功能。
基于服务器分配的、存储在客户端 Cookie 中的会话 ID,会话对象被广泛用于所有流行的 Web 服务器以在服务器端存储临时数据(可以是内存、数据库或专用的会话存储服务器等),万一客户端禁用了 Cookie,Web 服务器可能会采用上述相同的策略(在 URL 中存储 Cookie),或者,它们不允许客户端使用其功能,除非客户端启用了 Cookie。
情况
如今,无数网站(例如 Google、Facebook、Amazon、New York Times)依赖 HTTP Cookie 来存储用户偏好、登录信息、购物车等数据。如果您在浏览器中禁用 Cookie 并尝试访问如今许多极受欢迎的 Web 应用程序(如 Facebook、Twitter、Gmail、Amazon 等),您可能会看到以下截图。
一句话说,启用 Cookie 是使用这些 Web 巨头应用的强制性条件。
Cookie 的缺点
我调查并总结了 Cookie 的一些缺点如下(包含开始时提到的两个 W3C 要点):
- 大小和数量限制
大多数浏览器将 Cookie 大小限制为 4096 字节,每个域最多 300 个 Cookie。IETF 推荐的限制标准,参考:http://www.ietf.org/rfc/rfc2109.txt 第 6.3 节。 - 性能损耗
如果网站使用 Cookie,那么服务器/浏览器之间的每个 HTTP 请求/响应都必须包含所有 Cookie 键/值对在 HTTP 头中。 - 安全
Cookie 以明文形式存储在用户的本地硬盘上。如果开发人员没有妥善处理,则可能发生 Cookie/会话劫持。 - 能够为同一个 Web 应用程序的多个实例分别存储数据
Cookie 难以处理一个用例:同一 Web 应用程序的不同实例在不同窗口中运行而不相互干扰(sessionStorage
将很好地支持这一点,我将在本文后面介绍)。
Web Storage 机制
与在服务器/客户端之间传递并可被双方访问的 cookie 不同,sessionStorage
/ localStorage
是完全由具体的浏览器在客户端存储的。sessionStorage
临时存储一个 HTTP 会话中的数据,localStorage
将永久数据存储到客户端硬盘。优势显而易见:
- 数据不会通过 HTTP 请求/响应传输,从而节省带宽。
- 没有 4KB 的限制,网站在客户端存储大量数据时具有更大的灵活性。
注意:W3C “建议每个域的
localStorage
大小限制为 5MB”,并“欢迎反馈”,这比 Cookie 的 4KB 限制要大得多。 - 既然数据不通过网络传输,那么安全性会相对提高。
- 从 #3 进一步考虑,理论上,许多现有的 HTTPs 连接可以通过采用 Web Storage 来使用纯 HTTP,因为无需加密数据,数据存储在客户端。由于 HTTPs 的性能通常只有 HTTP 的 10%,最终要么性能得到提升,要么成本降低(采购更便宜的服务器硬件)。
在我看来,Web Storage 是HTML5 中最伟大的功能之一,我相信它将在不久的将来引发 HTTP Web 状态存储的革命!为什么?考虑到下面列出的几个场景,我猜它们很可能很快就会发生!
- 节省带宽。您拥有一个流行的电子邮件服务 Web 应用程序的电子邮件账户,并且您每天都使用它。在第一次访问某个具体浏览器时,它会将您 300 多个联系人列表以及您最新的 20 封邮件线程(第一页)存储到您浏览器的本地存储中,数据总共约 8KB。将来访问时,这 8KB 将不会从电子邮件服务器传输。
- 无下载时间 = 更好的用户体验。您喜欢观看一个流行的视频网站的视频,它会记录您观看的视频列表(默认 10 个最新观看的视频)以及索引的真实视频流数据。每次访问该网站时,您都可以查看您的观看历史记录,最重要的是,您可以一秒钟内重新观看列表中的任何视频 – 从您的本地存储。
- 同时使用多个帐户浏览相同的网站。出于某种原因,您在一个网站上注册了两个帐户,该网站使用会话存储,您可以在两个浏览器标签页中使用这两个帐户登录并独立交互。例如,同时在两个标签页中使用两个 Gmail 帐户检查 Gmail。
在 JavaScript 中操作存储
Storage 接口
sessionStorage
/ localStorage
都继承自 Storage 接口,它在 Web Storage 规范中定义。
interface Storage {
readonly attribute unsigned long length;
DOMString key(in unsigned long index);
getter any getItem(in DOMString key);
setter creator void setItem(in DOMString key, in any value);
deleter void removeItem(in DOMString key);
void clear();
};
基本操作
首先,我们需要检查具体的浏览器是否支持 Web Storage。根据 Wikipedia: 布局引擎比较 (HTML5),IE8+(IE8 不支持 Storage 事件,而 IE9 RC 支持)、Firefox 3.5+、Safari 4.0+、Chrome 4.0+都支持 Web Storage。我们可以编写一个简单的 JS 函数来检查客户端浏览器是否支持 Web Storage。
function supportsLocalStorage() {
return ('localStorage' in window) && window['localStorage'] !== null;
}
if(supportsLocalStorage()) {
// Web Storage invocation here
}
else {
alert('Sorry, your browser does not support Web Storage');
}
如果浏览器支持,那么在 JavaScript 中调用 sessionStorage
和 localStorage
非常容易。下面是演示设置/获取/删除/清除存储项的示例代码。
// Set/Get locallStorage
localStorage.{key} = strValue;
var localVal = localStorage.{key};
// Set/Get sessionlStorage
sessionStorage.{key} = strValue;
var sessionVal = sessionStorage.{key};
// Remove an storage item
localStorage.removeItem('key')
sessionStorage.removeItem('key')
// Clear storage
localStorage.clear();
sessionStorage.clear();
请注意一件事:sessionStorage
和 localStorage
都只能存储 DOM字符串值。如果我们想将 JavaScript 对象存储到其中,答案绝对是 JSON字符串。并且 StackOverFlow 上有人提供了一种优雅的方法,通过 JavaScript 原型来扩展 Storage.setObject
/ getObject
方法,代码如下。
Storage.prototype.setObject = function (key, value) {
this.setItem(key, JSON.stringify(value));
}
Storage.prototype.getObject = function (key) {
return this.getItem(key) && JSON.parse(this.getItem(key));
}
// By extending Storage's prototype above, just simply add/get
// JavaScript object into Storage
localStorage.setObject('key',objValue);
localStorage.getObject('key',objValue);
sessionStorage.setObject('key', objValue);
sessionStorage.getObject('key', objValue);
我附加的WebStorageDemo 展示了如何将 DOM 字符串和 JSON 字符串存储到 sessionStorage
/ localStorage
中,代码片段和截图如下。
将几个假的购物车商品存储到 sessionStorage
中
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
function Guid() {
return (S4() + S4() + "-" + S4() + "-" + S4() +
"-" + S4() + "-" + S4() + S4() + S4());
}
function ShoppingCartItem() {
this.ProductID = Guid();
this.ProductName = '';
this.CategoryID = -1;
this.Price = -1.0;
}
// Fake items in shopping cart, and store into sessionStorage
var item1 = new ShoppingCartItem();
item1.ProductName = 'HP WebOS Topaz';
item1.CategoryID = 8;
item1.Price = 849.99;
var item2 = new ShoppingCartItem();
item2.ProductName = 'Apple Ipad II';
item2.CategoryID = 10;
item2.Price = 799.99;
var currentUserShoppingCart = new Array();
currentUserShoppingCart[0] = item1;
currentUserShoppingCart[1] = item2;
sessionStorage.setObject('UserShoppingCart', currentUserShoppingCart);
深入了解 sessionStorage / localStorage 机制
正如我在开头提到的,sessionStorage
/ localStorage
都“完全由具体的浏览器在客户端存储”。它们之间的区别在于 sessionStorage
是窗口/标签页隔离的,而 localStorage
是跨域共享的。换句话说,sessionStorage
负责在发生在一个浏览器窗口/标签页中的 HTTP 会话生命周期内存储临时数据,每个窗口/标签页可以维护自己的会话数据;而 localStorage
将数据持久存储到用户的硬盘中,该具体域下的所有页面都可以操作 localStorage
。
让我们再看一个演示。我添加了一个存储自然数的 Counter
到 sessionStorage
中,并且一个可点击的按钮可以增加 Counter
,代码如下。
<input type="button" value="Counter++" önclick="sessionStorage.Counter++;" />
我打开了该页面的两个实例,分别点击每个页面上的Counter++,然后我们会看到它们各自保留自己的计数器。
相反,存储在 localStorage
中的数据可以被同一域下的所有页面访问。在下面的演示中,最初我将一个假的 UserProfile
插入到 localStorage
中,然后打开一个具有不同路径但属于同一域的另一个页面,我们将看到第一个页面存储的 UserProfile
可以被检索。
注意:既然
localStorage
是共享的,因此它可能会被多个页面同时操作。W3 强调浏览器应实现Storage 互斥锁以确保localStorage
的线程安全。
通过实现这些机制,sessionStorage
/ localStorage
应该能够以更直接、更有效、更安全的方式取代 Cookie。
Storage 事件
W3 定义了 StorageEvent
接口,如下所示。
interface StorageEvent : Event {
readonly attribute DOMString key;
readonly attribute any oldValue;
readonly attribute any newValue;
readonly attribute DOMString url;
readonly attribute Storage storageArea;
void initStorageEvent(in DOMString typeArg, in boolean canBubbleArg,
in boolean cancelableArg, in DOMString keyArg, in any oldValueArg,
in any newValueArg, in DOMString urlArg, in Storage storageAreaArg);
};
大多数著名浏览器(最新版本)都实现了 StorageEvent
,请看下面的截图:
然而,根据我的观察,IE 9 和 Gecko/Webkit 之间的实现略有不同。HTML5Demos 上的Storage 事件演示提示该事件仅在改变存储键的窗口/标签页之外的其他窗口/标签页中捕获。但是,如果您使用 IE 9,您会发现它实际上可以触发。
所以情况是:
- Gecko/Webkit:StorageEvent 仅在改变存储键的窗口/标签页之外的不同窗口/标签页中触发。此外,对现有键值的更改不会触发事件!
- IE 9:StorageEvent 在所有窗口/标签页中触发,并且在值更改时也会触发。
我为了方便起见,在下面的演示中使用了 IE 9。下面的 GIF 演示了当我将一个新对象插入到 localStorage
时,onStorageChanged
函数在两个页面实例上同时触发,并同时显示新插入的数据。
注意:由于localStorage
中存储的数据可以被同一域下的所有页面实例访问,这些页面实例可以同时监视更改(StorageEvent
)。我们可以实现 HTML5 之前难以实现的“实时跨页面数据传递”(另一种实现方式是 WebMessaging,但它更多地用于跨域通信)。
存储位置/格式
让我们更深入一层,了解浏览器如何存储 localStorage
:位置和数据格式。我观察了 Windows 7 上的 IE9 和 Google Chrome,我的结论总结在下表中。
浏览器 | localStorage 位置 |
数据格式 |
IE 9 RC | %userprofile%\AppData\Local\Microsoft\Internet Explorer\DOMStore\ | 纯文本 XML |
Chrome | %userprofile%\AppData\Local\Google\Chrome\User Data\Default\Local Storage | SQLite |
IE 9 实际上将 localStorage
项存储在一个简单的 XML 文件中,下面是我的演示中存储 UserProfile
对象的 XML。
<root>
<item
name="UserProfile"
value="{"NickName":"Wayne","EmailAddress"
:"WebMaster@WayneYe.com"}"
ltime="3439488560"
htime="30135140" />
</root>
Chrome 以一个SQLite 简单数据库表的形式存储数据,该表有两个列:Key 和 Value,截图如下。
演示代码注意事项
请下载 WebStorageDemo.zip,其中包含我上面所有的示例代码。我已将完整的演示放在我的博客上:http://WayneYe.com/Demo/HTML5/WebStorageDemo.htm。
请注意,如果您测试我附加的演示或在 IE 9 中尝试任何与 Web Storage 相关的代码,请确保在 HTML 头部添加了 X-UA-Capability meta 信息,或者在 IE 开发工具中明确将浏览模式/文档模式设置为Internet Explorer 9 标准,否则,由于 IE 6、7 不识别 sessionStorage
/ localStorage
,您可能会看到 JS 错误。
<meta http-equiv="X-UA-Compatible" content="IE=9" />
结论
与 HTTP Cookie 相比,HTML5 Web Storage 机制提供了相对更便捷、更灵活、更安全、更快速的方式来存储 HTTP 状态数据,它应该会逐渐取代 HTTP Cookie。特别是如今大多数流行的浏览器都支持它(尽管可能存在一些问题,例如我提到的 StorageEvent
),它们确实符合 W3C 标准,在 sessionStorage
/ localStorage
中存储数据。Ian Hickson“预计 HTML 5 将在 2012 年达到候选推荐标准 (CR) 阶段”,让我们满怀期待地关注未来。