缓存+存储

发布时间 2023-11-17 16:35:19作者: 柯基与佩奇

浏览器存储

一、Cookieb

  1. 什么是 Cookie 及应用场景

Cookie 指某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。 cookie 是服务端生成,客户端进行维护和存储。通过 cookie,可以让服务器知道请求是来源哪个客户端,就可以进行客户端状态的维护,比如登陆后刷新,请求头就会携带登陆时 response header 中的 set-cookie,Web 服务器接到请求时也能读出 cookie 的值,根据 cookie 值的内容就可以判断和恢复一些用户的信息状态。

典型的应用场景有:
记住密码,下次自动登录。
购物车功能。
记录用户浏览数据,进行商品(广告)推荐。

  1. Cookie 的原理及生成方式

第一次访问网站的时候,浏览器发出请求,服务器响应请求后,会在响应头里面添加一个 Set-Cookie 选项,将 cookie 放入到响应请求中,在浏览器第二次发请求的时候,会通过 Cookie 请求头部将 Cookie 信息发送给服务器,服务端会辨别用户身份,另外,Cookie 的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。

Cookie 的生成方式主要有两种:
生成方式一:http response header 中的 set-cookie

可以通过响应头里的 Set-Cookie 指定要存储的 Cookie 值。默认情况下,domain 被设置为设置 Cookie 页面的主机名,也可以手动设置 domain 的值。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2018 07:28:00 GMT;//可以指定一个特定的过期时间(Expires)或有效期(Max-Age)

当 Cookie 的过期时间被设定时,设定的日期和时间只与客户端相关,而不是服务端。

生成方式二:js 中可以通过 document.cookie 可以读写 cookie,以键值对的形式展示
例如在掘金社区控制台输入以下三句代码,便可以在 Chrome 的 Application 面板查看生成的 cookie:
document.cookie="userName=hello"
document.cookie="gender=male"
document.cookie='age=20;domain=.baidu.com'

  1. Cookie 的缺陷

(1)Cookie 不够大
Cookie 的大小限制在 4KB 左右,对于复杂的存储需求来说是不够用的。当 Cookie 超过 4KB 时,它将面临被裁切的命运。这样看来,Cookie 只能用来存取少量的信息。此外很多浏览器对一个站点的 cookie 个数也是有限制的。
这里需注意:各浏览器的 cookie 每一个 name=value 的 value 值大概在 4k,所以 4k 并不是一个域名下所有的 cookie 共享的,而是一个 name 的大小。

(2)过多的 Cookie 会带来巨大的性能浪费
Cookie 是紧跟域名的。同一个域名下的所有请求,都会携带 Cookie。试想,如果此刻仅仅是请求一张图片或者一个 CSS 文件,也要携带一个 Cookie 跑来跑去(关键是 Cookie 里存储的信息并不需要),这是一件多么劳民伤财的事情。Cookie 虽然小,请求却可以有很多,随着请求的叠加,这样的不必要的 Cookie 带来的开销将是无法想象的。

cookie 是用来维护用户信息的,而域名(domain)下所有请求都会携带 cookie,但对于静态文件的请求,携带 cookie 信息根本没有用,此时可以通过 cdn(存储静态文件的)的域名和主站的域名分开来解决。

(3)由于在 HTTP 请求中的 Cookie 是明文传递的,所以安全性成问题,除非用 HTTPS。

二、LocalStorage

  1. LocalStorage 的特点

保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。
大小为 5M 左右
仅在客户端使用,不和服务端进行通信
接口封装较好
基于上面的特点,LocalStorage 可以作为浏览器本地缓存方案,用来提升网页首屏渲染速度(根据第一请求返回时,将一些不变信息直接存储在本地)。

  1. 存入/读取数据

localStorage 保存的数据,以“键值对”的形式存在。也就是说,每一项数据都有一个键名和对应的值。所有的数据都是以文本格式保存。
存入数据使用 setItem 方法。它接受两个参数,第一个是键名,第二个是保存的数据。
localStorage.setItem("key","value");
读取数据使用 getItem 方法。它只有一个参数,就是键名。
let valueLocal = localStorage.getItem("key");

  1. 使用场景

LocalStorage 在存储方面没有什么特别的限制,理论上 Cookie 无法胜任的、可以用简单的键值对来存取的数据存储任务,都可以交给 LocalStorage 来做。

这里给举个例子,考虑到 LocalStorage 的特点之一是持久,有时更倾向于用它来存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串:

三、sessionStorage

