缓存和内容协商又称强缓存(有效期内的缓存)和弱缓存(服务器和浏览器商量是否能缓存的方案)

1 HTTP缓存

  • 例如我们访问百度,百度会发送html文件到客户端,而其他的js、css或图片等文件是由CDN发送给客户端的,并且在响应头添加Cache-Control: public, max-age=3600, must-revalidate,允许客户端缓存该文件。

2 Cache-Control释义

1
2
3
4
5
Cache-Control: 
public			// 公开内容,允许中间代理(小区、市级)进行缓存
				// 如果是private,则表示只允许用户的浏览器缓存
max-age=3600	// 缓存时间
must-revalidate	// 对于过期的缓存,应重新校验

3 HTTP内容协商

  • 协商的是缓存过期后能不能重用的问题
  • 过程:
    • 客户端第一次向CDN服务器请求js文件时,CDN服务器会在响应头上添加Cache-Control字段以及ETag字段
    • Etag字段值为该js文件编码(md5)后的哈希值字符串
    • 浏览器会将该js文件缓存,当缓存过期时,会向服务器发送协商请求,该请求头需要添加If-None-Match,其值为之前ETag的值。
    • 这样以来,服务器会对比自己的和协商请求头中的ETag值,进而得出缓存文件的处理方式:
      • 如果两次值不同,说明服务器端更新了该js文件,所以将以200状态码向客户端响应新的文件,告知浏览器将原来的缓存文件删除或覆盖;
      • 如果两次ETag值相同,说明不需要更新js文件,所以将以304状态码向客户端响应,告知客户端沿用缓存文件即可。

4 缓存方案

http协议版本 缓存(强缓存) 内容协商(弱缓存) 备注
HTTP/1.1 服务器响应时在响应头添加:
Cache-Control: max-age:3600,must-revalidate
ETag: ...
请求头:If-None-Match: ...ETag
响应:304+空内容/200+新内容
主流
HTTP/1.0 服务器响应时在响应头添加:
Expire: 时间点A
Last-Modified: 时间点B
请求头:If-Modified-Since: 时间点B
响应:304+空内容/200+新内容
依赖于收发双方的时间统一,如果用户PC时间不准确,将会影响缓存文件的时效性。

实际工作中,程序员可能会同时使用这两套方案,以兼容一些旧的网络设备和浏览器。

5 禁用缓存

  • 服务端要求禁用缓存:
1
2
3
Cache-Control: max-age=0, must-revalidate	// 不允许缓存,允许协商
Cache-Control: no-cache						// 不允许缓存,允许协商
Cache-Control: no-store						// 不允许缓存,也不允许协商
  • 浏览器端禁用缓存:
1
2
3
1在请求头添加
Cache-Control: no-cache, no-store, max-age=0
2在请求的url后添加 ?_=随机数 即可

6 实际应用

  • index.html不能缓存,响应头里加Cache-Control: max-age=0防止缓存,加Etag用于内容协商,加Expire: 过去的时间用于兼容旧浏览器
  • CSS、JS和图片文件一律在文件名后面加版本字符串,如index-855fcfd82e.js,响应头里加Cache-Control: max-age=2592000缓存一个月以上,Etag可以不加(因为内容不会变,过时的缓存可以用)。CDN服务商一般都会帮你做这些事。
  • 当CSS、JS和图片文件内容有变化时,直接修改文件名中的版本字符串(Webpack 会帮我们做),替换index.html里的旧文件名即可。旧文件的缓存会一直躺在用户的电脑里直到过期后被浏览器自动清理。
  • API响应头里加Cache-Control: no-store, max-age=0防止缓存和内容协商。

7 缓存API

  • 前端程序员一直在尝试自己用 JS 控制缓存,而不是用 HTTP 缓存。 比如曾经有人通过 AJAX 获取 CSS、JS,然后插入页面中,并存储在 LocalStorage 里作为缓存。其优点是在移动端网速特别慢时(比如 2G/3G 网络),读取 LocalStorage 比内容协商要快很多。 再加上有一段时间浏览器厂商宣传「Web 可以比原生应用相媲美」,于是浏览器专门给JS提供了 一套用于缓存的API:Cache API。样例代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// service-worker.js 中默认会有全局对象 self
self.addEventListener('fetch', function(event) { // self 可以监听页面请求
	console.log('发现浏览器在请求文件:', event.request.url);
	
 	event.respondWith( // 我来进行响应
 		caches.open("font-cache-v1").then(cache => // 打开缓存
 			cache.match(event.request) // 查找对应的请求
 				.then(function(response) {
 					if (response) { // 如果有对应的响应
 						console.log(' 发现已有缓存:', response)
 						return response
 					}else{ // 如果没有对应的响应
 						console.log(' 没有找到对应缓存')
 					}
 				}) 
 				.catch(function(error) {
 					console.error(' 未知错误', error);
 					throw error;
 				});
 		)
 	);
});
  • 实际工作中,经常将Cache APIservice worker结合使用,因为service worker可以劫持浏览器的请求,并进行响应;而Cache API可以对该请求的缓存进行增删改查。 注意,第一次请求是无法被劫持的,因为那时JS还没有被下载到浏览器中。但是随后的所有请求都可以被service worker劫持。