浏览器本地存储方式:Cookie、LocalStorage、SessionStorage

2,100 阅读20分钟

最近面试经常会被问到这个问题,写个博客来总结一下。

浏览器的三种本地存储方式

Cookie

网络早期,服务器无法知道两个请求是否来自同一个浏览器,为了解决这个问题,cookie 就出现了。

特性

  • Cookie 一旦创建成功,名称就无法修改
  • Cookie 是无法跨域名的,也就是说a域名和b域名下的cookie是无法共享的,这也是由Cookie的隐私安全性决定的,这样就能够阻止非法获取其他网站的 Cookie
  • 每个域名下 Cookie 的一般数量不能超过20个,每个 Cookie 的一般大小不能超过4kb
  • 每次发起 HTTP 请求都会携带 Cookie
  • cookie 也可以设置过期的时间,默认是会话结束的时候,当时间到期自动销毁
  • 存在安全问题,如果 Cookie 被拦截了那就可获得 session 的所有信息,即使加密也于事无补,无需知道 cookie 的意义,只要转发 cookie 就能达到目的

跨域共享 Cookie

如果需要域名之间跨域共享 Cookie,有两种方法:

  1. 使用 Nginx 反向代理
  2. 在一个站点登陆之后,往其他网站写 Cookie。服务端的 Session 存储到一个节点,Cookie 存储 sessionId。

如何工作

当网页要发 http 请求时,浏览器会先检查是否有相应的 cookie,有则自动添加在 request header 中的 cookie 字段中。所以对于那种设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。

使用场景

  • 最常见的使用场景就是 Cookie 和 session 结合使用,我们将sessionId存储到Cookie中,每次发请求都会携带这个sessionId,这样服务端就知道是谁发起的请求,从而响应相应的信息。

    文章后半部分有对 session 的介绍

  • 可以用来统计页面的点击次数

cookie值既可以设置,也可以读取。

设置

客户端设置
document.cookie = '名字=值';
document.cookie = 'username=cfangxu;domain=baike.baidu.com'    并且设置了生效域

注意:  客户端可以设置cookie 的下列选项:expires、domain、path、secure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

服务器端设置

不管你是请求一个资源文件(如 html/js/css/图片),还是发送一个ajax请求,服务端都会返回response。而response header中有一项叫set-cookie,是服务端专门用来设置cookie的。

Set-Cookie 消息头是一个字符串,其格式如下(中括号中的部分是可选的):
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]

注意:  一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。 服务端可以设置cookie 的所有选项:expires、domain、path、secure、HttpOnly
通过 Set-Cookie 指定的这些可选项只会在浏览器端使用,而不会被发送至服务器端。

读取

我们通过document.cookie来获取当前网站下的cookie的时候,得到的字符串形式的值,它包含了当前网站下所有的cookie(为避免跨域脚本(xss)攻击,这个方法只能获取非 HttpOnly 类型的cookie)。它会把所有的cookie通过一个分号+空格的形式串联起来,例如username=chenfangxu; job=coding

修改 cookie

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除

把要删除的cookie的过期时间设置成已过去的时间,path/domain/这几个选项一定要旧cookie 保持一样。

注意

如果只设置一个值,那么算cookie中的value; 设置的两个cookie,key值如果设置的相同,下面的也会把上面的覆盖。

cookie的属性(可选项)

过期时间

如果我们想长时间存放一个cookie。需要在设置这个cookie的时候同时给他设置一个过期的时间。如果不设置,cookie默认是临时存储的,当浏览器关闭进程的时候自动销毁

注意:document.cookie = '名称=值;expires=' + GMT(格林威治时间)格式的日期型字符串; 

一般设置天数:new Date().setDate( oDate.getDate() + 5 ); 比当前时间多5天

一个设置cookie时效性的例子

function setCookie(c_name, value, expiredays){
    var exdate=new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie=c_name+ "=" + escape(value) + ((expiredays==null) ? "" : ";expires="+exdate.toGMTString())
}
使用方法:setCookie('username','cfangxu',30)

expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替,两者的作用都是限制cookie 的有效时间。expires的值是一个时间点(cookie失效时刻= expires),而max-age 的值是一个以秒为单位时间段(cookie失效时刻= 创建时刻+ max-age)。
另外,max-age 的默认值是 -1(即有效期为 session );max-age有三种可能值:负数、0、正数。
负数:有效期session;
0:删除cookie;
正数:有效期为创建时刻+ max-age

cookie的域概念(domain选项)

domain指定了 cookie 将要被发送至哪个或哪些域中。默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。