sessionStorage 保存的数据用于浏览器的一次会话,当会话结束(通常是该窗口关闭),数据被清空;sessionStorage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 内容便无法共享;localStorage 在所有同源窗口中都是共享的;cookie 也是在所有同源窗口中都是共享的。除了保存期限的长短不同,SessionStorage 的属性和方法与 LocalStorage 完全一样。

  1. sessionStorage 的特点

会话级别的浏览器存储
大小为 5M 左右
仅在客户端使用,不和服务端进行通信
接口封装较好
基于上面的特点,sessionStorage 可以有效对表单信息进行维护,比如刷新时,表单信息不丢失。

  1. 使用场景

sessionStorage 更适合用来存储生命周期和它同步的会话级别的信息。这些信息只适用于当前会话,当开启新的会话时,它也需要相应的更新或释放。比如微博的 sessionStorage 就主要是存储本次会话的浏览足迹:
lasturl 对应的就是上一次访问的 URL 地址,这个地址是即时的。当切换 URL 时,它随之更新,当关闭页面时,留着它也确实没有什么意义了,干脆释放吧。这样的数据用 sessionStorage 来处理再合适不过。

  1. sessionStorage 、localStorage 和 cookie 之间的区别

共同点:都是保存在浏览器端,且都遵循同源策略。
不同点:在于生命周期与作用域的不同
作用域:localStorage 只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份 localStorage 数据。sessionStorage 比 localStorage 更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)
生命周期:localStorage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 sessionStorage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。

Web Storage 是一个从定义到使用都非常简单的东西。它使用键值对的形式进行存储,这种模式有点类似于对象,却甚至连对象都不是——它只能存储字符串,要想得到对象,还需要先对字符串进行一轮解析。

四、IndexedDB

IndexedDB 是一种低级 API,用于客户端存储大量结构化数据(包括文件和 blobs)。该 API 使用索引来实现对该数据的高性能搜索。IndexedDB 是一个运行在浏览器上的非关系型数据库。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据。

  1. IndexedDB 的特点

键值对储存。
IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

异步
IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

支持事务。
IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

同源限制
IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

储存空间大
IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。

支持二进制储存。
IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。

  1. IndexedDB 的常见操作

在 IndexedDB 大部分操作并不是常用的调用方法,返回结果的模式,而是请求——响应的模式。

(1)建立打开 IndexedDB ----window.indexedDB.open("testDB")
这条指令并不会返回一个 DB 对象的句柄,得到的是一个 IDBOpenDBRequest 对象,而希望得到的 DB 对象在其 result 属性中

除了 result,IDBOpenDBRequest 接口定义了几个重要属性:
onerror: 请求失败的回调函数句柄
onsuccess:请求成功的回调函数句柄
onupgradeneeded:请求数据库版本变化句柄

<script>
  function openDB(name) {
    let request = window.indexedDB.open(name); //建立打开IndexedDB
    request.onerror = function (e) {
      console.log("open indexdb error");
    };
    request.onsuccess = function (e) {
      myDB.db = e.target.result; //这是一个 IDBDatabase对象,这就是IndexedDB对象
      console.log(myDB.db); //此处就可以获取到db实例
    };
  }
  let myDB = {
    name: "testDB",
    version: "1",
    db: null,
  };
  openDB(myDB.name);
</script>

控制台得到一个 IDBDatabase 对象,这就是 IndexedDB 对象

(2)关闭 IndexedDB----indexdb.close()

function closeDB(db) {
  db.close();
}

(3)删除 IndexedDB----window.indexedDB.deleteDatabase(indexdb)

function deleteDB(name) {
  indexedDB.deleteDatabase(name);
}

浏览器缓存

缓存分为强缓存和协商缓存。强缓存不过服务器,协商缓存需要过服务器,协商缓存返回的状态码是 304。两类缓存机制可以同时存在,强缓存的优先级高于协商缓存。当执行强缓存时,如若缓存命中,则直接使用缓存数据库中的数据,不再进行缓存协商

1. 强缓存

HTTP1.0 提供 Expires,值为一个绝对时间表示缓存新鲜日期
HTTP1.1 增加了 Cache-Control: max-age=,值为以秒为单位的最大新鲜时间

(1)Expires(HTTP1.0):Exprires 的值为服务端返回的数据到期时间。
当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差。另一方面,Expires 是 HTTP1.0 的产物,故现在大多数使用 Cache-Control 替代

缺点:使用的是绝对时间,如果服务端和客户端的时间产生偏差,那么会导致命中缓存产生偏差

Pragma(HTTP1.0):HTTP1.0 时的遗留字段,当值为"no-cache"时强制验证缓存,Pragma 禁用缓存,如果又给 Expires 定义一个还未到期的时间,那么 Pragma 字段的优先级会更高。服务端响应添加'Pragma': 'no-cache',浏览器表现行为和刷新(F5)类似。

