浏览器存储那些事
# 前言
随着移动网络的发展与演化,我们手机上现在除了有原生 App,还能跑“WebApp”——它即开即用,用完即走。一个优秀的 WebApp 甚至可以拥有和原生 App 媲美的功能和体验。WebApp 优异的性能表现,有一部分原因要归功于浏览器存储技术的提升。cookie 存储数据的功能已经很难满足开发所需,逐渐被 Web Storage、IndexedDB 所取代,本文将介绍这几种存储方式的差异和优缺点。
# 一、cookie
# 1.cookie 的来源
cookie 的本职工作并非本地存储,而是“维持状态”。因为 HTTP 协议是无状态的,HTTP 协议自身不对请求和响应之间的通信状态进行保存,通俗来说,服务器不知道用户上一次做了什么,这严重阻碍了交互式 Web 应用程序的实现。在典型的网上购物场景中,用户浏览了几个页面,买了一盒饼干和两瓶饮料。最后结帐时,由于 HTTP 的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么,于是就诞生了 cookie。它就是用来绕开 HTTP 的无状态性的“额外手段”之一。服务器可以设置或读取 cookie 中包含信息,借此维护用户跟服务器会话中的状态。
在刚才的购物场景中,当用户选购了第一项商品,服务器在向用户发送网页的同时,还发送了一段 cookie,记录着那项商品的信息。当用户访问另一个页面,浏览器会把 cookie 发送给服务器,于是服务器知道他之前选购了什么。用户继续选购饮料,服务器就在原来那段 Cookie 里追加新的商品信息。结帐时,服务器读取发送来的 cookie 就行了。
# 2.什么是 cookie
cookie 指某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。 cookie 是服务端生成,客户端进行维护和存储,存储在内存或者磁盘中。通过 cookie,可以让服务器知道请求是来源哪个客户端,就可以进行客户端状态的维护,比如登陆后刷新,请求头就会携带登陆时 response header 中的 Set-Cookie,Web 服务器接到请求时也能读出 cookie 的值,根据 cookie 值的内容就可以判断和恢复一些用户的信息状态。
简而言之,cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
cookie 主要用于以下三个方面:
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等)
# 3.cookie 的原理及其构成
第一次访问网站的时候,浏览器发出请求,服务器端生成 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 在浏览器中是由以下参数构成的:
- name:唯一标识 cookie 的名称。cookie 名不区分大小写,因此 myCookie 和 MyCookie 是同一个名称。不过,实践中最好将 cookie 名当成区分大小写来对待,因为一些服务器软件可能这样对待它们。cookie 名必须经过 URL 编码。
- value:存储在 cookie 里的字符串值。这个值必须经过 URL 编码。
- Domain:cookie 有效的域。发送到这个域的所有请求都会包含对应的 cookie。如果不指定,默认为文档来源(由协议、域名和端口共同定义),不包含子域名。如果指定了
Domain
,则一般包含子域名。因此,指定Domain
比省略它的限制要少。但是,当子域需要共享有关用户的信息时,这可能会有所帮助。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 - Path:请求 URL 中包含这个路径才会把 cookie 发送到服务器。
// 例如,设置 Path=/docs,则以下地址都会匹配:
/docs
/docs/Web/
/docs/Web/HTTP
- Expires/Max-Age:设置 cookie 过期时间(Expires)或有效期(Max-Age)(即什么时间之后就不发送到服务器了)。简单名/值对形式的 cookie 只在当前会话期间存在,用户关闭浏览器就会丢失。如果想让 cookie 的生命周期超过单个浏览对话,那就指定 Expires/Max-Age,max-age 优先级高于 expires。
- Secure:设置之后,只在使用 SSL 安全连接的情况下才会把 cookie 发送到服务器。例如,请求
https://www.wrox.com
会发送 cookie,而请求http://www.wrox.com
则不会。 - HttpOnly:设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS)。
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 的名/值对。
# 4.Javascript 中的 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()对名称和值进行编码
# 5.cookie 的缺陷
- cookie 不够大
每个 cookie 的大小为 4KB(名字和值都包含在这 4KB 之内),对于复杂的存储需求来说是不够用的。当 cookie 超过 4KB 时,它将面临被裁切的命运。这样看来,cookie 只能用来存取少量的信息。此外很多浏览器对一个站点的 cookie 个数也是有限制的(一般来说不超过 300 个 cookie)。
- 过多的 cookie 会带来巨大的性能浪费
cookie 是与特定域绑定的。同一个域名下的所有请求,都会携带 cookie。大家试想,如果我们此刻仅仅是请求一张图片或者一个 CSS 文件,我们也要携带一个 cookie 跑来跑去(关键是 cookie 里存储的信息并不需要),这是一件多么劳民伤财的事情。cookie 虽然小,但随着请求的叠加,这样的不必要的 cookie 带来的开销将是无法想象的。
cookie 是用来维护用户信息的,而域名(domain)下所有请求都会携带 cookie,但对于静态文件的请求,携带 cookie 信息根本没有用,此时可以通过 CDN(存储静态文件的)的域名和主站的域名分开来解决。
- 由于在 HTTP 请求中的 cookie 是明文传递的,所以安全性成问题,除非用 HTTPS。
# 6.cookie 与安全
有两种方法可以确保 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
Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。Web Storage API 包含了两个对象:localStorage 和 sessionStorage,本质上是映射字符串键和值的对象化。localStorage 是永久存储机制,sessionStorage 是跨会话的存储机制。这两种浏览器存储 API 提供了在浏览器中不受页面刷新影响而存储数据的两种方式。
# 1、Storage 对象
Window 对象的 localStorage 和 sessionStorage 属性引用的是 Storage 对象。Storage 对象用于保存名/值对数据,直至存储空间上限(由浏览器决定)。一般来说,客户端数据的大小限制是按照每个源(协议、域和端口)来设置的,因此每个源有固定大小的数据存储空间。不同浏览器给 localStorage 和 sessionStorage 设置了不同的空间限制,但大多数会限制为每个源 5MB。
Storage 对象定义了如下方法:
- clear():删除所有值;不在 Firefox 中实现。
- getItem(name):取得给定 name 的值。
- key(index):取得给定数值位置的名称。
- removeItem(name):删除给定 name 的名/值对。
- setItem(name, value):设置给定 name 的值。
Storage 对象中的键值对总是以字符串的形式存储,这意味着数值类型会自动转化为字符串类型。
# 2、sessionStorage
sessionStorage 对象只存储会话数据,这意味着数据只会存储到浏览器关闭。这跟浏览器关闭时会消失的会话 cookie 类似。存储在 sessionStorage 中的数据不受页面刷新影响,可以在浏览器崩溃并重启后恢复(取决于浏览器,Firefox 和 WebKit 支持,IE 不支持)。
sessionStorage 特别应该注意一点就是,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 数据便无法共享。
localStorage 与 sessionStorage 在 API 方面无异,这里我们以 sessionStorage 为例:
- 存储数据:setItem()
sessionStorage.setItem('user_name', 'juejin')
- 读取数据: getItem()
sessionStorage.getItem('user_name')
- 删除某一键名对应的数据: removeItem()
sessionStorage.removeItem('user_name')
- 清空数据记录:clear()
sessionStorage.clear()
虽然 Web Storage 存储数据会带来诸多便利,但实际开发中 使用它也有不便之处:
- sessionStorage 本身有 API,但是只是简单的 key/value 形式
- sessionStorage 只存储字符串,需要转换成 json 对象
基于上面两点,开发过程中会对它进行封装后再调用:
// 碍于文章篇幅,并未将完整代码展示出来
// 想要获取完整的代码,可以加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) || '{}');
}
# 3、localStorage
localStorage 类似 sessionStorage,但其区别在于:存储在 localStorage 的数据可以长期保留;而当页面会话结束(即当页面被关闭时),存储在 sessionStorage 的数据会被清除 。要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议。
考虑到 localStorage 的特点之一是持久,有时我们更倾向于用它来存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串:
有的网站还会用它存储一些不经常更新的 CSS、JS 等静态资源。
# 4、Web Storage 与 cookie 之间的区别
我们先来说说两者的共同点,然后再细说下哪些地方有区别:
- 共同点:都是保存在浏览器端,且都遵循同源策略。
- 不同点:在于生命周期与作用域的不同
作用域:localStorage 只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份 localStorage 数据。不过 sessionStorage 比 localStorage 更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下
生命周期:localStorage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 sessionStorage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。
说到底,Web Storage 是对 Cookie 的拓展,它只能用于存储少量的简单数据。当遇到大规模的、结构复杂的数据时,Web Storage 也爱莫能助了。这时候我们就要清楚我们的终极大 boss——IndexedDB!
# 四、IndexedDB
Indexed Database API 简称 IndexedDB,是浏览器中存储结构化数据的一个方案。IndexedDB 背后的思想是创造一套 API,方便 JavaScript 对象的存储和获取,同时也支持查询和搜索。
IndexedDB 是类似于 MySQL 或 Web SQL Database 的数据库。与传统数据库最大的区别在于,IndexedDB 使用对象存储而不是表格保存数据。IndexedDB 数据库就是在一个公共命名空间下的一组对象存储,类似于 NoSQL 风格的实现。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。
# 1.IndexedDB 的特点
- 键值对储存。
IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
- 异步
IndexedDB 的设计几乎完全是异步的。为此,大多数操作以请求的形式执行,这些请求会异步执行,产生成功的结果或错误。绝大多数 IndexedDB 操作要求添加 onerror 和 onsuccess 事件处理程序来确定输出。IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 localStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
- 支持事务。
IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 同源限制
IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
- 储存空间大
IndexedDB 的储存空间比 localStorage 大得多,一般来说不少于 250MB,甚至没有上限。
- 支持二进制储存。
IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
# 2.IndexedDB 使用流程
在 IndexedDB 大部分操作并不是我们常用的调用方法,返回结果的模式,而是请求——响应的模式。
接下来,通过一个基本的 IndexedDB 使用流程,旨在对 IndexedDB 形成一个感性的认知:
- 打开/创建一个 IndexedDB 数据库(当该数据库不存在时,open 方法会直接创建一个名为 admin 新数据库)
// 后面的回调中,我们可以通过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')
}
- 创建一个 object store(object store 对标到数据库中的“表”单位)
// 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')
}
# 3.Web Storage、cookie 和 IndexedDB 之间的区别
有了这些存储手段,就可以在客户端通过使用 JavaScript 存储可观的数据。因为这些数据没有加密,所以要注意不能使用它们存储敏感信息。
# 总结
正是浏览器存储、缓存技术的出现和发展,为我们的前端应用带来了无限的转机。近年来基于存储、缓存技术的第三方库层出不绝,此外还衍生出了 PWA 这样优秀的 Web 应用模型。总结下本文几个核心观点:
- Cookie 的本职工作并非本地存储,而是“维持状态”。
- Web Storage 定义了两个对象用于存储数据:sessionStorage 和 localStorage。前者用于严格保存浏览器一次会话期间的数据,因为数据会在浏览器关闭时被删除。后者用于会话之外持久保存数据。
- IndexedDB 是类似于 SQL 数据库的结构化数据存储机制。不同的是,IndexedDB 存储的是对象,而不是数据表。