每日一句
You make me want to be a better person.
释义:你让我想成为一个更好的人。
什么是浏览器缓存?
百度:浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从[本地磁盘]显示文档,这样就可以加速页面的阅览。
优点:
- 减少了冗余的数据传输,节省了网费
- 减少了服务器的负担,大大提升了网站的性能
- 加快了客户端加载网页的速度
缺点:
资源如果有更改,会导致客户端不及时更新就会造成用户获取信息滞后。
浏览器缓存的分类
主要分为:强缓存(本地缓存)和协商缓存(弱缓存)
由上图可知:
在浏览器第一次发送请求后,需要再次发送请求时,浏览器会首先获取该资源缓存的header信息,然后根据expires和Cache-Control来判断是否命中强缓存,若命中则直接从缓存中获取资源,包括缓存的header信息,本次请求不会与服务器进行通信;
如果没有命中强缓存,浏览器会发送请求到服务器,该请求会携带第一次请求返回的有关缓存的header字段信息(Last-Modified/IF-Modified-Since、Etag/IF-None-Match),由服务器根据请求中的相关header信息来对比结果是否命中协商缓存。
协商缓存命中场景
Etag:
客户端会通过
If-None-Match头将先前服务器端返回的Etag发送给服务器,服务器会对比发过来的Etag是否与服务器的相同。相同时就将If-None-Match的值设为false,返回状态304,使用本地缓存;不同时就将If-None-Match的值设为true,返回状态为200,重新向服务器返回数据。
If-Modified-Since:
客户端还会通过If-Modified-Since将最后修改时间戳发送给服务器,服务器端通过这个时间戳判断是否是最新的,如果是,则返回304。如不是,则返回新的数据。
强缓存命中场景
利用http头中的`Expires`和`Cache-Control`两个字段来控制的,用来表示资源的缓存时间。
Expires 该字段是http1.0时的规范,它的值为一个绝对时间的GMT格式的时间字符串,代表资源的失效时间。如:
expires:Feb, 13 Mar 2021 14:55:00 GMT。在这时间之前都会命中强缓存。如果本地时间与服务器的时候偏差比较大,则会导致缓存混乱。
Cache-Control: 是http1.1中出现的,利用该字段的max-age来判断,这个值是一个相对时间。 如:
Cache-Control:max-age=3600
Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高
除了该字段还有其他的几个常用的值:
- no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
- no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
浏览器刷新时做了啥
当你点“刷新”按钮的时候,浏览器会在请求头里加一个“Cache-Control: maxage=0”。因为 max-age 是“生存时间”,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。
Ctrl+F5 的“强制刷新”又是什么样的呢?
它其实是发了一个“Cache-Control: no-cache”,含义和“max-age=0”基本一样,就看后台的服务器怎么理解,通常两者的效果是相同的。
为什么要有etag?
使用last-modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要etag呢?HTTP1.1中etag的出现主要是为了解决几个last-modified比较难解决的问题:
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新get;
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比如在说1s内修改了N次),if-modified-since能检查到的粒度是秒级的,这种修改无法判断;
- 某些服务器不能精确的得到文件的最后修改时间。
如何设置强缓存与协商缓存
- node 环境
强缓存
// 设置过期时间在30000毫秒,也就是30秒后
ctx.set('Expires', new Date(Date.now() + 30000))
// 设置300秒有效期
ctx.set('Cache-Control', 'max-age=300')
协商缓存
Last-Modified,If-Modified-Since
// 首先设置Cache-Control:no-cache, 使客户端不走强缓存
// 再判断客户端请求是否有带ifModifiedSince字段,没有就设置Last-Modified字段,并返回资源文件。如果有就用fs.stat读取资源文件的修改时间,并进行对比,如果时间一样,则返回状态码304。
ctx.set('Cache-Control', 'no-cache')
const ifModifiedSince = ctx.request.header['if-modified-since']
const fileStat = await getFileStat(filePath)
if (ifModifiedSince === fileStat.mtime.toGMTString()) {
ctx.status = 304
} else {
ctx.set('Last-Modified', fileStat.mtime.toGMTString())
ctx.body = await parseStatic(filePath)
}
etag、If-None-Match
etag的关键点在于计算资源文件的唯一性,这里使用nodejs内置的crypto模块来计算文件的hash值,并用十六进制的字符串表示。
crpto不仅支持字符串的加密,还支持传入buffer加密。
ctx.set('Cache-Control', 'no-cache')
const fileBuffer = await parseStatic(filePath)
const ifNoneMatch = ctx.request.headers['if-none-match']
const hash = crypto.createHash('md5')
hash.update(fileBuffer)
const etag = `"${hash.digest('hex')}"`
if (ifNoneMatch === etag) {
ctx.status = 304
} else {
ctx.set('etag', etag)
ctx.body = fileBuffer
}
- nginx环境
location /demo {
add_header Cache-Control "no-cache, no-store";
}
总结
合理的使用强缓存和协商缓存具体需要看项目的使用场景和需求。
像目前常见的单页面应用,因为通常打包都是新生成html与相应的静态资源依赖,所以可以对html文件配置协商缓存,而打包生成的依赖,例如js、css这些文件可以使用强缓存。或者只对第三方库使用强缓存,因为第三方库通常版本更新较慢,可以锁定版本。