随着移动网络的发展与演化,我们手机上现在除了有原生 App,还能跑“WebApp”——它即开即用,用完即走。一个优秀的 WebApp 甚至可以拥有和原生 App 媲美的功能和体验。WebApp 优异的性能表现,有一部分原因要归功于浏览器存储技术的提升。cookie存储数据的功能已经很难满足开发所需,逐渐被Web Storage、IndexedDB所取代,本文将介绍这几种存储方式的差异和优缺点。
cookie 的本职工作并非本地存储,而是“维持状态”。因为HTTP协议是无状态的,HTTP协议自身不对请求和响应之间的通信状态进行保存,通俗来说,服务器不知道用户上一次做了什么,这严重阻碍了交互式Web应用程序的实现。在典型的网上购物场景中,用户浏览了几个页面,买了一盒饼干和两瓶饮料。最后结帐时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么,于是就诞生了cookie。它就是用来绕开HTTP的无状态性的“额外手段”之一。服务器可以设置或读取cookie中包含信息,借此维护用户跟服务器会话中的状态。
在刚才的购物场景中,当用户选购了第一项商品,服务器在向用户发送网页的同时,还发送了一段cookie,记录着那项商品的信息。当用户访问另一个页面,浏览器会把cookie发送给服务器,于是服务器知道他之前选购了什么。用户继续选购饮料,服务器就在原来那段Cookie里追加新的商品信息。结帐时,服务器读取发送来的cookie就行了。
cookie指某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。 cookie是服务端生成,客户端进行维护和存储,存储在内存或者磁盘中。通过cookie,可以让服务器知道请求是来源哪个客户端,就可以进行客户端状态的维护,比如登陆后刷新,请求头就会携带登陆时response header中的Set-Cookie,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状态。
简而言之,cookie 使基于无状态的HTTP协议记录稳定的状态信息成为了可能。
cookie 主要用于以下三个方面:
简单的HTTP请求过程
第一次访问网站的时候,浏览器发出请求,服务器端生成 cookie在响应中通过Set-Cookie头部告知客户端(允许多个Set-Cookie头部传递多个值),客户端得到 cookie后,后续请求都会自动将 cookie头部携带至请求中发送给服务器(见下面例子),另外,cookie的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。
// 一个HTTP响应:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value
这个HTTP响应会设置一个名为"name",值为"value"的cookie。名和值在发送时都会经过URL编码。浏览器会存储这些会话信息,并在之后的每个请求中都会通过HTTP头部cookie再将它们发回服务器,比如:
GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value
cookie在浏览器中是由以下参数构成的:
Domain
,则一般包含子域名。因此,指定 Domain
比省略它的限制要少。但是,当子域需要共享有关用户的信息时,这可能会有所帮助。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。// 例如,设置 Path=/docs,则以下地址都会匹配:
/docs
/docs/Web/
/docs/Web/HTTP
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value
这里创建的cookie对所有wrox.com的子域及该域中的所有页面有效(通过path=/指定)。不过,这个cookie只能在SSL连接上发送,因为设置了secure标志。
要知道,域、路径、过期时间和secure标志用于告诉浏览器什么情况下应该在请求中包含cookie。这些参数并不会随请求发送给服务器,实际发送的只有cookie的名/值对。
一般说来,cookie的生成方式主要有两种,一种是上文提到的在响应中通过Set-Cookie头部告知客户端;另外一种就是在JavaScript中可以通过document.cookie可以读写cookie,如下:
//读取浏览器中的cookie
console.log(document.cookie);
//写入cookie
document.cookie='myname=langlixingzhou;path=/;domain=.baidu.com';
在JavaScript中处理cookie比较麻烦,因为接口过于简单,只有BOM的document.cookie属性。在设置值时,可以通过document.cookie属性设置新的cookie字符串。这个字符串在被解析后会添加到原有cookie中。设置document.cookie不会覆盖之前存在的任何cookie,除非设置了已有的cookie。要为创建的cookie指定额外的信息,只要像Set-Cookie头部一样直接在后面追加相同格式的字符串即可:
document.cookie = encodeURIComponent("name") + "=" +
encodeURIComponent("Nicholas") + "; domain=.wrox.com; path=/";
// 使用encodeURIComponent()对名称和值进行编码
每个cookie的大小为4KB(名字和值都包含在这4KB之内),对于复杂的存储需求来说是不够用的。当 cookie 超过 4KB 时,它将面临被裁切的命运。这样看来,cookie 只能用来存取少量的信息。此外很多浏览器对一个站点的cookie个数也是有限制的(一般来说不超过300个cookie)。
cookie是与特定域绑定的。同一个域名下的所有请求,都会携带 cookie。大家试想,如果我们此刻仅仅是请求一张图片或者一个 CSS 文件,我们也要携带一个 cookie 跑来跑去(关键是 cookie 里存储的信息并不需要),这是一件多么劳民伤财的事情。cookie 虽然小,但随着请求的叠加,这样的不必要的 cookie 带来的开销将是无法想象的。
cookie是用来维护用户信息的,而域名(domain)下所有请求都会携带cookie,但对于静态文件的请求,携带cookie信息根本没有用,此时可以通过CDN(存储静态文件的)的域名和主站的域名分开来解决。
有两种方法可以确保 cookie 被安全发送,并且不会被意外的参与者或脚本访问:Secure
属性和HttpOnly
属性。
标记为 Secure
的 cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,因此可以预防中间人攻击。但即便设置了 Secure
标记,敏感信息也不应该通过 cookie 传输,因为 cookie 有其固有的不安全性,Secure
标记也无法提供确实的安全保障, 例如,可以访问客户端硬盘的人可以读取它。
从 Chrome 52 和 Firefox 52 开始,不安全的站点(http:
)无法使用cookie的 Secure
标记。
JavaScript Document.cookie API 无法访问带有 HttpOnly
属性的cookie;此类 cookie 仅作用于服务器。例如,持久化服务器端会话的 cookie 不需要对 JavaScript 可用,而应具有 HttpOnly
属性。此预防措施有助于缓解跨站点脚本(XSS)攻击。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2019 07:28:00 GMT; Secure; HttpOnly
对cookie的限制及其特性决定了cookie并不是存储大量数据的理想方式,让“专业的人做专业的事情”,Web Storage 出现了。
HTML5中新增了本地存储的解决方案----Web Storage,这样有了Web Storage后,cookie能只做它应该做的事情了—— 作为客户端与服务器交互的通道,保持客户端状态。
Web Storage的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用cookie的问题。Web Storage API包含了两个对象:localStorage和sessionStorage,本质上是映射字符串键和值的对象化。localStorage是永久存储机制,sessionStorage是跨会话的存储机制。这两种浏览器存储API提供了在浏览器中不受页面刷新影响而存储数据的两种方式。
Window 对象的localStorage 和 sessionStorage 属性引用的是 Storage对象。Storage对象用于保存名/值对数据,直至存储空间上限(由浏览器决定)。一般来说,客户端数据的大小限制是按照每个源(协议、域和端口)来设置的,因此每个源有固定大小的数据存储空间。不同浏览器给localStorage和sessionStorage设置了不同的空间限制,但大多数会限制为每个源5MB。
Storage对象定义了如下方法:
Storage 对象中的键值对总是以字符串的形式存储,这意味着数值类型会自动转化为字符串类型。
sessionStorage对象只存储会话数据,这意味着数据只会存储到浏览器关闭。这跟浏览器关闭时会消失的会话cookie类似。存储在sessionStorage中的数据不受页面刷新影响,可以在浏览器崩溃并重启后恢复(取决于浏览器,Firefox和WebKit支持,IE不支持)。
sessionStorage 特别应该注意一点就是,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 数据便无法共享。
localStorage 与 sessionStorage 在 API 方面无异,这里我们以 sessionStorage 为例:
sessionStorage.setItem('user_name', 'juejin')
sessionStorage.getItem('user_name')
sessionStorage.removeItem('user_name')
sessionStorage.clear()
虽然Web Storage存储数据会带来诸多便利,但实际开发中使用它也有不便之处:
基于上面两点,开发过程中会对它进行封装后再调用:
// 碍于文章篇幅,并未将完整代码展示出来
// 想要获取完整的代码,可以加wx:qqlcx55
// 将属性存储在某一模块下
setItem(key,value,module_name){
if (module_name){
let val = this.getItem(module_name);
val[key] = value;
this.setItem(module_name, val);
}else{
let val = this.getStorage();
val[key] = value;
window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(val));
}
},
// 获取某一个模块下面的属性
getItem(key,module_name){
if (module_name){
let val = this.getItem(module_name);
if(val) return val[key];
}
return this.getStorage()[key];
},
getStorage(){
return JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || '{}');
}
localStorage 类似 sessionStorage,但其区别在于:存储在 localStorage 的数据可以长期保留;而当页面会话结束(即当页面被关闭时),存储在 sessionStorage 的数据会被清除 。要访问同一个localStorage对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议。
考虑到 localStorage 的特点之一是持久,有时我们更倾向于用它来存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串:
Base64 格式的图片字符串
有的网站还会用它存储一些不经常更新的 CSS、JS 等静态资源。
我们先来说说两者的共同点,然后再细说下哪些地方有区别:
作用域:localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。不过sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下 生命周期:localStorage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 sessionStorage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。
说到底,Web Storage 是对 Cookie 的拓展,它只能用于存储少量的简单数据。当遇到大规模的、结构复杂的数据时,Web Storage 也爱莫能助了。这时候我们就要清楚我们的终极大 boss——IndexedDB!
Indexed Database API简称IndexedDB,是浏览器中存储结构化数据的一个方案。IndexedDB背后的思想是创造一套API,方便JavaScript对象的存储和获取,同时也支持查询和搜索。
IndexedDB是类似于MySQL或Web SQL Database的数据库。与传统数据库最大的区别在于,IndexedDB使用对象存储而不是表格保存数据。IndexedDB数据库就是在一个公共命名空间下的一组对象存储,类似于NoSQL风格的实现。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。
IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
IndexedDB的设计几乎完全是异步的。为此,大多数操作以请求的形式执行,这些请求会异步执行,产生成功的结果或错误。绝大多数IndexedDB操作要求添加onerror和onsuccess事件处理程序来确定输出。IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 localStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
IndexedDB 的储存空间比 localStorage 大得多,一般来说不少于 250MB,甚至没有上限。
IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
在IndexedDB大部分操作并不是我们常用的调用方法,返回结果的模式,而是请求——响应的模式。
接下来,我们遵循 MDN 推荐的操作模式,通过一个基本的 IndexedDB 使用流程,旨在对 IndexedDB 形成一个感性的认知:
// 后面的回调中,我们可以通过event.target.result拿到数据库实例
let db
// 参数1位数据库名,参数2为版本号
const request = window.indexedDB.open("admin", 1)
// 使用IndexedDB失败时的监听函数
request.onerror = function(event) {
console.log('无法使用IndexedDB')
}
// 成功
request.onsuccess = function(event){
// 此处就可以获取到db实例
db = event.target.result
console.log("你打开了IndexedDB")
}
// onupgradeneeded事件会在初始化数据库/版本发生更新时被调用,我们在它的监听函数中创建object store
request.onupgradeneeded = function(event){
let objectStore
// 如果同名表未被创建过,则新建test表
if (!db.objectStoreNames.contains('test')) {
objectStore = db.createObjectStore('test', { keyPath: 'id' })
}
}
// 创建事务,指定表格名称和读写权限
const transaction = db.transaction(["test"],"readwrite")
// 拿到Object Store对象
const objectStore = transaction.objectStore("test")
// 向表格写入数据
objectStore.add({id: 1, name: 'juejin'})
// 操作成功时的监听函数
transaction.oncomplete = function(event) {
console.log("操作成功")
}
// 操作失败时的监听函数
transaction.onerror = function(event) {
console.log("这里有一个Error")
}
有了这些存储手段,就可以在客户端通过使用JavaScript存储可观的数据。因为这些数据没有加密,所以要注意不能使用它们存储敏感信息。
正是浏览器存储、缓存技术的出现和发展,为我们的前端应用带来了无限的转机。近年来基于存储、缓存技术的第三方库层出不绝,此外还衍生出了 PWA 这样优秀的 Web 应用模型。总结下本文几个核心观点: