经过几次校招中的面试,发现http缓存这个知识点一直被拿来当成面试题,我第一次遇到这个问题时被问得一脸懵,于是便总结出以下的内容
在任何一个前端项目中,访问服务器获取数据都是很常见的事情,但是如果相同的数据被重复请求了不止一次,那么多余的请求次数必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。如果用户使用的是按量计费的方式访问网络,那么多余的请求还会隐性地增加用户的网络流量资费。因此考虑使用缓存技术对已获取的资源进行重用,是一种提升网站性能与用户体验的有效策略。
缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免重新向服务器发起资源请求。
缓存的技术种类有很多,比如代理缓存、浏览器缓存、网关缓存、负载均衡器及内容分发网络等,它们大致可以分为两类:共享缓存和私有缓存。共享缓存指的是缓存内容可被多个用户使用,如公司内部架设的Web代理;私有缓存指的是只能单独被用户使用的缓存,如浏览器缓存。
HTTP 缓存应该算是前端开发中最常接触的缓存机制之一,它又可细分为强制缓存与协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。下面就来具体看HTTP缓存的具体机制及缓存的决策策略。
强缓存
// 响应头通过设置Expires
res.writeHead(200, {
// 缺点:客户端时间和服务器时间可能不同步
Expires: new Date('2021-5-27 21:40').toUTCString()
})
// 通过设置Cache-Control
res.writeHead(200, {
'Cache-Control': 'max-age=5' // 滑动时间,单位是秒
})
协商缓存
// 通过设置文件修改时间来进行协商缓存判断
const {
mtime } = fs.statSync('./img/03.jpg')
const ifModifiedSince = req.headers['if-modified-since']
if (ifModifiedSince === mtime.toUTCString()) {
// 缓存生效
res.statusCode = 304
res.end()
return
}
const data = fs.readFileSync('./img/03.jpg')
// 告诉客户端该资源要使用协商缓存
// 客户端使用缓存数据之前问一下服务器缓存有效吗
// 服务端:
// 有效:返回 304 ,客户端使用本地缓存资源
// 无效:直接返回新的资源数据,客户端直接使用
res.setHeader('Cache-Control', 'no-cache')
// 服务端要下发一个字段告诉客户端这个资源的更新时间
res.setHeader('last-modified', mtime.toUTCString())
res.end(data)
// last-modified的不足
// 见下面
// 通过文件内容是否变化进行协商缓存判断
const data = fs.readFileSync('./img/04.jpg')
// 基于文件内容生成一个唯一的密码戳
const etagContent = etag(data)
const ifNoneMatch = req.headers['if-none-match']
if (ifNoneMatch === etagContent) {
res.statusCode = 304
res.end()
return
}
// 告诉客户端要进行协商缓存
res.setHeader('Cache-Control', 'no-cache')
// 把该资源的内容密码戳发给客户端
res.setHeader('etag', etagContent)
res.end(data)
通过 last-modified 所实现的协商缓存能够满足大部分的使用场景,但也存在两个比较明显的缺陷:
- 首先它只是根据资源最后的修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但内容并没有发生任何变化,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。这无疑会造成网络带宽资源的浪费,以及延长用户获取到目标资源的时间。
- 其次标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么上述通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。
其实造成上述两种缺陷的原因相同,就是服务器无法仅依据资源修改的时间戳来识别出真正的更新,进而导致重新发起了请求,该重新请求却使用了缓存的 Bug 场景。
基于ETag的协商缓存
为了弥补通过时间戳判断的不足,从 HTTP 1.1 规范开始新增了一个 ETag 的头信息,即实体标签(Entity Tag)。
其内容主要是服务器为不同资源进行哈希运算所生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的 ETag 标签值就会不同,因此可以使用 ETag 对文件资源进行更精准的变化感知。