那些必须了解的前端缓存知识

大纲:

  1. 什么是web缓存(前端缓存)
  2. 缓存可以解决什么问题?缺点是什么?
  3. 强制缓存原理讲解
    • 基于Expires字段实现的强缓存
    • 基于Cache-control实现的强缓存
  1. 协商缓存原理讲解
    • 基于last-modified实现的协商缓存
    • 基于ETag实现的协商缓存

1.什么是web缓存?

web缓存主要指的是两部分:浏览器缓存和http缓存。
其中http缓存是web缓存的核心,是最难懂的那一部分,也是最重要的那一部分。
浏览器缓存主要指sessionStorage、localStorage、cookie等等(具体不再详细描述)
http缓存:

Web 缓存是可以自动保存常见文档副本的 HTTP 设备。当 Web 请求抵达缓存时, 如果本地有“已缓存的”副本,就可以从本地存储设备而不是原始服务器中提取这个文档。

看图,问题就是出在,服务器需要处理http的请求,并且http去传输数据,需要带宽,带宽是要钱买的啊。而我们缓存,就是为了让服务器不去处理这个请求,客户端也可以拿到数据

注意,我们的缓存主要是针对html,css,img等静态资源,常规情况下,我们不会去缓存一些动态资源,因为缓存动态资源的话,数据的实时性就不会不太好,所以我们一般都只会去缓存一些不太容易被改变的静态资源。

2. 缓存可以解决什么问题?缺点是什么?

缓存解决的问题:

  • 减少不必要的网络传输,节约宽带(就是省钱)
  • 更快的加载页面(就是加速)
  • 减少服务器负载,避免服务器过载的情况出现(就是减载)

缺点:

  • 占内存(有些缓存会被存到内存中)

http缓存分为两种:强缓存 和 协商缓存

http缓存流程图

3. 强制缓存-基于Expires字段实现的强缓存

在以前,我们通常会使用响应头的Expires字段去实现强缓存。如下图↓

Expires字段的作用是,设定一个强缓存时间。在此时间范围内,则从内存(或磁盘)中读取缓存返回。
但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。

因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。

3. 强制缓存-基于Cache-control实现的强缓存

Cache-control这个字段在http1.1中被增加,Cache-control完美解决了Expires本地时间和服务器时间不同步的问题。是当下的项目中实现强缓存的最常规方法。
Cache-control的使用方法页很简单,只要在资源的响应头上写上需要缓存多久就好了,单位是秒。比如↓

1
2
3
4
//往响应头中写入需要缓存的时间
res.writeHead(200,{
'Cache-Control':'max-age=10'
});

下图的意思就是,从该资源第一次返回的时候开始,往后的10秒钟内如果该资源被再次请求,则从缓存中读取。

Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。
Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。

注意,no-cache和no-store是一组互斥属性,这两个属性不能同时出现在Cache-Control中。
public和private也是一组互斥属性。他们两个不能同时出现在响应头的cache-control字段中。

Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。

4.协商缓存原理讲解-基于last-modified实现的协商缓存

基于last-modified的协商缓存实现方式是:

  1. 首先需要在服务器端读出文件修改时间,
  2. 将读出来的修改时间赋给响应头的last-modified字段。
  3. 最后设置Cache-control:no-cache
    (三步缺一不可)


注意圈出来的三行。
第一行,读出修改时间。
第二行,给该资源响应头的last-modified字段赋值修改时间
第三行,给该资源响应头的Cache-Control字段值设置为:no-cache.(上文有介绍,Cache-control:no-cache的意思是跳过强缓存校验,直接进行协商缓存。)
还没完。到这里还无法实现协商缓存
当客户端读取到last-modified的时候,会在下次的请求标头中携带一个字段:If-Modified-Since。

而这个请求头中的If-Modified-Since就是服务器第一次修改时候给他的时间,也就是上图中的

这一行

那么之后每次对该资源的请求,都会带上If-Modified-Since这个字段,而务端就需要拿到这个时间并再次读取该资源的修改时间,让他们两个做一个比对来决定是读取缓存还是返回新的资源。

使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。

1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。

2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。

为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)

4.协商缓存原理讲解-基于ETag实现的协商缓存

ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。

文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。

我们来看一下流程↓

  1. 第一次请求某资源的时候,服务端读取文件并计算出文件指纹,将文件指纹放在响应头的etag字段中跟资源一起返回给客户端。
  2. 第二次请求某资源的时候,客户端自动从缓存中读取出上一次服务端返回的ETag也就是文件指纹。并赋给请求头的if-None-Match字段,让上一次的文件指纹跟随请求一起回到服务端。
  3. 服务端拿到请求头中的is-None-Match字段值(也就是上一次的文件指纹),并再次读取目标资源并生成文件指纹,两个指纹做对比。如果两个文件指纹完全吻合,说明文件没有被改变,则直接返回304状态码和一个空的响应体并return。如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag中并返回给客户端

代码示例:

流程示意图:

值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。

5.在vue项目中设置,解决项目升级的缓存问题

5.1 直接在index.html中加入了这几行代码
1
2
3
4
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">

缺点:升级时缓存问题倒解决了,但直接导致了用户每次访问你的程序时都要重新请求服务器,所有的静态资源都无法用缓存了,浪费流量,网络压力变大。

5.2 Nginx 配合 vue.config.js 进行配置

由于打包后的js、css和图片,一般名称都带有hash值,名称中的hash变了,自然会拉取新文件,所以我们可以将这类文件设置为强制缓存,只要文件名不变,就一直缓存,比如缓存100天或者一年。
而html文件则不能设为强制缓存,一般html名称是没法带hash值的,所以html如果设置了强制缓存,则永远也没法更新,html不更新,其引用的js、css等名称也不会更新,则整个服务都没有更新,只能让用户清除缓存了。所以针对html文件,我们可以设置协商缓存或者直接不使用缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Nginx配置
location /udaam-ui {
root /usr/local/ui-workspace;
index index.html index.htm;
try_files $uri $uri/ /udaam-ui/index.html;
if ($request_filename ~* .*\.(js|css|woff|png|jpg|jpeg)$){
expires 100d; #js、css、图片缓存100
#add_header Cache-Control "max-age = 8640000"; #或者设置max-age
}
if ($request_filename ~* .*\.(?:htm|html)$){
add_header Cache-Control "no-cache, no-store"; #html不缓存
}
}
  • no-cache浏览器会缓存,但刷新页面或者重新打开时 会请求服务器,服务器可以响应304,如果文件有改动就会响应200
  • no-store浏览器不缓存,刷新页面需要重新下载页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vue.config.js 配置
const timeStamp = new Date().getTime()
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
return {
output: {
// 输出重构 打包编译后的 文件名称 【模块名称.版本号.时间戳】
filename: `js/[name].[chunkhash].${timeStamp}.js`,
chunkFilename: `js/[id].[chunkhash].${timeStamp}.js`
}
}
}
},
修改output的filename和chunkFilename
  • filename 指列在entry 中,打包后输出的文件的名称。
  • chunkFilename 指未列在entry 中,却又需要被打包出来的文件的名称。
1
2
3
4
5
6
7
8
// 修改打包后的css
const timeStamp = new Date().getTime()
css: {
extract: { // 打包后css文件名称添加时间戳
filename: `css/[name].${timeStamp}.css`,
chunkFilename: `css/chunk.[id].${timeStamp}.css`
}
}
文章作者: qinwei
文章链接: https://qw-null.github.io/2022/09/15/前端缓存知识/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 QW's Blog