浏览器会把 domain 的值与请求的域名做一个尾部比较(即从字符串的尾部开始比较),并将匹配的 cookie 发送至服务器。

客户端设置

document.cookie = "username=cfangxu;path=/;domain=qq.com"
如上:“www.qq.com" 与 "sports.qq.com" 公用一个关联的域名"qq.com",我们如果想让 "sports.qq.com" 下的cookie被 "www.qq.com" 访问,我们就需要用到 cookie 的domain属性,并且需要把path属性设置为 "/"。

服务端设置

Set-Cookie: username=cfangxu;path=/;domain=qq.com
注:一定的是同域之间的访问,不能把domain的值设置成非主域的域名。

cookie的路径概念(path选项)

cookie 一般都是由于用户访问页面而被创建的,可是并不是只有在创建 cookie 的页面才可以访问这个 cookie。
因为安全方面的考虑,默认情况下,只有与创建 cookie 的页面在同一个目录或子目录下的网页才可以访问。
即path属性可以为服务器特定文档指定cookie,这个属性设置的url且带有这个前缀的url路径都是有效的。

客户端设置

 最常用的例子就是让 cookie 在根目录下,这样不管是哪个子页面创建的 cookie,所有的页面都可以访问到了。

document.cookie = "username=cfangxu; path=/"

服务端设置

Set-Cookie:name=cfangxu; path=/blog

如上设置:path 选项值会与 /blog,/blogrool 等等相匹配;任何以 /blog 开头的选项都是合法的。需要注意的是,只有在 domain 选项核实完毕之后才会对 path 属性进行比较。path 属性的默认值是发送 Set-Cookie 消息头所对应的 URL 中的 path 部分。

domain和path总结:

domain是域名,path是路径,两者加起来就构成了 URL,domain和path一起来限制 cookie 能被哪些 URL 访问。 所以domain和path2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

cookie的安全性(secure选项)

通常 cookie 信息都是使用HTTP连接传递数据,这种传递方式很容易被查看,所以 cookie 存储的信息容易被窃取。假如 cookie 中所传递的内容比较重要,那么就要求使用加密的数据传输。

secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

document.cookie = "username=cfangxu; secure"

把cookie设置为secure,只保证 cookie 与服务器之间的数据传输过程加密,而保存在本地的 cookie文件并不加密。就算设置了secure 属性也并不代表他人不能看到你机器本地保存的 cookie 信息。机密且敏感的信息绝不应该在 cookie 中存储或传输,因为 cookie 的整个机制原本都是不安全的。

注意:如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

httpOnly

这个选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookie带httpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie。

在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。

cookie的编码

cookie其实是个字符串,但这个字符串中等号、分号、空格被当做了特殊符号。所以当cookie的 key 和 value 中含有这3个特殊字符时,需要对其进行额外编码,一般会用escape进行编码,读取时用unescape进行解码;当然也可以用encodeURIComponent/decodeURIComponent或者encodeURI/decodeURI,查看关于编码的介绍

第三方cookie

通常cookie的域和浏览器地址的域匹配,这被称为第一方cookie。那么第三方cookie就是cookie的域和地址栏中的域不匹配,这种cookie通常被用在第三方广告网站。为了跟踪用户的浏览记录,并且根据收集的用户的浏览习惯,给用户推送相关的广告。
关于第三方cookie和cookie的安全问题可以查看mp.weixin.qq.com/s/oOGIuJCpl…

LocalStorage

HTML5新方法,不过IE8及以上浏览器都兼容。

优点

  • 在大小方面,LocalStorage一般大小为5MB,可以储存更多的信息
  • LocalStorage是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在
  • 仅储存在本地,不像Cookie那样每次HTTP请求都会被携带

缺点

  • 存在浏览器兼容问题,IE8以下版本的浏览器不支持
  • 如果浏览器设置为隐私模式,那我们将无法读取到LocalStorage
  • LocalStorage受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问
  • 本质上是对字符串的读取,如果存储内容过多会消耗内存空间,造成页面卡顿

LocalStorage的常用API

// 保存数据到
localStorage localStorage.setItem('key', 'value');

// 从 localStorage
获取数据 let data = localStorage.getItem('key');

// 从 localStorage 删除保存的数据
localStorage.removeItem('key');

// 从 localStorage 删除所有保存的数据
localStorage.clear();

// 获取某个索引的Key
localStorage.key(index)

使用场景

  • 有些网站有换肤的功能,这时候就可以将换肤的信息存储在本地的LocalStorage中,当需要换肤的时候,直接操作LocalStorage即可
  • 在网站中的用户浏览信息也会存储在LocalStorage中,还有网站的一些不常变动的个人信息等也可以存储在本地的LocalStorage中

