在浏览器中使用fetch或者XHRHttpRequest发送请求时,浏览器会根据响应中header里的一些字段做一些缓存,来减少http请求。
内存缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭页面,内存中的缓存也就被释放了。
磁盘缓存,比起memory cache,它的读取速度更慢,不过容量跟存储时效比memory cache更强。
下载的资源是使用memory cache和disk cache进行存储是浏览器自行决定的,开发者并不能进行干预。
而对于浏览器怎么决定使用哪种方式存储,网上倒是没有统一的答案,只能大概总结成以下两点
service worker 的缓存与浏览器其他内建的缓存机制不同,它给予开发者自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。使用 service worker的话,传输协议必须为 https。因为 service worker 中涉及到请求拦截,所以必须使用 https 协议来保障安全。
service worker是运行在浏览器背后的独立线程,独立于当前页面,提供了那些不需要与页面交互的功能在网页背后悄悄执行的能力。
其中最重要的作用之一就是离线资源缓存.
当service worker监听fetch事件以后,对应的请求都会经过service worker。通过chrome的network工具,可以看到此类请求会标注:from service worker。如果service worker中出现了问题,会导致所有请求失败,包括普通的html文件。所以service worker的代码质量、容错性一定要很好才能保证web app正常运行。
而浏览器读取的缓存的优先级为
memory cache > service worker > disk cache
如果都没有缓存的话才会发送请求去去获取资源。
push cache是 http/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话中存在,一旦会话结束就被释放,并且缓存时间也很短暂。不过由于push cache还存在一个 bug 以及大多数的浏览器使用的还是http/1.1,因此还未被推广使用,感兴趣的可以去看看HTTP/2 push is tougher than I thought 这篇文章。
浏览器遵循http缓存的定义来实现缓存策略,分为强缓存和协商缓存。当客户端请求某个资源时,获取缓存的流程如下
http header 判断它是否命中强缓存,如果命中,则直接从本地获取缓存资源,不会发请求到服务器;request header验证这个资源是否命中协商缓存,称为http再验证,如果命中,服务器将请求返回,但不返回资源,而是告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;f5刷新网页时,跳过强缓存,但是会检查协商缓存;Expires(该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间)
使用node写一个demo:
const server = http.createServer((req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "http://localhost:8081",
"Access-Control-Allow-Credentials": true,
"Content-Type": "application/json;charset=utf-8",
Expires: new Date(
new Date().getTime() + 60 * 60 * 24 * 365 * 1000
).toUTCString(),
});
res.end(JSON.stringify({ code: 0, data: "cached for one year" }));
});
Cache-Control:max-age(该字段是 http1.1 的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒)
相比起Expires只能设置时间,Cache-Control具有更多属性
max-age: 设置缓存的最大的有效时间,单位为秒(s)。max-age会覆盖掉Expires;
const server = http.createServer((req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "http://localhost:8081",
"Access-Control-Allow-Credentials": true,
"Content-Type": "application/json;charset=utf-8",
"Cache-Control": "max-age=31536000",
});
res.end(JSON.stringify({ code: 0, data: "cached for one year" }));
});
// 同时设置cache-control: max-age 跟 expires
const server = http.createServer((req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "http://localhost:8081",
"Access-Control-Allow-Credentials": true,
"Content-Type": "application/json;charset=utf-8",
// expires设置5s秒后过期,cache-control:max-age设置一年过期,在浏览器的network可以看到5秒后其实并没有过期
Expires: new Date(new Date().getTime() + 5000).toUTCString(),
"Cache-Control": "max-age=31536000",
});
res.end(JSON.stringify({ code: 0, data: "cached for one year" }));
});
s-maxage: 只用于共享缓存,比如CDN缓存(s -> share)。与max-age 的区别是:max-age用于普通缓存,
而s-maxage用于代理缓存。如果存在s-maxage, 则会覆盖max-age 和 Expires(浏览器并不处理该字段);
public: 响应会被缓存,并且在多用户间共享(包括http请求返回时中间一些代理服务器以及发出请求的客户端浏览器,都可以对返回内容进行缓存操作)。默认是public;
private: 响应只作为私有的缓存,不能在用户间共享(只有发起请求的浏览器才可以进行缓存)。如果要求http认证,响应会自动设置为private
no-cache: 可以在本地进行缓存(协商缓存,表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存),但每次发请求时,都要向服务器进行验证,如果服务器允许,才能使用本地缓存。
no-store: 绝对禁止缓存。
max-stale:能容忍的最大过期时间。max-stale指令标示了客户端愿意接收一个已经过期了的响应。如果指定了max-stale的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何age的响应(在客户端中发送,服务端判断使用)
min-fresh:能够容忍的最小新鲜度。min-fresh标示了客户端不愿意接受新鲜度不多于当前的age加上min-fresh设定的时间之和的响应。(在客户端中发送,服务端判断使用)
must-revalidate: 如果页面过期,则去服务器进行获取。
在以上这些属性中,可以同时设置多个(用,字符隔开即可),不过当属性冲突时,属性之间也是有优先级的,而在其中no-store的优先级最高。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,这时候就需要用到协商缓存策略。
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。主要有以下两种情况:
304 和 Not Modified200 和请求结果Last-Modified: 值为资源最后更新时间,随服务器 response 返回,即使文件改回去,日期也会变化;
If-Modified-Since: 浏览器再次请求这个资源时,如果第一次请求的响应中携带有Last-Modified字段,就会带上if-Modified-Since,值是Last-Modified的值。通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存;
const stat = fs.statSync(__dirname + "/public/index.css");
if (req.headers["if-modified-since"] === stat.ctime.toUTCString()) {
res.writeHead(304, {});
res.end();
return;
}
const file = fs.readFileSync(__dirname + "/public/index.css");
res.writeHead(200, {
"Access-Control-Allow-Origin": "*",
"Content-Type": "text/css",
"Last-Modified": stat.ctime.toUTCString(),
});
res.end(file);
使用Last-Modified存在一些问题:
Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源ETag:服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成If-None-Match:览器再次请求这个资源时,如果第一次请求的响应中携带有ETag字段,就会带上if-None-Match,值是ETag的值。 服务器通过比较请求头部的 If-None-Match 与当前资源的 ETag 是否一致来判断资源是否在两次请求之间有过修改,如果没有修改,则命中协商缓存// 使用crypto库来加密解析生成etag
const file = fs.readFileSync(__dirname + "/public/index.css");
const etag = crypto.createHash("md5").update(file).digest("hex");
if (req.headers["if-none-match"] === etag) {
res.writeHead(304, {
ETag: etag,
});
res.end();
return;
}
res.writeHead(200, {
"Access-Control-Allow-Origin": "*",
"Content-Type": "text/css",
ETag: etag,
});
res.end(file);
在服务端设置后返回304状态码,即使body为空,浏览器也能正确解析,在network中显示状态码为304。 对于浏览器而言,如果多次访问都是未修改,那么浏览器可能会缓存在内存或者磁盘中(这时候状态码为 200),过一段时间才会重新请求而且有时候浏览器确实不处理 304,响应码还是 200,但是确实没从服务器中获取资源。