浏览器缓存是前端的老友,常见的是发版后,测试却发现没有变化,使用 ctr + F5 强制刷新之后、或清除浏览器缓存,才能正常展示

对于浏览器缓存,前端对它是又爱又恨,有时想保留,有时想禁掉,所以其机制是究竟为何

什么是浏览器缓存

浏览器缓存就是浏览器根据 url 第一次访问网站之后,将网站的 html、css、js、图片等文件复制一份保留到浏览器中,当你二次访问这个 url 的网站时,如果网站没有明确表示有更新时,浏览器直接在缓存中查找内容,不会再次请求网页内容,只有网页明确表示有更新时,浏览器才会向服务器发起网路请求,再次下载网页。

浏览器缓存优点

  1. 减少网络带宽消耗

对于网站运营者或者访问网页的用户,带宽代表着成本 ,使用浏览器缓存可以减少网络流量,从而降低运营成本。

  1. 降低服务器压力

使用浏览器缓存之后,除第一次访问需要向服务器请求网站全部资源,后续访问可以重复使用浏览器本地缓存,减少对服务器的请求,间接降低服务器的压力,同时,搜索引擎的爬虫也会根据缓存过期机制降低抓取的频率,也可以降低服务器压力。

  1. 减少网络延迟,加快网页加载

浏览器缓存 web 资源后,减少网络请求,可以更快速地获取到服务器返回数据,同时使用浏览器缓存内的文件比服务器获取快很多,所以网页加载速度明显快很多。

浏览器的缓存规则

首先,基本的缓存类型包括强缓存协商缓存,再进行一次简单介绍:

1.强缓存: 不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 size 显示 from disk cache 或 from memory cache 两种(灰色表示缓存)。 2.协商缓存: 向服务器发送请求,服务器会根据这个请求的 request header 的一些参数来判断是否命中协商缓存,如果命中,则返回 304 状态码并带上新的 response header 通知浏览器从缓存中读取资源;

共同点:都是从客户端缓存中读取资源; 区别是强缓存不会发请求,协商缓存会发请求。

对于浏览器端的缓存规则,是在 http 协议和 meta 标签中定义的。分别从两个维度:新鲜度和校验值,规定浏览器是否可以直接使用缓存中的副本,还是直接从服务器获取最新资源。

  • 新鲜度(过期):浏览器缓存的有效期,缓存必须满足以下两个条件,浏览器才会认为是最新的,可以直接使用。

    • 含有完整的过期时间控制头信息,并在有效期内。
    • 浏览器已经使用过这个副本,并且在会话中已经检查过新鲜度。
  • 校验值(验证):服务器返回资源的时候,会在响应头信息中带上资源实体标签 Entity Tag,可以用来作为浏览器再次请求过程的校验标识,如果发现校验标识不匹配,说明资源已经被修改过或过期,浏览器需要重新请求资源。

HTTP 协议头

http 请求和响应头中,与缓存相关的常见类型:

规则 消息报头 值/示例 类型 作用
新鲜度 Pragma no-cache 响应 告诉浏览器忽略资源的缓存副本,每次访问都需要去服务器拉取http1.0 中存在的字段,在 http1.1 已被抛弃,使用 Cache-Control 替代,但为了做 http 协议的向下兼容,很多网站依旧会带上这个字段
Expires Mon, 15 Aug 2016 03:56:47 GMT 响应 启用缓存和定义缓存时间。告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求http1.0 中存在的字段,该字段所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。在 HTTP 1.1 版开始,使用 Cache-Control: max-age=秒替代
Cache-Control no-cache 响应 告诉浏览器忽略资源的缓存副本,强制每次请求直接发送给服务器,拉取资源,但不是“不缓存”
no-store 响应 强制缓存在任何情况下都不要保留任何副本
max-age=[秒] 响应 指明缓存副本的有效时长,从请求时间开始到过期时间之间的秒数
public 响应 任何路径的缓存者(本地缓存、代理服务器),可以无条件的缓存该资源
private 响应 只针对单个用户或者实体(不同用户、窗口)缓存资源
Last-Modified Mon, 15 Aug 2016 03:56:47 GMT 响应 告诉浏览器这个资源最后的修改时间。服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
If-Modified-Since Mon, 15 Aug 2016 03:56:47 GMT 请求 其值为上次响应头的 Last-Modified 值,再次向 web 服务器请求时带上头 If-Modified-Since。web 服务器收到请求后发现有头 If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),包括更新 Last-Modified 的值,HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应 HTTP 304(无需请求,节省浏览),告知浏览器继续使用所保存的 cache
校验值 ETag “fd56273325a2114818df4f29a628226d” 请求 告诉浏览器当前资源在服务器的唯一标识符(生成规则由服务器决定)
If-None-Match “fd56273325a2114818df4f29a628226d” 请求 当资源过期时(使用 Cache-Control 标识的 max-age),发现资源具有 Etage 声明,则再次向 web 服务器请求时带上头 If-None-Match(Etag 的值)。web 服务器收到请求后发现有头 If-None-Match 则与被请求资源的相应校验串进行比对,决定返回 200 或 304
各种类型之间的关系和区别:
  • Cache-Control 与 Expires:它两作用一样,都表明当前资源的有效期,控制浏览器是取缓存还是直接向服务器获取,Cache-Control 可以设置的更细致,如果同时设置,它的优先级高于 Expires。
  • Last-Modified / ETag 与 Cache-Control / Expires:配置 Last-Modified/ETag 的情况下,浏览器再次访问 URL 的资源,还是会发送请求到服务器,询问文件是否已经修改,如果没有,服务器会给浏览器返回 304,浏览器直接从本地缓存中取就好了,反之,服务器会直接向浏览器返回数据。Cache-Control / Expires 检测本地缓存是否还在有效期内,在有效期内,直接使用本地缓存,阻止发送请求。如果同时设置,Cache-Control / Expiress 优先级更高。一般情况下,两者配合使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时 Last-Modified/ETag 将能够很好利用 304,从而减少响应开销。
  • Last-Modified 与 ETag:ETag 主要是为了解决 Last-Modified 比较难解决的问题:1、Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度。2、如果某些文件会被定期生成,当有时内容并没有任何变化,但 Last-Modified 却改变了,导致文件没法使用缓存。3、有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形。ETag 是服务器自动生成或开发者生成对应资源在服务器的唯一标识符,能够更加精准控制缓存。两者可以一起使用,服务器优先验证 ETag,一致时,才会继续比对 Last-Mofifed,才决定是否要返回 304。