(2)Cache-Control(HTTP1.1):有很多属性,不同的属性代表的意义也不同
private:客户端可以缓存
public:客户端和代理服务器都可以缓存
max-age=t:缓存内容将在 t 秒后失效
no-cache:需要使用协商缓存来验证缓存数据
no-store:所有内容都不会缓存

请注意 no-cache 指令很多人误以为是不缓存,这是不准确的,no-cache 的意思是可以缓存,但每次用应该去想服务器验证缓存是否可用。no-store 才是不缓存内容。当在首部字段 Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令。命中强缓存的表现形式:Firefox 浏览器表现为一个灰色的 200 状态码。Chrome 浏览器状态码表现为 200 (from disk cache)或是 200 OK (from memory cache)

2. 协商缓存

协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回 304 状态码,浏览器拿到此状态码就可以直接使用缓存数据了

ETag 优先级比 Last-Modified 高

Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。

  1. if-Modified-Since:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有 if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回 304 和响应报文头,浏览器只需要从缓存中获取信息即可。
    (1)如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
    (2)如果没有被修改:那么只需传输响应 header,服务器返回:304 Not Modified

  2. if-Unmodified-Since: 从某个时间点算起, 是否文件没有被修改,使用的是相对时间,不需要关心客户端和服务端的时间偏差
    (1)如果没有被修改:则开始`继续'传送文件,服务器返回: 200 OK
    (2)如果文件被修改:则不传输,服务器返回: 412 Precondition failed (预处理错误)

这两个的区别是一个是修改了才下载一个是没修改才下载。如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为 Last-Modified 时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1 推出了 Etag

Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)

  1. If-Match:条件请求,携带上一次请求资源的 ETag,服务器根据这个字段判断文件是否有新的修改

  2. If-None-Match: 再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现 If-None-Match 则与被请求资源的唯一标识进行对比。
    (1)不同,说明资源被改动过,则响应整个资源内容,返回状态码 200。
    (2)相同,说明资源无心修改,则响应 header,浏览器直接从缓存中获取数据信息。返回状态码 304.

但是实际应用中由于 Etag 的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用 Etag 了。

3. 缓存场景

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略

  1. 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存
  2. 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新
  3. 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件

sessionStorage在同一网站多个标签页内共享数据的问题总结

一直以来以为的 sessionStorage 的生命周期是这样的:在 sessionStorage 中存储的数据会在当前浏览器的同一网站的多个标签页中共享,并在此网站的最后一个标签页被关闭后清除。注意:这是错误的。
之所以会这么认为,是因为写代码的时候,sessionStorage 给表现就是这样的。
假设有一个 index.html:

<!-- 使用一个新标签页打开自身,并设置一个 sessionStorage -->
<a href="index.html" target="_blank" οnclick="sessionStorage.setItem('j', 's')">
  open myself
</a>

接下来:

  1. 在浏览器中打开这个 index.html,称之为标签页 A。注意:需要用 http 协议打开!例如 http://localhost/index.html
  2. 点击页面上的链接,此时会弹出来标签页 B。
  3. 在标签页 B 中打开控制台并执行 sessionStorage.getItem('j')

控制台会输出 's',这说明标签页 A 和 B 共享了 sessionStorage 中的数据;接下来,先关闭这两个标签页,然后再打开一个标签页 C,再读取一下 j 的值,得到的是 null。

这看起来跟本文一开始的说法是一致的,但遇到了一个奇怪的事情

给上面的步骤添加第四步:

  1. 在浏览器中打开这个 index.html,称之为标签页 A。注意:需要用 http 协议打开!例如 http://localhost/index.html
  2. 点击页面上的链接,此时会弹出来标签页 B。
  3. 在标签页 B 中打开控制台并执行 sessionStorage.getItem('j'),得到 's'
  4. 新建一个新标签页 D,然后在地址栏内输入 http://localhost/index.html 打开同样的页面, 然后执行 sessionStorage.getItem('j') 。

得到的结果是 null。

为什么标签页 B 中得到的是 's',为什么标签页 D 中却是 null?
标签页 B 和标签页 D 之间唯一的不同就是它们被打开的方式:标签页 B 是通过在标签页 A 中点击链接打开的,但标签页 D 是在浏览器地址栏输入地址打开的。

通过点击链接(或者用了 window.open)打开的新标签页之间是属于同一个 session 的,但新开一个标签页总是会初始化一个新的 session,即使网站是一样的,它们也不属于同一个 session。