背景
一个H5的活动首页,包含了诸多关联子页面,文件是放在服务器NGINX的静态目录下,自发布上线之后,可能因为疫情原因要临时关闭预约通道页,替换为一个新写的通知页面,也或者是页面版本升级要修复一些问题,但是由于 浏览器缓存、CDN、代理缓存服务器的一些问题没处理好,服务器上更新替换相关页面文件后,客户端用户不一定能实时看到最新的内容,从而导致 资源服务器
到 用户客户端
两端信息不同步产生一些麻烦。这便是计算机网络中 HTTP协议 控制下的 缓存机制
所决定的。
优点
- 客户端加载速度快
- 降低服务器和网络传输压力
缺点
- 服务端更改内容不能及时同步到客户端
- 不同应用场景需要搭配不同方案实现
如果你的应用场景需要及时让客户端用户看到最新修改的网页内容,那么你需要了解并使用HTTP缓存相关的知识。
RFC7234
在 RFC7234 第 5 章节中,定义了与 缓存
相关的 HTTP/1.1 标准头字段的语法和语义。
相关头字段
所属头 | 字段名称 | 含义 | 备注 |
---|---|---|---|
Response | Age | 对象在缓存代理中存贮的时长,以秒为单位。接近 0 表示从服务器新获取不久 | HTTP/1.1 |
Response | Cache-Control | 被用于在http请求和响应中,通过指定指令来实现缓存机制。详见下文 | |
Response | Expires | 过期时间 | HTTP/1.0 |
Response | Pragma | Pragma: no-cache 与 Cache-Control: no-cache 效果一致。强制要求缓存服务器在返回缓存的版本之前将请求提交到源头服务器进行验证。兼容 HTTP/1.0 客户端 |
HTTP/1.0 |
Response | Warning | 有关该消息的状态可能出现的问题的信息,可能是多个标题 | HTTP/1.1 |
Response | ETag | 资源的唯一标识符,类似文件md5 | |
Request | If-None-Match | 服务端取If-None-Match 跟当前版本的ETag 比较,如果不一致,响应 200 并返回内容,一致响应 304 不返回内容 |
|
Request | If-Match | 常用于POST 和PUT 请求 如果和Etag 匹配,则允许提交响应200 ,不匹配则响应412 不提交数据 |
|
Response | Last-Modified | 服务端的最后修改时间 | |
Request | If-Modified-Since | 如果从这个时间后修改了,服务器响应200 和内容,否则响应304 ,不返回内容 |
|
Request | If-Unmodified-Since | 如果从这个时间后没有修改,服务器响应200 和内容 |
|
Response | Immutable | 不可变的内容,告诉客户端不用再去服务器请求 |
相关状态码
状态码 | 含义 |
---|---|
200 | Ok (成功) |
304 | Not Modified (未修改) |
412 | Precondition Failed (前提条件失败) |
浏览器缓存分类
- 协商缓存
- 强制缓存
协商缓存
顾名思义,需要客户端跟服务器协商共同解决缓存问题。
协商缓存可使用的头组合有 Last-Modified/if-Modify-Since
和 ETag/if-None-Match
需要服务端手动处理判断请求头字段,以 node 部分代码为例:
1、Last-Modified/if-Modify-Since
// 获取 if-modified-since 这个请求头
const ifModifiedSince = req.headers['if-modified-since'];
// 获取最后修改的时间
const lastModified = stat.ctime.toGMTString();
// 判断两者是否相等,如果相等返回304读取浏览器缓存。否则的话,重新发请求
if (ifModifiedSince === lastModified) {
res.writeHead(304);
res.end();
} else {
res.setHeader('Content-Type', mime.getType(filepath));
res.setHeader('Last-Modified', stat.ctime.toGMTString());
// fs.createReadStream(filepath).pipe(res);
}
复制代码
2、ETag/if-None-Match
//Etag 实体内容,他是根据文件内容,算出一个唯一的值。
let md5 = crypto.createHash('md5')
let rs = fs.createReadStream(abs)
let arr = [];
// 你要先写入响应头再写入响应体
rs.on('data', function(chunk) {
md5.update(chunk); arr.push(chunk)
})
rs.on('end', function() {
let etag = md5.digest('base64');
if(req.headers['if-none-match'] === etag) {
console.log(req.headers['if-none-match'])
res.statusCode = 304;
res.end()
return
}
res.setHeader('Etag', etag)
// If-None-Match 和 Etag 是一对, If-None-Match是浏览器的, Etag是服务端的
res.end(Buffer.concat(arr))
})
复制代码
两种组合对比,
方法 | 优点 | 缺点 | 优先级 |
---|---|---|---|
Last-Modified/if-Modify-Since | 性能较好 | 1、以秒为单位,控制精度差,文件可能1s内修改多次;2、修改电脑时间会导致失效;3、服务器动态生成的文件替换,创建时间永远是更新时间,缓存失效; | 低 |
ETag/if-None-Match | 更加精准 | 1、每次请求都需要读取并计算文件hash值,性能较差;2、大文件一般要用文件名+最后修改时间组合生成etag; | 高 |
强制缓存
浏览器在加载资源的时候,会先根据本地缓存资源的header中的信息(Expires 和 Cache-Control)来判断是否需要强制缓存。如果命中的话,则会直接使用缓存中的资源。否则的话,会继续向服务器发送请求。
关联的头字段有 Expires
和 Cache-Control
1、Expires
表示缓存到期时间,是 当前时间+缓存时间,浏览器在未过期之前不需要再次请求。
服务端代码以 node 为例:
let { pathname } = url.parse(req.url, true);
let abs = path.join(__dirname, pathname);
res.setHeader('Expires', new Date(Date.now() + 20000).toGMTString());
fs.stat(path.join(__dirname, pathname), (err, stat) => {
if(err) {
res.statusCode = 404;
res.end('not found')
return
}
if(stat.isFile()) {
fs.createReadStream(abs).pipe(res)
}
})
复制代码
2、Cache-Control
键值列表
键 | 含义 | 备注 |
---|---|---|
max-age | 最大有效时间(单位s),是相对时间,在该时间内,客户端不需要向服务器发送请求 | response request |
no-store | 禁止使用缓存,每一次都要重新请求数据。 |
response request |
no-cache | 非“不缓存”,需要进行协商缓存,不管本地副本是否过期,使用资源副本前,一定要到源服务器进行副本有效性校验。(与etag联合控制)。 | response request |
private | 私密,只能被客户端用户浏览器缓存,不允许中继代理服务器、CDN等缓存 | response |
public | 共享,客户端浏览器、CDN、代理服务器 都可缓存 | response |
max-stale | 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。 | request |
min-fresh | 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。 | request |
must-revalidate | 告诉浏览器、缓存服务器,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。 | response |
proxy-revalidate | 指令要求所有的缓存服务器在接收到客户端带有该指令的请求返回响应之前,必须再次验证缓存的有效性。 | response |
no-transform | 无论是在请求还是响应中,缓存都不能改变实体主体的媒体类型 | response request |
服务端代码 以 node 为例:
res.setHeader('Cache-Control', 'max-age=20')
复制代码
res.setHeader('Cache-Control', 'no-store')
复制代码
res.setHeader('Cache-Control', 'no-cache')
复制代码
多个值用逗号分隔
res.setHeader('Cache-Control', 'public, max-age=1024')
复制代码
注意:
- 禁止缓存应该使用
no-store
而不是no-cache
- 指定
no-cache
或max-age=0
表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。这样既可以保证内容的有效性,又可以保证能获取到最新的内容。
对比
字段 | 优点 | 缺点 | 备注 | 优先级 |
---|---|---|---|---|
Expires | 兼容性较好、简单易用 | 使用绝对时间,当服务器的时间和客户端的时间不一样的情况下,缓存失效 | HTTP 1.0 |
低 |
Cache-Control | 解决了绝对时间问题、可用指令丰富 | 不兼容HTTP 1.0客户端、使用max-age 不等于0时,仍然存在过期前修改文件客户端无法获取最新内容 |
HTTP 1.1 |
高 |
总结
如果需要完全禁止缓存,直接使用 强制缓存 Cache-Control: no-store
指令控制。如果要优化性能和实现缓存功能,则根据具体的业务场景,搭配新版的 协商缓存 Etag
和 强制缓存 Cache-Control
共同去控制和实现。
涉及到资源要使用的 CDN
或者 代理服务器
,则需要丰富的控制指令组合实现...
附加相关配置
在 location / {} 内添加如下配置,
add_header Cache-Control no-store,no-cache,must-revalidate;`
add_header Pragma no-cache;`
add_header Expires 0;`
复制代码
网页头部添加信息描述
<meta http-equiv="Expires" content="0">`
<meta http-equiv="Pragma" content="no-cache">`
<meta http-equiv="Cache-control" content="no-store,no-cache,must-revalidate">`
<meta http-equiv="Cache" content="no-cache">`
复制代码
资源文件(css|js)链接添加参数随机数
$("link,script").each(function () {
var t = Math.random().toFixed(4);
if ($(this).attr("src")) {
var $src = $(this).attr("src");
$(this).attr("src", $src + "?v=" + t)
} else if ($(this).attr("href")) {
var $href = $(this).attr("href");
$(this).attr("href", $href + "?v=" + t)
}
})
复制代码
参考文档
结束语
雄关漫道真如铁, 而今迈步从头越。从头越, 苍山如海, 残阳如血。
我是 darifo ,一名兴趣广泛的全栈工程师,欢迎私信一起交流学习~
原创文章,掘金独发,转载请注明出处,谢谢!
以上,如有错误,敬请批评指正!