storage事件

当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。
当storage发生改变的时候触发。
注意:  当前页面对storage的操作会触发其他页面的storage事件
事件的回调函数中有一个参数event,是一个StorageEvent对象,提供了一些实用的属性,如下表:

PropertyTypeDescription
keyStringThe named key that was added, removed, or moddified
oldValueAnyThe previous value(now overwritten), or null if a new item was added
newValueAnyThe new value, or null if an item was added
url/uriStringThe page that called the method that triggered this change

SessionStorage

SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。

SessionStorage与LocalStorage对比:

  • SessionStorage和LocalStorage都在本地进行数据存储
  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享
  • LocalStorage和SessionStorage都不能被爬虫爬取

SessionStorage的常用API:

// 保存数据到 sessionStorage
sessionStorage.setItem('key', 'value');

// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('key');

// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem('key');

// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();

// 获取某个索引的Key
sessionStorage.key(index)

SessionStorage的使用场景:

  • 由于SessionStorage具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了。

Cookie、LocalStorage、SessionStorage 三者区别

  • cookie:其实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存时间由 expires 属性指定(默认是会话结束后失效),并且 cookie 只能被同源的页面访问共享。
  • localStorage:html5 提供的一种浏览器本地存储的方法,它一般也能够存储 5M 或者更大的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会失效,并且 localStorage 也只能被同源页面所访问共享。
  • sessionStorage:html5 提供的一种浏览器本地存储的方法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保存的数据。它一般能够存储 5M 或者更大的数据,它在当前窗口关闭后就失效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享。

上面几种方式都是存储少量数据的时候的存储方式,当需要在本地存储大量数据的时候,我们可以使用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,它内部采用对象仓库的形式存储数据,它更接近 NoSQL 数据库。


Session

什么是Session?Session什么时候产生?

Session:在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

为什么要使用Session?

因为很多第三方可以获取到这个Cookie,服务器无法判断Cookie是不是真实用户发送的,所以Cookie可以伪造,伪造Cookie实现登录进行一些HTTP请求。如果从安全性上来讲,Session比Cookie安全性稍微高一些,我们先要知道一个概念--SessionID。SessionID是什么?客户端第一次请求服务器的时候,服务器会为客户端创建一个Session,并将通过特殊算法算出一个session的ID,下次请求资源时(Session未过期),浏览器会将sessionID(实质是Cookie)放置到请求头中,服务器接收到请求后就得到该请求的SessionID,服务器找到该id的session返还给请求者使用。

Session的生命周期?

根据需求设定,一般来说,半小时。举个例子,你登录一个服务器,服务器返回给你一个sessionID,登录成功之后的半小时之内没有对该服务器进行任何HTTP请求,半小时后你进行一次HTTP请求,会提示你重新登录。

Session有哪些缺陷?

因为Session是存储在服务器当中的,所以Session过多,会对服务器产生压力。在我看来,Session的生命周期算是减少服务器压力的一种方式。

Session 与 Cookie 的区别

Session是基于Cookie实现的,具体做法是把SessionID存在Cookie中,每次服务器收到请求后会根据ID找到相应的Session并返还给请求者。

Cookie是存在浏览器中的文件,Session是存在服务器中的文件。

Session过多会对服务器产生压力,但是Session的生命周期一般都半小时,也是减少服务器压力的一种方式。


其他前端储存方式

web SQL database

先说个会被取代的,为什么会被取代,主要有以下几个原因:

  1. W3C舍弃 Web SQL database草案,而且是在2010年年底,规范不支持了,浏览器厂商已经支持的就支持了,没有支持的也不打算支持了,比如IE和Firefox。
  2. 为什么要舍弃?因为 Web SQL database 本质上是一个关系型数据库,后端可能熟悉,但是前端就有很多不熟悉了,虽然SQL的简单操作不难,但是也得需要学习。
  3. SQL熟悉后,真实操作中还得把你要存储的东西,比如对象,转成SQL语句,也挺麻烦的。

indexedDB

来自MDN的解释: indexedDB 是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。

所以,IndexedDB API是强大的,但对于简单的情况可能看起来太复杂了,所以要看你的业务场景来选择到底是用还是不用。

indexedDB 是一个基于JavaScript的面向对象的数据库。 IndexedDB允许你存储和检索用键索引的对象;

