说起Web缓存, 缓存行为不就是在家中冰箱里存储水果、蔬菜、肉类等食物嘛; 编程世界的行为应该来源于生活! 缓存的目的就是为了快速、方便;
做饭时能快速使用,想吃的时候可以顺手拿出来,这不就是网站的快吗;食品也有保质期不就是缓存的过期时间吗,当然也并不是所有食物都能(需要冰箱存储),
这个时候就需要从市场购买新鲜食物,这不就是要从服务器获取最新资源嘛
缓存你真的懂吗?
以下内容部分抄自《前端开发-核心知识进阶》一书! 资质平庸,就做个抄书仔吧,抄书不算偷吧,哈哈; 还有很多来源于网络, 向分享的朋友致敬!
缓存的概念
-
缓存是一个宽泛的概念,尤其Web缓存,可以分为很多种,如数据库缓存、服务器缓存、CDN缓存、HTTP缓存。甚至一个函数的执行结果都可以被缓存
-
HTTP缓存是用于临时存储(缓存)Web文档(如HTML页面和图像),以减少服务器延迟的一种信息技术。HTTP缓存系统会将通过该系统缓存的文档的副本保存下来,如果请求满足某些条件,则可以由缓存
内容来返回请求结果 -
《HTTP权威指南》中这样介绍缓存: 在前端开发中,性能一直是被大家所重视的一点,然而判断一个网站性能如何最直观的方法就是看网页打开速度。 其中,提供网页打开速度的一个方式就是使用缓存
。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,因此可以减少带宽,降低网络负荷
缓存带来的好处
- 使网页加载和呈现速度更快
- 由于减少不必要的数据传输,因而可以节省网络流量和带宽
- 减少了服务器的负担,提升了网站性能(对于只有很少用户访问的情况下,服务器的负担没有明显效果;但是在高并发的场景下,使用缓存对于减少服务器的负担就非常有帮助了)
缓存分类
按缓存位置分类
浏览器的资源缓存可以分为硬盘缓存和内存缓存两类。当首次访问网页时,资源文件被缓存在内存中,同时也会在本地磁盘中保留一份副本。
当用户刷新页面时,如果缓存的资源没有过期,就可以直接从内存中读取数据并加载。当用户关闭页面后,当前页面缓存在内存中的资源就会被清空。当用户再一次访问页面时,如果资源文件的缓存没有过期,就可以从本地磁盘加载数据并再次缓存到内存中。
- 内存缓存 memory cache
- 硬盘缓存 disk cache
- service worker
- …
按缓存策略分类
缓存策略是理解缓存的最重要的一环;说到底,缓存的核心就是解决什么时候使用缓存、什么时候更新缓存的问题。
- 强缓存
- Expires
- Cache-control
- no-cache
- max-age
- no-store
- public
- private
- 协商缓存
-
Etag
- Etag
- If-None-Match
-
Last-modified
- Last-Modified (位于响应头)
- If-Modified-Since (位于请求头)
-
http.createServer((req, res)=>{
let { pathname } = url.parse(req.url, true);
let absoultePath = path.join(__dirname, pathname);
fs.stat(path.join(__dirname, pathname), (err, stat) => {
// 路径不存在
if(err){
res.statusCode = '404';
res.end("Not Found");
return;
}
if(stat.isFile()){
res.setHeader("Last-Modified", stat.ctime.toGMTString());
if(req.headers['if-modified-since'] === stat.ctime.toGMTString()){
res.statusCode = '304';
res.end();
return;
}
}
});
});
我们该如何合理的应用缓存呢?
-
强缓存的优先级最高,并且在缓存有效期内浏览器不会因为资源的改动而发送请求,因此强制缓存的使用适用于大型且不易修改的资源文件,例如:第三方的CSS、JS或图片资源。如果想提高缓存的灵活性,也可以为文件名加上hash标识进行版本的区分。
-
协商缓存灵活性高,适用于数据的缓存,采用etag标识比对文件内容是否发生变化的灵活性最高,也最为可靠。对于数据缓存,我们可以重点考虑将数据缓存在内存中,因为内存加载速度最快,并且数据体积小。
-
强缓存如果生效,不需要再和服务器发生交互,而协商缓存不管是否有效,都需要与服务端发生交互
-
如果两类缓存同时存在,强缓存优先级高于协商缓存,也就是说,当执行强缓存的规则时,如果缓存生效,直接使用缓存,不再执行协商缓存规则;
缓存对比
强制缓存
强制缓存,在缓存数据未生效的情况下,可以直接使用缓存数据; 那么浏览器如果判断缓存数据是否失效呢? 在没有缓存数据的时候,浏览器向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应头中
协商缓存(也有叫对比缓存)
- 需要进行比较,然后判断是否可以使用缓存
- 浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中
- 再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据
请求流程
第一次请求
第二次请求
Etag
ETag是实体标签的缩写,根据实体内容生成的一段hash字符串,可以标识资源的状态; 当资源发生改变时,ETag也随之发生变化;ETag是Web服务端产生的,然后发给浏览器客户端
1、客户端要判断缓存是否可用,可以先获取缓存中文档的ETag, 然后通过If-None-Match发送请求给Web服务器询问此缓存是否可用
2、服务器收到请求,将服务器中此文件的ETag,跟请求头中的If-None-Match相比较,如果值是一样的,说明缓存还是最新的,Web服务器将发送304 Not Modified响应码给客户端表示缓存未修改过,可以使用
3、如果不一样则Web服务器将发送该文档的最新版本给浏览器客户端
let http = require('http');
let fs = require('fs');
let path = require('path');
let mime = require('mime');
let crypto = require('crypto');
http.createServer(function (req, res) {
let file = path.join(__dirname, req.url);
fs.stat(file, (err, stat) => {
if (err) {
sendError(err, req, res, file, stat);
} else {
let ifNoneMatch = req.headers['if-none-match'];
let etag = crypto.createHash('sha1').update(stat.ctime.toGMTString() + stat.size).digest('hex');
if (ifNoneMatch) {
if (ifNoneMatch == etag) {
res.writeHead(304);
res.end();
} else {
send(req, res, file, etag);
}
} else {
send(req, res, file, etag);
}
}
});
}).listen(8080);
function send(req, res, file, etag) {
res.setHeader('ETag', etag);
res.writeHead(200, { 'Content-Type': mime.lookup(file) });
fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, etag) {
res.writeHead(400, { "Content-Type": 'text/html' });
res.end(err ? err.toString() : "Not Found");
}
通过最后修改时间来判断缓存是否可用
1、Last-Modified: 响应时(服务端)告诉客户端此资源的最后修改时间( 响应头:Last-Modified: Fri, 21 Jul 2023 03:18:19 GMT)
2、If-Modified-Since: 当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向服务器请求时带上头If-Modified-Since(请求头:If-Modified-Since: Fri, 21 Jul 2023 03:18:19 GMT)
3、服务器收到请求后发现有头If-Modified-Since则将其与被请求资源的最后修改时间进行比对,如果最后修改时间比较新,说明资源被修改过,则响应最新的资源内容并返回200状态码
4、如果最后修改时间和If-Modified-Since一样,说明资源没有修改,则响应304表示未更新,告知浏览器继续使用所保存的缓存文件
let http = require('http');
let fs = require('fs');
let path = require('path');
let mime = require('mime');
http.createServer(function (req, res) {
let file = path.join(__dirname, req.url);
fs.stat(file, (err, stat) => {
if (err) {
sendError(err, req, res, file, stat);
} else {
let ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince) {
if (ifModifiedSince == stat.ctime.toGMTString()) {
res.writeHead(304);
res.end();
} else {
send(req, res, file, stat);
}
} else {
send(req, res, file, stat);
}
}
});
}).listen(8080);
function send(req, res, file, stat) {
res.setHeader('Last-Modified', stat.ctime.toGMTString());
res.writeHead(200, { 'Content-Type': mime.getType(file) });
fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, stat) {
res.writeHead(400, { "Content-Type": 'text/html' });
res.end(err ? err.toString() : "Not Found");
}
最后修改时间存在问题
1、某些服务器不能精确得到文件的最后修改时间, 这样就无法通过最后修改时间来判断文件是否需要更新了
2、某些文件的修改非常频繁,在秒以下的时间内进行修改, Last-Modified只能精确到秒
3、一些文件的最后修改时间改变了,但是内容并未发生改变, 我们不希望客户端认为这个文件已经修改了
4、如果同样的一个文件位于多个CDN服务器上的时候内容虽然一样,修改时间不一样
如何干脆不发请求
- 浏览器会将文件缓存到Cache目录,第二次请求时浏览器会先检查Cache目录下是否含有该文件;如果有并且还没到Expires设置的时间,即文件还没有过期,那么此时浏览器将直接从Cache目录中读取文件,而不再发送请求
- Expires是服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次向服务器请求,这是HTTP1.0的内容;现在浏览器均默认使用HTTP1.1,所以基本可以忽略
- Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据,如果同时设置的话,其优先级高于Expires
Cache-Control Cache-Control:private, max-age=60, no-cache
- private 客户端可以缓存
- public 客户端和代理服务器都可以缓存
- max-age=60 缓存内容有效期为60秒
- no-cache 需要使用协商缓存验证数据,强制向源服务器再次验证
- no-store 所有内容都不会缓存,强制缓存和协商缓存都不会触发
let http = require('http');
let fs = require('fs');
let path = require('path');
let mime = require('mime');
let crypto = require('crypto');
http.createServer(function (req, res) {
let file = path.join(__dirname, req.url);
console.log(file);
fs.stat(file, (err, stat) => {
if (err) {
sendError(err, req, res, file, stat);
} else {
send(req, res, file);
}
});
}).listen(8080);
function send(req, res, file) {
let expires = new Date(Date.now() + 60 * 1000);
res.setHeader('Expires', expires.toUTCString());
res.setHeader('Cache-Control', 'max-age=60');
res.writeHead(200, { 'Content-Type': mime.lookup(file) });
fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, etag) {
res.writeHead(400, { "Content-Type": 'text/html' });
res.end(err ? err.toString() : "Not Found");
}
缓存和浏览器操作
缓存中最重要的一环是浏览器,常见的浏览器行为对应哪些缓存行为呢? 大概如下(不同厂商浏览器或浏览器不同版本的引擎可能会有差别):
- 当用户使用Ctrl + F5快捷键强制刷新页面时,浏览器会直接从服务器加载网页信息,跳过强缓存和协商缓存
- 当用户仅仅使用F5快捷键刷新网页时,浏览器的加载过程会跳过强缓存,但是仍然会进行协商缓存
浏览器相关操作 | Expires/Cache-Control | Last-Modified/etag |
---|---|---|
在地址栏中按回车键 | 有效 | 有效 |
页面跳转 | 有效 | 有效 |
新开窗口 | 有效 | 有效 |
浏览器前进、后退 | 有效 | 有效 |
浏览器刷新 | 无效 | 有效 |
强制刷新 | 无效 | 无效 |
缓存相关面试题
1、如何禁止浏览器不缓存静态资源
在实际工作中,很多场景都需要禁用浏览器缓存。比如,可以使用Chrome隐私模式,在代码层面设置相关请求头,设置如下:
a: 在请求头设置Cache-Control
Cache-Control: no-cache,no-store,must-revalidate
b: 使用meta标签来声明缓存规则
<meta http-equiv="cache-control" content="no-cache,no-store,must-revalidate">
c:给请求的资源添加一个版本号,如下所示:
<link rel="stylesheet" type="text/css" href="./asstes.css?vision=1.8.9" />
2、设置cache-Control: max-age=0
请求/响应头会有什么效果
上面的响应头属于强缓存,因为max-age的值设置为0,所以浏览器必须发送请求重新验证资源。这时,浏览器会根据协商缓存机制进行缓存,并可能返回200 或 304
3、设置Cache-Control: max-age=60, must-revalidate
请求/响应头会有什么效果
如果资源在60秒内会再次被访问,那么根据强缓存机制可以直接返回缓存资源内容;如果超过60s,则必须发送网络请求到服务器端,以验证资源的有效性
4、大厂为什么都不怎么用etag
大厂多使用负载均衡的方式来调度HTTP请求。因此,同一个客户端对同一个页面的多次请求很可能被分配到不同的服务器来响应,而根据etag的计算原理,不同的服务器
有可能在资源内容没有变化的情况下,计算出不一样的etag,而使缓存失效
结语
- 生活再累都要给自己一点阳光,最起码要好好吃饭,在这个不确定、不放心的时代,多在家吃顿饭何尝不是一件幸福的事情!