web存储指的是在web通信过程中,由客户端(如浏览器)对少量数据进行的本地存储(注:本文所说的是广义的web存储,而不单指Web Storage)。web存储类型主要包括古老的cookie,以及HTML5推出的sessionStorage、localStorage和indexedDB等。
web存储的宿主环境通常是浏览器,借助浏览器提供的JavaScript接口,我们可以实现对这些数据的增删改查。
这要从最古老的的cookie说起。
cookie的诞生源于这样一个矛盾:http协议是无状态的,而服务端却有区分不同客户端的实际需要。
先来理解http协议的无状态性。无状态性指的是,http协议对任意两次数据传输是没有记忆能力的。举个例子,假如某个客户端先向服务端发送了一条消息,随后又向服务端发送了另一条消息,那么服务器无法知道这两条消息来自同一个客户端。
之所以要将http协议设计为无状态的,是因为要维护所有客户端的连接状态对服务端的性能消耗很大,这会严重限制服务器的并发能力。并且在大多数情况下,维护这个状态并不能带来什么收益。所以http协议的设计者索性不对状态进行维护。
但在很多情况下,服务端必须区分不同的客户端。拿淘宝来说,在收到下单请求时,服务器必须知道是哪个用户在下单,这就要求它必须知道当前的下单请求和之前的哪个登录请求来自同一个客户端(否则该客户端每次发请求都必须把自己的账号信息和所有参数带上)。
为此,服务端设计了session(会话)机制。一个用户登录之后,服务端会为该用户创建一个会话对象,存储与该用户相关的参数(如用户名、登录时间等),这个对象直到用户退出登录或登录超时才会被销毁。每个会话对象有一个唯一的id,客户端只要在随后发送的请求中携带上这个id,服务端就能确定是哪个用户发送的请求。
这样,客户端必须有一种存储这个id的机制,来保证其在退出登录前的每个请求中都可以携带这个id。于是,cookie诞生了。
用户登录时,服务端会为该用户创建一个会话(session)对象,然后服务端把会话对象的id写入响应参数中发送给客户端。客户端收到响应后,取出该参数,保存在浏览器内。随后每次发送请求时都携带上这个id,服务端根据这个id,就可以确定该请求来自于哪个用户。这个用于存储id的特殊字段,就称为cookie(当然,cookie不仅仅可以存储session的id,还可以存储其他参数)。
所以说,cookie的基本作用就是携带用户标识,帮助服务端区分不同的客户端。
cookie之后的几类存储:sessionStorage、localStorage和indexedDB出现的原因就很简单了:将更多的会话数据保存在浏览器端,可以减少网络传输,提升网站性能(虽然cookie也可以这样使用,但它的单个容量只有4KB,所以不适合这类用途)。
上面我们已经对cookie的来源进行了详细的介绍,这里要介绍的是cookie的用法及特点。
HTTP Cookie简称cookie,是在HTTP请求发送Set-Cookie HTTP头作为响应的一部分,通过name=value的形式存储的字符串。前端收到该响应后,会将cookie保存在浏览器内,并在每次的用户请求中携带该字符串。
cookie在前端可以通过document.cookie获得,如:
> document.cookie
< "_ga=77911531582348967; _gid=10966846041582958782"
上面的cookie里共存储了两个参数:_ga和_gid,两个参数由一个分号和一个空格隔开。cookie本身是用encodeURIComponent编码的,因此,如果cookie中包含中文,需要用对应的decodeURIComponent解码才能得到对应值。
除了key和value外,每个参数还会携带其他几个属性,包括域、路径、失效时间、安全标志等,这些参数在使用document.cookie输出时无法看到,不过可以在调试工具中查看:
上面几个参数的含义分别是:
一般来说,cookie只用于保存与登录相关的信息,其中最重要的是服务端会话(session)对象的id。它不适合存储大量的数据,因为浏览器限制了每个cookie的大小不能超过4KB。此外,浏览器对单个域下cookie的数量也是有限制的,该数量因浏览器而异,对开发者来说,这个数量一般不要超过20个。
cookie默认会在会话结束时失效,也可以手动修改max-age
或Expires
参数来设置cookie的失效时间,前者是规定cookie在多少秒后失效,而后者则是规定cookie在某个时间点失效。如果同时设置了这两个参数,则max-age优先级更高。
向cookie添加参数很简单:
document.cookie = "name=oeschger";
document.cookie = "favorite=tripe;max-age=120;path=/";
alert(document.cookie);
// 显示: name=oeschger; favorite=tripe
注意,我们为favorite设置的其他参数虽然没有通过document.cookie打印出来,但它们仍然是有效的。比如上面的name会在服务端的session销毁时失效,而favorite则会在120秒后失效。
cookie没有提供直接删除参数的接口,但是可以通过将参数的max-age属性设置为0或者将Expires设置为当前时间,来使其立即失效,以达到删除cookie的目的,如:
document.cookie = "favorite=; max-age=0;";
这样就可以从cookie中删除favorite。
cookie作为客户端存储,有一个很大的优点,就是不受服务端架构的影响。我们知道,在分布式架构下,session很难在各个服务器之间共享。而cookie则完全不存在这个问题,它存储于客户端,可以被发送到任意一台服务器。
同时,cookie也存在几个很明显的缺点。一个是安全问题,cookie中存储了客户端的身份认证信息,如果被窃取,就可能产生不可预估的损失。另一个是上面提到的容量问题,这使得cookie不能广泛用于web存储。此外,cookie对通信带来的性能损耗也是不可忽视的。因为cookie中的所有参数会在每次向后端发送请求时被携带,如果cookie很大,就会导致每次的http请求体积变大,影响网站的响应速度。综上所述,cookie仅用于保存极少量的用户身份认证信息,并且需要通过加密策略保证cookie传输安全。
为什么要把这两个放在一起介绍呢?
因为它们都是继承自Storage
,原理和语法上都有极高的相似性,以至于网上有大量关于两者异同的介绍文章。先来看看它们的相同点:
1.从原理上来说,两者都属于浏览器端存储。前者称为“会话存储”,后者称为“本地存储”。它们都被部署在window对象上,因此可以通过下面的方式访问:
window.localStorage
// 或者直接访问localStorage
window.sessionStorage
// 或者直接访问sessionStorage
2.两者的操作语法是一致的(这里以localStorage为例):
localStorage.setItem("name", "carter"); // 设置name: carter
localStorage.age = "24"; // 设置age: 24
localStorage.getItem("name"); // 获取name的值carter
localStorage.removeItem("name"); // 删除name
localStorage.clear(); // 清空localStorage
sessionStorage的语法同上。
3.两者都遵循同源策略。即该存储只在同一个域下可以共享,跨域无法访问,这样可以保证数据的安全性。
4.localStorage和sessionStorage各自拥有5MB的存储空间,并且只能保存字符串类型的数据。对于非字符串类型的数据,一般需要使用JSON.stringify
方法压缩成字符串,使用时再用JSON.parse
进行解析。
1.两者的失效时间是不同的。
localStorage本身是不会失效的,即使关闭浏览器,下次再访问该网站仍然有效。localStorage没有提供直接设置失效时间的方法,我们需要使用一种特殊的策略来定期清除localStorage:
localStorage.setItem("name", JSON.stringify({
value: "carter",
time: (new Date()).getTime() // 保存时间戳
}))
function getItem(key, maxAge){
let obj = localStorage.getItem(key);
if(obj){
// 获取变量值对象
obj = JSON.parse(obj);
}
// 存储时间小于最大生命周期时才读取该参数,否则将其清除
if((new Date()).getTime() - obj.time < maxAge){
return obj.value;
} else {
localStorage.removeItem(key);
return "";
}
}
getItem("name", 60 * 60 * 1000); // 失效时间为1小时
简单来说,就是把保存localStorage的时间一同存储进去,取值的时候再手动判断是否超时。
但sessionStorage与页面会话是绑定的,当某个页面会话失效时,对应的sessionStorage就会被清除。需要注意的是,sessionStorage只在会话页面失效时才会失效,也就是说刷新页面或者通过某种方式恢复当前页面时(如点击浏览器的回退按钮,或使用浏览器的页面恢复功能),sessionStorage并不会失效,换句话说,当某个页面创建了sessionStorage之后,它就总是与其创建的sessionStorage共存亡。
2. 两者的有效范围不同。
在不跨域的情况下,localStorage可以跨页签生效,而sessionStorage仅在当前页签范围内有效。
这就是说,如果你新打开了一个与当前页面同域的页面,这个新页面会与当前页面共享localStorage,却不能共享sessionStorage。而如果你是在当前页面的iframe内打开的,并且没有跨域,那么localStorage和sessionStorage都是可以共享的。
尽管5M的空间已经相对较大了,但仍然无法满足所有的前端存储需求。
这是因为前端数据缓存对web站点的性能提升是巨大的(它可以有效减少http数据传输量,而这通常是导致网站卡顿的最主要因素),因此越来越多的站点倾向于在前端存储更多的数据。localStorage和sessionStorage无法满足这样的需求,因为一方面,它们的容量只有5M大小;另一方面,由于是非结构化存储,当数据量较大时,它们的操作速度不够快。
为此,HTML5规范推出了前端的事务型数据库indexedDB。它可以存储大量的结构化数据,具有几乎可以媲美后端数据库的读写性能(当然,从功能和容量上远不及后端数据库)。
使用indexedDB大概需要以下几个流程:
var request = window.indexedDB.open(databaseName, version);
这样就可以打开或新建一个indexedDB数据库。当所传入的数据库名不存在时,就会新建一个数据库,否则将打开已有数据库。当省略了数据库版本号时,如果数据库已存在,则默认为当前版本,否则版本号为1
。
调用open方法后,返回的是一个IDBRequest
对象,通过向其注册error
、success
和upgradenedded
事件,可以处理打开数据的结果。
error
表示数据库打开失败:
request.onerror = function(err){
console.log("数据库打开失败:" + err);
}
success
表示数据库打开成功,此时可以执行数据库读操作:
request.onsuccess = function (event) {
let db = request.result;
console.log('数据库打开成功');
};
upgradeneeded
表示数据库需要升级,不同于一般的后端数据库,indexedDB每次修改数据库的内容都必须升级数据库版本:
request.onupgradeneeded = function (event) {
let db = event.target.result;
}
需要注意的是,新建数据库的相关操作本身被归类为升级数据库版本,因此需要在upgradeneeded事件中进行:
request.onupgradeneeded = function (event) {
db = event.target.result;
var objectStore;
if (!db.objectStoreNames.contains('person')) {
objectStore = db.createObjectStore('person', {
keyPath: 'id' });
}
}
由于工作中暂未用到过indexedDB,缺少实际使用经验,因此这里暂不详述。如果感兴趣,请参考阮一峰:浏览器数据库 IndexedDB 入门教程。
我们上面所介绍的四类存储都属于前端存储,除了cookie之外,其他三个都是直接通过JavaScript来访问和操作的。
严格来说,除了cookie外,其他三类存储都不是绝对必要的,那我们为什么还需要前端存储呢?
答案是,减少http数据传输,提升网站性能。
为了便于理解,我们把访问网站的整个过程做一个比喻,它就像我们去图书馆读书一样。图书馆本身就是整个网站,书架就是我们所用的后端数据库,而书架上的书就是保存在数据库中的所有网站数据。
图书馆的图书管理系统就是网站提供的后台服务,它向用户提供办卡、检索、借阅、还书等服务。
前端就像是图书馆内的读书桌,我们可以在读书桌上阅读自己喜欢的图书。而我们所读的书,就是网站为我们提供的静态页面。
那么前端缓存怎么理解呢?
它就像我们放在读书桌手边的待读图书。想象一下,假如我们来图书馆需要查阅十本书,如果我们每次查阅完一本就重新去书架上取下一本,效率显然是很低的。这时候我们会一次把十本书全部取过来放在读书桌上。那么对我们来说,书桌上这十本触手可及的图书就是我们的前端缓存,它们离我们如此近,所以我们不需要重复十次从书架找书的过程。
我们做一个更进一步的比喻,来理解各类存储。
设想一下,现在图书馆引进了一个预约系统,每个读者可以在来图书馆之前向图书馆预约想要查阅的图书,图书馆便可以提前将这几本书准备好,放在某个单独为其预留的预约区。每个预约区就是该用户对应的session(会话)对象,这就是服务端存储session。
那么图书馆是怎么知道这些书分别是谁预定的呢?很简单,每个预约区都对应某个人的读书卡号,凭读书卡号可以取走对应预约区的书,这里你的读书卡号就是cookie。
假如你是该图书馆的长期用户,图书馆专门为你预留了一块读书区,你可以将自己经常阅读的图书放在这里,即使你离开了,图书馆也不会清理这些书。这块长期可用的读书区,就是localStorage。
假如你只是一个普通用户,在来图书馆后临时找了块读书区。那么当你离开图书馆时,图书馆就会把你刚刚借阅的书从读书区清理掉。下次你再来图书馆,就必须重新借阅这些书,这块临时的读书区,就是sessionStorage。
那么什么是indexedDB呢?假如有人需要在图书馆举办一次读书会,参与人数众多,涉及的图书品类各异。为了保证与会人员快速借阅到图书,图书馆决定开辟一块单独的区域,把所有可能用到的图书全部整理好,并为读书会专门设计了一个小型的图书管理系统。由于读书会的某些特点,这块区域图书的摆放和管理方式不同于整个图书馆(解释为什么后端采用关系型数据库,而前端采用事务型数据库),这样可以保证在图书量不大的情况下借阅效率更高。那么这块单独开辟的区域(包括它附属的图书管理系统),就是一个前端数据库indexedDB。
web存储是浏览器为开发者提供的最常用的功能之一,基本上已经完全代替了之前用隐藏表单临时存储数据的方案。
想要掌握各类web存储,不光要学习它们的语法,更要注意它们的生命周期、存储容量、使用场景等。随着前端技术的发展和用户对前端体验要求的不断提高,网站对前端存储的需求必然会越来越大,因此现在不算常用的indexedDB,在未来甚至有可能称为网站的标配。作为前端开发者,应该对它们有一个全面的认识,并在工作中多使用,多总结。