IndexedDB 鼓励使用的基本模式如下所示:

  • 打开数据库并且开始一个事务。
  • 创建一个 object store。
  • 构建一个请求来执行一些数据库操作,像增加或提取数据等。
  • 通过监听正确类型的 DOM 事件以等待操作完成。
  • 在操作结果上进行一些操作(可以在 request 对象中找到)

1、首先打开indexedDB数据库

语法: window.indexedDB.open(dbName, version)

var db;
// 打开数据库,open还有第二个参数版本号
var request = window.indexedDB.open('myTestDatabase');
// 数据库打开成功后
request.onsuccess = function (event) {
    // 存储数据结果,后面所有的数据库操作都离不开它。
    db = request.result;
}
request.onerror = function (event) {
    alert("Why didn't you allow my web app to use IndexedDB?!");
}

// 数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
request.onupgradeneeded = function (event) {

}

onupgradeneeded事件:  更新数据库的 schema,也就是创建或者删除对象存储空间,这个事件将会作为一个允许你处理对象存储空间的 versionchange 事务的一部分被调用。在数据库第一次被打开时或者当指定的版本号高于当前被持久化的数据库的版本号时,这个 versionchange 事务将被创建。onupgradeneeded 是我们唯一可以修改数据库结构的地方。在这里面,我们可以创建和删除对象存储空间以及构建和删除索引。

2、构建数据库

IndexedDB 使用对象存储空间而不是表,并且一个单独的数据库可以包含任意数量的对象存储空间。每当一个值被存储进一个对象存储空间时,它会被和一个键相关联。

  // 数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
  request.onupgradeneeded = function (event) {

      //之前咱们不是在success中得到了db了么,为什么还要在这获取,
      //因为在当前事件函数执行后才会去执行success事件
      var db = event.target.result;

      // 创建一个对象存储空间,keyPath是id,keyGenerator是自增的
      var objectStore = db.createObjectStore('testItem',{keyPath: 'id',autoIncrement: true});
      // 创建一个索引来通过id搜索,id是自增的,不会有重复,所以可以用唯一索引
      objectStore.createIndex('id','id',{unique: true})

      objectStore.createIndex('name','name');
      objectStore.createIndex('age','age');

      //添加一条信息道数据库中
      objectStore.add({name: 'cfangxu', age: '27'});

  }

注意:  执行完后,在调试工具栏Application的indexedDB中也看不到,你得右键刷新一下。

创建索引的语法:

objectStore.createIndex(indexName, keyPath, objectParameters)

indexName:创建的索引名称,可以使用空名称作为索引。
keyPath:索引使用的关键路径,可以使用空的keyPath, 或者keyPath传为数组keyPath也是可以的。
objectParameters:可选参数。常用参数之一是unique,表示该字段值是否唯一,不能重复。例如,本demo中id是不能重复的,于是有设置:

3、添加数据

上面的代码建好了字段,并且添加了一条数据,但是我们如果想在onupgradeneeded事件外面操作,接下来的步骤了。
由于数据库的操作都是基于事务(transaction)来进行,于是,无论是添加编辑还是删除数据库,我们都要先建立一个事务(transaction),然后才能继续下面的操作。
语法: var transaction = db.transaction(dbName, "readwrite");
第一个参数是事务希望跨越的对象存储空间的列表,可以是数组或者字符串。如果你希望事务能够跨越所有的对象存储空间你可以传入一个空数组。如果你没有为第二个参数指定任何内容,你得到的是只读事务。因为这里我们是想要写入所以我们需要传入 "readwrite" 标识。

var timer = setInterval(function () {
    if(db) {
        clearInterval(timer);
        // 新建一个事务
        var transaction = db.transaction(['testItem'], 'readwrite');
        // 打开一个存储对象
        var objectStore = transaction.objectStore('testItem');
        // 添加数据到对象中
        objectStore.add({ name: 'xiaoming', age: '12' });
        objectStore.add({ name: 'xiaolong', age: '20' });
    }
},100)

为什么要用一个间隔定时器?  因为这是一个demo,正常的是要有操作才能进行数据库的写入,在我们的demo中,js执行到transaction会比indexedDB的onsuccess事件回调快,导致会拿到db为undefined,所以写了个间隔定时器等它一会。

4、获取数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.get(1);
getRquest.onsuccess = function (event) {
    console.log(getRquest.result);
}
//输出:{name: "cfangxu", age: "27", id: 1}

5、修改数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.put({ name: 'chenfangxu', age: '27', id:1 });
// 修改了id为1的那条数据

6、删除数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.delete(1);
// 删除了id为1的那条数据

上面的例子执行完后,一定一定要右键刷新indexedDB,它自己是不会变的。


参考文章:很全很全的前端本地存储讲解