本文内容来自于以下链接文章中,本文仅作整合和记录作用,如有需要,请跳转至原文阅读。
web 缓存大致可以分为:
本文重点介绍 cookie
、localStorage
、sessionStorage
。
cookie
是存储在客户端中一种特殊的字符串,以键值对形式存在,挂载在 document
对象下,可以直接使用 document.cookie
进行获取和设置相关信息。
HTTP 协议本身无状态,服务器无法判断用户身份,因此我们常用 cookie
对用户基本信息和身份信息进行校验。
当用户第一次访问一个网站时:
Set-Cookie
cookie
cookie
主要属性:
属性名称 | 属性用途 | 注意事项 |
---|---|---|
NAME=VALUE | 键值对,可以设置要保存的Key/Value | NAME不能重复,否则覆盖 |
Expires | 失效时间,单位为 s,即过时后会被客户端删除 | Cookie中通过getMaxAge()和setMaxAge(int maxAge)来读写该属性。maxAge有3种值,分别为正数,负数和0,具体表示:正数表示失效时间,当maxAge属性为负数(会被创建),则表示该Cookie只是一个临时Cookie,不会被持久化,仅在本浏览器窗口或者本窗口打开的子窗口中有效,关闭浏览器后该Cookie立即失效,当maxAge为0时(不会被创建),表示立即删除Cookie |
Domain | 设置可以操作 Cookie 的域名 | |
Path | 设置可以操作的cookie的具体路径,一般默认为/,表示根目录下的页面都有权利操作cookie | |
Secure | 安全模式下传输cookie信息 | 如果设置了这个属性,那么只会在 SSH 连接时才会回传该 Cookie |
特性 | cookie | localStorage | sessionStorage |
---|---|---|---|
数据的生命周期 | cookie一般由服务器生成,可设置失效时间 | 除非被清除,否则永久存在 | 仅在当前会话页面有效,关闭页面或者浏览器后被清除 |
存放数据大小 | 4K左右 | 一般为5M | - |
与服务端通信 | 每次被携带在HTTP头中,用来做用户身份校验,如果数据存储过多可能会带来性能问题 | 仅在客户端保存,不参与和服务器通信 | - |
易用性 | 没有现成方法,需要自行封装 | 原生接口可以接受,亦可以再次封装来对Object和Array有更好的支持 | - |
注意,我们缓存主要是针对 html、css、img 等静态资源,常规情况下,我们不会缓存一些动态资源,因为缓存动态资源的话,数据的实时性不太好,所以我们一般都只会去缓存一些不太容易被改变的静态资源。
先说说缓存可以解决什么问题。
在日常开发中,最最最最最关心的还是“更快的加载页面”。尤其是 React/Vue等 SPA 应用来说,首屏加载是老生常谈的问题,这个时候缓存就显得非常重要。不需要往后端请求,直接在缓存中读取。速度上,会有显著的提升,是一种提升网站性能和用户体验的有效策略。
http缓存又分为两种,强制缓存和协商缓存。
http缓存流程图↓:
如果浏览器判断请求的目标资源有效命中强缓存,则直接从内存中读取目标资源,无需与服务器做任何通信。
在以前,我们通常会使用响应头的 Expires
字段实现强缓存,如下图↓:
Expires
字段作用:设置一个强缓存时间,在此时间范围内,从内存或磁盘中读取缓存返回。
但是 Expires
已经被废弃了。对于强缓存来说,Expires
已经不再是实现强缓存的首选。
因为Expires判断强缓存是否过期的机制是:获取本地时间戳,对比先前拿到的资源文件中的 Expires字段的时间来判断。但是本地时间可能不准确。
Expires 过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的问题,所以 Expires 几乎不被使用。
现在的项目中,强缓存通常使用cache-control
字段代替Expires
。
Cache-Control
这个字段是在 http1.1 中被增加的,完美解决了 Expires
本地时间和服务器时间不同步的问题,是当下项目中实现强缓存的最常规方法。
Cache-Control
使用方法很简单,只要在资源的响应头上写上需要缓存多久就好了,单位是s,比如:
//往响应头中写入需要缓存的时间
res.writeHead(200,{
'Cache-Control':'max-age=10'
});
下图意思是:从该资源第一次返回时开始,往后的10秒中如果再次请求该资源,则从缓存中读取。
注意,
Expires
中是具体的时间,而Cache-Control
中是一个滑动时间,从服务器第一次返回资源时开始计时。所以不需要对比客户端和服务端的时间,解决了Expires
所存在的巨大漏洞。
Cache-Control
有六大属性:
max-age
决定客户端资源被缓存多久s-maxage
决定代理服务器缓存的时长no-cache
表示强制进行协商缓存no-store
表示禁止任何缓存策略public
表示资源既可以被浏览器缓存,也可以被代理服务器缓存private
表示只能被浏览器缓存no-cache
不是像字面意思一样禁止缓存,而是强制进行协商缓存。
可以这样理解,
cache-control
是强缓存,那禁止缓存不就是禁止强缓存,那就是进行协商缓存了。
no-store
才是真正的禁止所有缓存策略。
注意,
no-cache
和no-store
是一组互斥属性,这俩属性不能同时出现在cache-control
中。
因为一般情况是这样的:
但是有些情况是例外的,比如出现代理服务器:
public
和 private
就是决定资源是否可以在代理服务器上进行缓存的属性。
public
表示资源在客户端和代理服务器都可以被缓存private
表示资源只能在客户端被缓存,拒绝资源在代理服务器缓存private
。基于 last-modified
的协商缓存实现方式是:
last-modified
字段。cache-control: no-cache
。
还没完。到这里还无法实现协商缓存。
当客户端读取到 last-modified
时,会在下次请求表头中携带一个字段:If-Modified-Since
。这个值就是服务器第一次修改时候给的时间。
// If-Modified-Since等于这段时间
res.setHeader('last-modified', mtime.toUTCString());
那么之后每次对该资源的请求,都会带上If-Modified-Since
字段,而服务端需要拿到这个时间并再次读取该资源的修改时间,让这俩对比来决定是读取缓存还是返回新的资源。
用一张图来解释:
因此以上方式的协商缓存已经有两个明显的漏洞:
- 因为时根据修改时间来判断。如果文件内容未修改,仅修改文件名,此时修改时间变化导致缓存会失效。
- 当文件在极短时间内修改完成(比如几百毫秒)。因为文件修改时间记录的最小记录单位是s,所以这样文件修改时间不会改变。即使文件内容修改了,依然不会返回新的文件。
为了解决这俩问题,从 http1.1
开始新增了一个头信息,Etag(Entity实体标签)
。
Etag
就是将原来比较 时间戳
的形式修改成了比较 文件指纹
。
文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。
来看一下流程:
Etag
字段中跟资源一起返回给客户端。Etag
,并赋给请求头的 if-None-Match
字段。If-None-Match
字段,并再次读取目标资源并生成文件指纹,两个指纹做对比,如果未改变则返回304,如果改变则返回新的 Etag
。
从校验流程上来说,协商缓存的修改时间比对和文件指纹比对,几乎是一样的。
值得注意的是,不同于
cache-control
是expires
的完全替代方案。Etag
并不是last-modified
的完全替代方案,而是last-modified
的补充方案。意味着根据业务场景选取合适的方案。
从前端角度来说
你什么都不用干,缓存是缓存在前端,但实际上代码是后端的同学来写的。如果你需要实现前端缓存的话啊,通知后端的同学加响应头就好了。
从后端角度来说
参考此文章。
有哈希值的文件设置强缓存即可。没有哈希值的文件(比如index.html)设置协商缓存。
我们给css设置强缓存,哪怕缓存1W年。只要我们重新打包,生产新的哈希值。那么文件名就更改了。对于机器来说,更改了文件名的文件,就是一个新的文件。
举个例子:
比如,有一个css文件a1
第一次打包a1.css文件追加哈希值变成了 a1.aaaaa.css,我们给a1.aaaaa.css设置了强缓存1W年。
然后项目改动,我们又打包了一次。打包后生产新的哈希值,a1.aaaaa.css变成了a1.bbbbb.css文件。那么当我们第一次访问a1.bbbbb.css文件的时候是不会被缓存。因为1W年的缓存是给a1.aaaaa.css文件做的。关我a1.bbbbb.css文件什么事?这样我们也就能拿到最新的改动。
其他可以被webpack生成哈希值的文件同理。
既然img/css这些文件都可以用强缓存。通过更改文件名的方式来获取最新的数据,为什么我堂堂index.html就要用协商呢?
因为一般情况下,index.html是不会设置哈希值的。(具体得看自己项目下的dist文件夹)