在开发中不知道大家有没有遇到当文件资源过大时该如何处理呢?例如,我有一张很大很高清的背景图需要加载。
从网络分析中可以看出加载这张图片共花费了6秒多的时间,这在开发中应该是比较难以接受的,而对于类似这种静态资源利用http的缓存就可以很好的解决,所以就让我们来了解一下http中的缓存吧。
强制缓存
主要过程是客户端接收到带有强制缓存标识的响应头信息时,会对该资源进行强制缓存(存储的浏览器的内存中),如果浏览器再次访问该资源时,会先从缓存中获取,若判断所请求的目标资源有效且命中,则可直接从强制缓存中返回请求响应,无需与服务器再进行通信。
如何设置强制缓存
在HTTP协议中提供了两个头部信息可设置强制缓存
本文都是基于以下代码在node中利用http模块的环境下进行的测试:
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
http.createServer((req,res)=>{
const {pathname} = url.parse(req.url);
console.log(pathname);
if(pathname === '/'){
const file = fs.readFileSync(path.resolve(__dirname,'pages','index.html'));
res.end(file);
}
else if(pathname === '/images/bg.jpg'){
const file = fs.readFileSync(path.resolve(__dirname,'pages','images','bg.jpg'));
res.end(file);
}
else{
res.statusCode = 404;
res.end();
}
}).listen(5000,()=>{
console.log('http://127.0.0.1:5000')
})
expires:
它需要得到一个GMT格式的字符串作为它的值,该值会是资源缓存到期的具体时间。
// 将图片资源的访问路径修改响应头
let time = Date.now();
res.writeHead(200,{
'Expires':new Date(time+10000).toUTCString()
})
// toUTCString方法是将时间格式化为GMT时间
const file = fs.readFileSync(path.resolve(__dirname,'pages','images','bg.jpg'));
res.end(file);
在上代码中,我们将资源的过期时间设置为了10秒后,再修改响应头信息后重新访问路径就会发现之后如果中10秒内重新访问,服务器不会收到请求信息。
字段expires被设置为一个指定日期
无缓存的请求信息
在大小列中可得知资源来自缓存,服务器中也未收到图片的请求,页面响应速度得到极大提升。
cache-control:
该字段是HTTP1.1协议新增,主要目的是用于扩展和完善expires的功能,若要实习如上述的缓存10秒功能,我们只需要将响应头中的cache-control的值加上一个max-age就行了,表示的是最长过期时间,单位是秒。
res.writeHead(200,{
'cache-control':'max-age=10'
})
const file = fs.readFileSync(path.resolve(__dirname,'pages','images','bg.jpg'));
res.end(file);
cache-control还可以设置其他信息
no-store:表示禁止所有缓存策略,客户端每次请求都需要向服务器得到新的响应no-cache:表示强制进行协商缓存,对于每次请求不会判断缓存是否过期,而是直接与服务器协商验证缓存是否有效。public:表示响应资源可以被浏览器缓存也可以被代理服务器缓存。private:表示响应资源只能被浏览器缓存。与public为互斥属性,若没有显示的指定则默认为private。max-age:表示缓存的过期时间。s-maxage:它主要是单独为代理服务器设置缓存时间,且仅在设置了public属性值时才有效。
协商缓存
与强制缓存的区别就是在使用本地缓存之前,需向服务器发起一个请求来判断与之前协商保存的本地资源是否有效。
基于Last-Modified
last-modified表示最后修改时间,所以我们可以通过判断文件的最后修改时间来判断文件是否改变。在node中我们可以通过fs模块中方法来获取到文件的最后修改时间。在这个过程中我们需要在响应头中添加一条last-modified字段表示资源的最后修改日期(同样是GMT格式时间)。同时还需将cache-control设置为no-cache,表示强制进行协商缓存。当我们设置了该字段后在之后的请求头中会自动添加if-modified-since字段表示当前缓存的最后修改时间。
const file = fs.readFileSync(path.resolve(__dirname,'pages','images','bg.jpg'));
const {mtime} = fs.statSync(path.resolve(__dirname,'pages','images','bg.jpg')); // 可以得到文件的最后一次修改时间对象
const ifModifiedSince = req.headers['if-modified-since'];
if(ifModifiedSince === mtime.toUTCString()){
// 如果文件未修改则缓存生效
res.statusCode = 304;
res.end();
return;
}
res.setHeader('last-modified',mtime.toUTCString());
res.setHeader('Cache-Control','no-cache');
res.end(file);
我们可以通过对比服务器中的文件最后修改时间和客户端中所存储的时间是否相同,来判断是否采用缓存,如果相同返回状态码304(Not Modified),表示未修改采用缓存,如果判断未过则响应新的资源。
基于ETag
ETag也是HTTP1.1新增的一个头信息,表示实体标签(Entity Tag)。它的判断方式主要是通过计算hash值。服务器通过对资源进行哈希运算生成一个hash值(文件指纹),只要文件的编码出现差异对应的hash值也会不同,因此我们可以将ETag的值设为该hash值来资源判断是否更改。同样当客户端收到了带有ETag的响应头后,其下一次请求会额外的附加一个if-none-match字段信息,存储的是上传ETag的值,服务器可利用其来判断文件。
const file = fs.readFileSync(path.resolve(__dirname,'pages','images','bg.jpg'));
const etagContent = etag(file); // 利用node中导入etag模块进行计算
const ifNoneMatch = req.headers['if-none-match'];
if(ifNoneMatch === etagContent){
// 对比hash无误则缓存生效
res.statusCode = 304;
res.end();
return;
}
res.setHeader('ETag',etagContent);
res.setHeader('Cache-Control','no-cache');
res.end(file);
Last-Modified与ETag的取舍
当我们考虑用何种方式来进行协商缓存时,我们就应该知道它们各自有什么不足的地方。
Last-Modified:
- 时间精度:因为文件系统的时间精度可能是秒级或更低,这可能导致相同文件在短时间内多次修改,但是其
Last-Modified时间戳并未变化,因此客户端无法检测到实际内容的变化。 - 文件内容不变但时间变化:如果文件内容没有实际变化(例如空文件),但是文件的
Last-Modified时间戳变了,客户端会错误地认为文件内容发生了变化,导致不必要的资源传输。
ETag:
- 计算成本:
ETag通常是服务器根据资源内容生成的唯一标识符,所以要计算所有文件信息,计算ETag会增加服务器的计算负载,特别是对于大文件或动态生成的内容。 - 文件系统不同步:对于分布式系统或者负载均衡环境下的多个服务器,可能会导致相同内容在不同服务器生成不同的
ETag,从而降低了缓存的命中率。
缓存所解决的问题
不知道各位在了解完http的缓存机制后是否知道缓存到底是解决了什么问题呢。
- 提升用户体验: 这是最显而易见的,缓存可以显著提升用户访问网站的速度和体验,因为用户能更快速地加载页面和资源,减少了等待时间和页面加载时间。
- 减少网络延迟和带宽消耗: 缓存使得客户端可以在本地存储资源的副本,避免了每次请求都需要从服务器获取资源。这样可以显著减少请求的网络延迟和整体的带宽消耗。
- 减轻服务器负载: 当资源可以从缓存中获取时,服务器可以避免生成或者传输资源,减少了服务器的负载,提高了服务器的性能和响应速度。