大家在日常的开发工作过程中,有没有遇到过下面几种情况:
- 部署/发布前端工程后,增加的功能或修改的bug没有生效
- 测试同学测试功能时经常暴力地清除所有浏览器缓存
- 前端开发同学经常说:你“强刷”一下就好了
- …
遇到上面这些情况,大部分同学就知道了,这是前端有缓存的原因,那具体什么是前端缓存
呢,前端缓存
仅仅和前端有关系吗?
前端缓存 / 浏览器缓存
前端缓存
,是浏览器为了提升网站的加载性能,缩短用户等待时间而采取的措施,浏览器总是想尽量少地向服务器发送请求,能够从自己保存的副本中得到的,就不去麻烦服务器了,毕竟自己动手丰衣足食嘛,所以更准确的叫法应该为浏览器缓存
,下文中如果出现缓存
等字眼,指的就是前端缓存
或浏览器缓存
。
由上所述,缓存对于用户来说是友好的,而且对大多数用户透明的,普通用户可能最多只是感觉再次进入一个网站时速度变快了而已,再进一步可能某些用户发现一些静态页面断网后还能被访问。但是对于开发同学就需要对缓存有所了解,并在发布新版时特别注意。
缓存机制
那缓存是具体是什么呢?我们可以将其理解为我们下载到硬盘的文件,比如说老师为我们制作了一份课件,我们将其下载放在了硬盘中,那之后我们什么时候想看,只需要到这份课件保存的文件夹内,将其打开就好了,而不必再次下载了。但是有一天老师发现了课件里面有一些问题,那老师为了不误导我们肯定想要修正这些问题,修正过后呢,老师在上课的时候就告诉我们“同学们,你们上次下载的课件有些问题,我已经改了,你们再重新下载一下,之前那份就不要看了”。那我们回去之后就重新下载老师修改过后的课件,之前的课件就再也不需要看了。
明白了上面的例子,其实也就明白了缓存的机制,对应于缓存,工作流程应该是这样的:
用户小U使用浏览器小B访问了一个页面P,浏览器将页面P和其中包含的资源文件(一般包含css、js、图片等文件)保存下来了,一段时间内呢小B反复访问页面P,小B都会从自己的存储区取出相应文件为小U渲染出同样的页面P。然后有一天页面P更新了,比如说主题变了或者里面的按钮功能变化了,当小B再次访问页面P时,它知道自己之前保存的页面P的资源文件已经过时了,然后就再次访问一下页面P,再将页面P和其中包含的资源文件保存下来,之前保存的页面P和其中包含的资源文件就没用了。
那上面两个流程是不是都有个重新下载的过程,那重要的就是如果感知到资源的变化。第一个例子中是老师通知同学们资源变化了,第二个例子就没有上面说的那么简单了,没人主动告诉小B它缓存的资源已经过时了,那小B是通过什么方式才能知道自己缓存的资源已经过时了呢:
缓存的状态
状态 | 备注 |
---|---|
无缓存 | 初次见面,第一次进入某个页面 |
缓存过时/非法 | 重逢不识君,进入过某个页面,并有对应缓存,但再次进入时可能已经过时了 |
缓存合法 | 归来仍是少年,进入过某个页面,并有对应缓存,再次进入时仍然是有效的 |
而缓存状态的变化,需要浏览器和服务器之间达成某些协议,这些协议由HTTP头部确定及执行,这里我们介绍最常用的:
缓存头部
- Cache-Control
- Etag及If-None-Match夫妇
- Last-Modified及If-Modified-Since夫妇
Cache-Control
Cache-Control
的语法及使用姿势众多,刚兴趣的可直接到MDN查看,这里我们只介绍它在响应头中用于告知浏览器如何进行缓存行为:
Response Headers
Cache-Control: public, max-age=31536000
上面的响应头中Cache-Control: public, max-age=31536000
告知浏览器从当前请求的时间点开始,再次请求此资源如果还未超过31536000秒(1年),你就就不必问我了,放心地使用你本地保存的就好了。但是这就有个问题了,如果一年内的某天,此资源变化了,浏览器该如何知道呢?很遗憾,这种情况下浏览器在1年内是不会再知道了。
所以如果web server想要对资源设置诸如Cache-Control: public, max-age=31536000
响应头,一般需要前端搭配文件名hash
来使用,这在各种构建工具或脚手架中一般都有相应的配置,如webpack配置:
// webpack
module.exports = {
// ...
output: {
filename: '[name].[contenthash].js',
// filename: '[name].[hash].js',
// ...
},
optimization: {
moduleIds: 'hashed',
// ...
},
// ...
}
适用资源:基本所有的资源型文件(如js、css、图片、字体文件等)。
设置为1年没有什么其他含义,只是一个较大的时间区间而已。
当我们按照上面方式配置完成后,满心欢喜的去发布新版了,可是部署完成后再次访问,尴尬了,没有变化?这就要注意了,缓存对于html文件也是生效的,我知道这很显而易见,但是很多人容易忽略。
特别是对于单页面应用来说,我们一般只有一个index.html,在index.html中引入其他js、css等资源文件,上面的步骤只是对于index.html中引入的资源文件名中添加了hash
,保证发布新版后这些引入的资源文件在浏览器缓存中不存在,但是如果浏览器取得index.html是通过本地缓存得到的呢?
<!-- 缓存内index.html -->
<link rel="stylesheet" href="index.v1.css" />
<script src="index.v1.js"></script>
<!-- 新版的index.html -->
<link rel="stylesheet" href="index.v2.css" />
<script src="index.v2.js"></script>
这里为了方便,我们使用v1、v2等代指
hash
。
很显然,如果浏览器从缓存中获取index.html,然后肯定会尝试获取index.v1.css和index.v1.js,而这两个文件再缓存中也是存在的且是合法的(假设还在1年内),那自然用户看到的页面及功能都是老的了。那我们应该如何为html文件设置缓存策略呢?
我们可以在web server的配置中针对html文件设置Cache-Control: no-cache
,当我们请求html文件时,响应头会包含:
Response Headers
Cache-Control: no-cache
要特别注意
no-cache
(允许缓存,但是使用前要向服务器确定缓存是否合法,确定方案下面会讲到)和no-store
(不允许缓存)的区别。
这样的话,当我们发布新版后,浏览器请求index.html发现有缓存,但并不会无脑的使用,而是会向服务器确认一下当前的缓存是否合法,如果合法则直接使用缓存内的版本,否则会向服务器请求最新的index.html,之后我们的index.v2.css和index.v2.js就能够被正确获取,用户就能够看到新的页面了。
ETag及If-None-Match
ETag
被称为实体标签或版本标识符,ETag
变化代表资源的变化。
上面说道,浏览器有时需要向服务器确定缓存是否合法,那通过ETag
响应头部和If-None-Match
请求头部就能够确定,大致过程如下:
- 首次请求index.html,服务器响应头部中包含
ETag: W/"v1"
首部 - 浏览器缓存index.html,再次请求index.html时,请求头部包含
If-None-Match: W/"v1"
- 服务器确定index.html是否有变化,如果没有变化,则状态码返回
304
,并且没有响应体,浏览器将使用本地缓存;如果有变化,则状态码返回200
,将新的index.html传给浏览器,并返回新的ETag: W/"v2"
首部 - 重复2、3
v1、v2只是为了标识出ETag的变化,实际上生成ETag的算法也不唯一,甚至简单地使用版本号也可以。关于ETag详细信息可参阅MDN。
Last-Modified及If-Modified-Since
除了ETag
及If-None-Match
,使用Last-Modified
和If-Modified-Since
组合也能起到类似的效果,大致过程和前组合类似:
- 首次请求index.html,服务器响应头部中包含
Last-Modified: Wed, 21 Oct 2019 07:28:00 GMT
首部 - 浏览器缓存index.html,再次请求index.html时,请求头部包含
If-Modified-Since: Wed, 21 Oct 2019 07:28:00 GMT
- 服务器确定index.html是否有变化(通过资源的最近修改时间和Wed, 21 Oct 2019 07:28:00 GMT对比),如果没有变化,则状态码返回
304
,并且没有响应体,浏览器将使用本地缓存;如果有变化,则状态码返回200
,将新的index.html传给浏览器,并返回新的Last-Modified: Wed, 22 Oct 2019 09:32:00 GMT
首部 - 重复2、3
Last-Modified
和If-Modified-Since
组合准确度不如ETag
及If-None-Match
组合,所以不太推荐使用:
- 有些服务器无法正确地判断资源的最近修改日期
- 如果资源的变化周期在秒级以下,只能精确到秒的修改日期就不那么精确了
确定缓存是否合法,也会发请求和服务器端通信,但如果缓存有效,服务器发回的响应中是不包含响应体的,这样流量消耗是很小的,只有头部的消耗;即使缓存无效,也只是相当于发了一个首次请求而已。
总结
综上所述,我们可以在部署前端工程时使用如下方案,保证用户能够享受缓存带来的便利,也能保证不会因为缓存造成更新不生效的问题:
- 针对大部分资源文件,使用
Cache-Control: public, max-age=31536000
及文件名hash
的方案 - 针对html文件,使用
Cache-Control: no-cache
和ETag
方案
为不同类型资源配置响应头部是web server的工作,请求头部是浏览器的自发工作,文件hash是前端的工作。