不能缓存的请求

也并非所有请求都可以被缓存,包括:

  • post 请求无法被缓存。
  • 需要根据 cookie、认证信息等决定输入内容的动态请求不能被缓存。
  • http 响应头中不包含 Last-Modified/ETag,也不包含 Cache-Control/Expiress 的请求无法被缓存。
  • http 信息头明确设置 Cache-Control:no-cache,pragma:no-cache 或 Cache-Control:max-age=0 浏览器不缓存时。

前端发版后浏览器缓存问题

在简单了解浏览器的缓存机制后,在开发完发布新版本后,在有些电脑上总需要强刷才能获取到最新版本的内容,这显然是不合理的,需要通过一些手段进行解决,通过一番查询,找到几种解决方案进行处理

解决方案

  1. .html 页面加 meta 标签
1
2
3
<meta http-equiv="pragram" content="no-cache">
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="expires" content="0">
  1. 后端 nginx 配置,让 index.html 不缓存

vue 默认配置,打包后 cssjs 的名字后面都加了哈希值,不会有缓存问题,但是 index.html 在服务器端可能是有缓存的,需要在服务器配置不让缓存 index.html

1
2
3
location = /index.html {
add_header Cache-Control "no-cache, no-store";
}
  1. 使用 Vue 脚手架的情况下:vue.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 动态版本号
const version = new Date().getTime()
// 配置
module.exports = {
devServer: {},
filenameHashing: false, // 打包的时候不使用 hash 值,因为后面自行添加时间戳或者版本号了
css: {
// 是否使用 css 分离插件 ExtractTextPlugin
extract: {
// 输出编译后的文件名称:【文件名称.时间戳】、【文件名称.版本号.时间戳】...
filename: `css/[name].${version}.css`,
chunkFilename: `css/[name].${version}.css`
// filename: `css/[name].${process.env.VUE_APP_VERSION}.${version}.css`,
// chunkFilename: `css/[name].${process.env.VUE_APP_VERSION}.${version}.css`
}
},
configureWebpack: {
output: { // 输出编译后的文件名称:【文件名称.时间戳】、【文件名称.版本号.时间戳】...
filename: `js/[name].${version}.js`,
chunkFilename: `js/[name].${version}.js`
// filename: `js/[name].${process.env.VUE_APP_VERSION}.${version}.js`,
// chunkFilename: `js/[name].${process.env.VUE_APP_VERSION}.${version}.js`
}
}
}
  1. 使用 webpack 的情况下:webpack.config.js
1
2
3
4
5
6
7
8
9
const date = new Date()
const version = moment(date).format('YYYYMMDDHHmmssSSS') // 打包时候的版本号
const timestamp = date.getTime() // 时间戳
// 注意需下面这段放到配置导出中
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath(`js/[name].[chunkhash:8].${ version }.js?_t=${ timestamp }`),
chunkFilename: utils.assetsPath(`js/[name].[chunkhash:8].${ version }.js?_t=${ timestamp }`)
}
  1. 有新版本发布,及时拉取最新版本代码
  • 有时候发布了新版本,用户不刷新或强制刷新,一直不能看不到最新版本代码,通过使用插件version.js,封装了套在切换页面时检查服务器是否有新版本,有新版本则直接强制刷新拉取最新版本代码,这样也解决了缓存问题,切换页面就能及时同步到最新版本代码。

  • 使用方式,只需要两个位置导入使用:

1、「如果是原生开发,第一步可以改为手动创建文件,并每次修改版本号即可」在打包配置文件中(例如:vue.config.js)创建版本文件,因为只需要 build 时才需要创建版本文件,版本文件以时间戳为版本号,所以不需要操心。

1
2
3
// 在 build 时,每次创建/更新版本文件
const version = require('./src/utils/version')
version.create()

2、打包有了版本号,发布上去后,那就需要在合适位置,调用**version.getPro()**拉回来校验是否有新版本