概述
缓存的重要性不言而喻,通过网络请求资源缓慢并且降低了客户端的用户体验,增添了服务端的负担。很多短期之内不会经常发生变化的资源文件没必要每次访问都想服务端进行数据请求,而缓存策略的使用就是为了改善客户端的呈现时间,降低服务端的负担。
对于HTTP的缓存机制来说,策略体现在HTTP的头部信息的字段上,而这些策略根据是否需要重新向服务器端发起请求可以分为强缓存和协商缓存两大类。接下来用UML时序图的形式来呈现这两大类缓存策略的大体过程。
Tips: Vscode 配合插件 plantUML画(写)UML图很爽。相比之前用的ProcessOn拖拽的形式,你只需要熟悉plantUML的语法,在你的电脑上安装一下java,Graphviz 的环境,不用操心样式的展现UML真心舒服。下面的图我就是用这个工具去画的,很推荐。
- 强缓存
强缓存紧密联系着一个缓存时间期限,当浏览器请求资源的时候会查看缓存中的资源是否存在并且确定该缓存的资源是否过了“保质期”,若没有超过保质期则将取得缓存中的资源进行下一步处理
- 协商缓存
可见协商缓存无论如何都会和服务器交互,比较强缓存稍微要复杂一点,但是二者是相辅相成并且可以共同存在的,强缓存优先级较高,意味着请求一个资源时会先比较强缓存的字段,如果命中则不会再执行接下来的协商缓存的过程。
接下来就是要介绍,两类缓存相关HTTP header相关字段的控制实现了。
细节
1.强缓存
与强缓存相关的HTTP header 的字段有两个 Expires
以及Cache-Control
- Expires
expires 字段规定了缓存的资源的过期时间,在此时间之前,缓存中的资源都是有效的,该字段的 value 是一个格林威治时间格式(GMT)的时间,即世界标准时间,js 通过 new Date().toUTCString()
可得到,形如 Tue, 27 Feb 2018 06:37:48 GMT
。他的缺点很明显,时间期限是服务器生成,存在着客户端和服务器的时间误差,固定时间,HTTP 1.0时的规范。相比较接下来介绍的cache-control
优先级较低。
- cache-control
该字段的值(默认为private):
其中最常用的值max-age单位为秒,对比expires
体现着一个相对时间,即多少秒后这个强缓存机制下的缓存资源失效。
public
和private
区别在于是否有中间商赚差价(是否允许CDN代理服务器缓存)
需要注意的一点是该字段值定义为no-cache
并不是说,不准使用缓存,而是需要走接下来的优先级相对较低的另一类--协商缓存。真正决定不用缓存内的资源是将该值定义为no-store
2.协商缓存
协商缓存是通过客户端和服务端进行HTTP通信时,所在响应头和请求头中互相表达“暧昧”的,相互通气,互送缓存标识。
- Last-Modified 和 If-Modified-Since
第一次请求某一个资源时,由于一定不会走缓存,所以服务器端会在资源的响应头中加上一个形如Last-Modified:Mon, 26 Feb 2018 06:37:41 GMT
的字段告诉客户端浏览器,这个资源上次最后修改的时间;刷新页面再次请求,这时候的协商缓存会在请求头中加上一个形如If-Modified-Since:Mon, 26 Feb 2018 06:37:41 GMT
,字面翻译就是,是否在上个“暧昧”时间后修改了,值毫无疑问是服务端上一次响应给他的时间,让服务器去判断是否在此时间之后资源内容发生了变化。
整个过程也很简单,最后的结果也很简短。如果服务端发现改变了资源,就伴着200的 statuscode 和新鲜的资源给到客户端,若是没有修改,304 Not Modified让客户端从缓存中取。
- Etag 和 If-None-Match
同样,第一次客户端请求一个资源文件时,服务端随资源在响应头部中甩来一个字段 Etag ,形如ETag:W/"1823823287"
该字段的值是该资源在服务器端的唯一标识,生成的Etag值的策略有服务端决定,总之是资源的一个唯一的标识。资源发生变化则该值也发生变化。下一次客户端请求同一个资源的时候,在请求头将这次得到的值放在请求头中一个叫 If-None-Match 的字段中甩给服务端。
整个过程也很简单,最后的结果也很简短。如果服务端发现改变了资源,就伴着200的 statuscode 和新鲜的资源给到客户端,若是没有修改,304 Not Modified让客户端从缓存中取。
上述两个方式中,Etag 和 If-None-Match的优先级要高于Last-Modified 和 If-Modified-Since,进而会衍生出一个思考,二者相比功能相同,但是表达形式决定了 Etag 解决了 Last-Modified 存在的一些问题,比如Last-Modified 是比较时间,精确到秒,若是毫秒级的改变则没法兼顾,存在着周期性更改的资源,然而有可能资源本身的内容并没有改变,那如果重新请求响应意义并不是那么的大。所以不难理解Etag具有高优先级有他的合理之处。
简单操练一下
使用Node的Express框架能够轻松的观察到这些字段带来的影响实验。接下来通过使用Express4.x依旧保留下来的中间件express.static搭建一个建议的静态文件响应服务器。
其中 public 文件夹里面放着一个主页面和两张不一样的图片用来改变
//app.js
const express = require('express')
const path = require('path')
const app = express()
app.use(express.static(path.join(__dirname, "public"), {
etag: false,
lastModified:false,
cacheControl: false,
setHeaders:function(res,path,stat) {
res.set({
expires: new Date(Date.now() + 60000),
})
}
}))
app.listen(3000, () => {
console.log('App listening on port 3000!');
});
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
<img src="./stream.jpg">
</body>
</html>
详细的express.static的细节请参考文档,这里不过多赘述。
之前我们提到过,expires这个选项是优先级相比较其他的控制选项,所以想要种 expires 观察效果是记得向上述app.js一样把其他的都关掉,因为他们都是默认为 true
的。
然后启动服务器,访问3000端口,打开开发人员工具,你就会看到如下被种了expires
的响应头了。
之后,你改变图片的名称,调换两张图片的名字,你就会发现在 expires 的时间到之前图片都不会发生变化,并且可见,他是从缓存中取得
接下来,改变app.js
app.use(express.static(path.join(__dirname, "public"), {
etag: false,
maxAge:30000,
lastModified:false,
setHeaders:function(res,path,stat) {
res.set({
expires: new Date(Date.now() + 600000),
})
}
}))
maxAge
的单位是毫秒,此时由于优先级的关系,覆盖掉 expires 之后在30秒之内交换图片名称,重新刷新浏览器将不会看到资源发生变化,知道maxAge
的时间到。对应的响应头如下:
继续修改app.js:
app.use(express.static(path.join(__dirname, "public"), {
etag: false,
maxAge:30000,
cacheControl: false,
lastModified:true,
}))
你需要删掉对强缓存的设置,因为强缓存的设置会比协商缓存被先执行,同样的操作你将看到接下来的响应头和请求头。
随后,你将上述优先级最高的 Etag 字段改为 true
,得到的响应头和请求头信息如下:
此外你也会发现如下情况:
这种现象就解释了,协商缓存虽然总是要访问服务器,但是当资源没有变动的时候,服务端只会返回带着304状态码的很小的一个头部,并不会像第一次携带文件资源那么大了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。