前端缓存分为两部份:浏览器缓存、HTTP缓存,其中HTTP缓存是核心。
HTTP缓存又分为强制缓存
和协商缓存
。
强制缓存
强制缓存的工作原理是通过HTTP响应头中的特定字段来控制的。这些字段通常包括Expires
和Cache-Control
,它们指示了资源的缓存有效时间。
当浏览器在有效时间内再次请求同一资源时,它会直接从本地缓存中获取该资源,而不会向服务器发送请求。
强缓存的实现方式一【Expires】
js
Expires:new Date("2022-7-30 23:59:59");
Expires
字段的作用是,设定一个强缓存时间。在此时间范围内,从内存(或磁盘)中读取缓存返回。
缺陷
Expires判断强缓存是否过期的机制是:客户端获取本地时间戳,与缓存的资源中的Expires
字段的时间做比较。来判断是否需要对服务器发起请求。
这里有一个巨大的漏洞:如果本地时间不准确,或者说本地事件与服务器时间误差过大,则会出现该资源将无法被缓存或者资源被永远缓存的情况。
所以,Expires
字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires
,强缓存功能通常使用cache-control
字段来代替Expires
字段。
强缓存的实现方式二【Cache-control】
js
//后端往响应头中写入需要缓存的时间
res.writeHead(200,{
'Cache-Control':'max-age=10'
});
max-age 后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。该方式解决解决了Expires
所存在的巨大漏洞,不用再比对客户端和服务端的时间去判断缓存是否有效了。
Cache-control
有max-age 、s-maxage 、no-cache 、no-store 、private 、public这六个属性。
- max-age:决定客户端资源被缓存多久。
- s-maxage:决定代理服务器缓存的时长。
- no-cache:表示是强制进行协商缓存。
- no-store:是表示禁止任何缓存策略。
- public:表示资源即可以被浏览器缓存也可以被代理服务器缓存。
- private:表示资源只能被浏览器缓存。
no-cache 和 no-store
如果某一资源的Cache-control
中设置了no-cache ,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。
注意,no-cache和no-store是一组互斥属性,这两个属性不能同时出现在
Cache-Control
中。
public 和 private
某些情况下,客户端和浏览器的通信中间会出现代理服务器,public 和 private 决定了资源是否可以在代理服务器进行缓存。
- public 表示资源在
客户端和代理服务器
都可以被缓存。 - private 则表示资源只能在
客户端
被缓存,拒绝资源在代理服务器缓存。 - 如果这两个属性值都没有被设置,则默认为private。
注意,public 和private 也是一组互斥属性。他们两个不能同时出现在响应头的
cache-control
字段中。
max-age 和 s-maxage
- max-age 表示的时间资源在
客户端
缓存的时长。 - s-maxage 表示的是资源在
代理服务器
可以缓存的时长。
注意,max-age 和s-maxage并不互斥。他们可以一起使用。
Cache-control设置多个值的示例
js
Cache-control:max-age=10000,s-maxage=200000,public
协商缓存
协商缓存是浏览器与服务器之间进行通信以确认缓存资源是否仍然有效的过程。
协商缓存主要涉及两组HTTP头字段:Last-Modified
和If-Modified-Since
,以及ETag
和If-None-Match
。
协商缓存的实现方式一【last-modified】
基于last-modified
的协商缓存实现方式分为以下三个步骤(后端设置):
- 首先需要在服务器端读出文件修改时间;
- 将读出来的修改时间赋给响应头的
last-modified
字段; - 最后设置
Cache-control:no-cache
。
js
const http = require('http');
const fs = require('fs');
http.createServer((req,res)=>{
if(req.url === '/bg.png'){
// 协商缓存
const data = fs.readFileSync('./bg.png'); //读取资源
const {mtime} = fs.statSync('./bg.png'); //读取修改时间
res.setHeader('last-modified',mtime,toUTCString()); //设置文件最后修改时间
res.setHeader('Catch-Control','no-cache'); //Cache-control:no-cache的意思是跳过强缓存校验,直接进行协商缓存
res.send(data)
}else{
res.statusCode = 404
}
}).listen(8080,()=>{
console.log("8080启动成功")
})
当客户端在响应头中读取到last-modified
的时候,会在下次的请求头中携带一个字段:If-Modified-Since
,它的值就是代码中后端设置的last-modified
的值。
之后每次对该资源的请求,都会带上If-Modified-Since
这个字段,服务端就需要拿到这个时间并再次读取该资源的修改时间,让两个时间做比对来决定是读取缓存还是返回新的资源。
协商缓存过程结束,过程可通过如下流程图梳理:
缺陷
使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。
1、在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。
2、如果文件在几百毫秒内完成修改的话,文件修改时间不会改变。(因为文件修改时间记录的最小单位是秒)
为了解决上述的这两个问题。从http1.1
开始新增了一个头信息,ETag
(Entity 实体标签)
协商缓存的实现方式二【ETag】
ETag
就是将原先协商缓存的比较时间戳 的形式修改成了比较文件指纹。
文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。
实现流程:
1、第一次请求某资源的时候,服务端读取文件,计算出文件指纹,将文件指纹放在响应头的Etag
字段中跟资源一起返回给客户端。
2、第二次请求某资源的时候,客户端从缓存中读取出上一次服务端返回的ETag
赋给请求头的if-None-Match
字段,让上一次的文件指纹跟随请求一起回到服务端。
3、服务端拿到请求头中的is-None-Match
字段值后,再次读取目标资源并生成文件指纹,两个指纹做对比。如果指纹完全吻合,说明文件没有被改变,直接返回304状态码和一个空的响应体。如果指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag
中并返回给客户端。
js
const http = require('http');
const fs = require('fs');
const etag = require('etag');
http.createServer((req,res)=>{
if(req.url === '/bg.png'){
// 协商缓存 基于Etag
const data = fs.readFileSync('./bg.png'); //读取资源
const etagContent = etag(data); //根据文件生成唯一标识符
const {mtime} = fs.statSync('./bg.png'); //读取修改时间
const ifNoneMatch = req.headers['if-none-match']; //获取请求头中的上一次的文件指纹
// 比较文件指纹是否一致
if(ifNoneMatch === etagContent){
//如果文件指纹一致,则返回304状态码并返回一个空的响应体,并结束代码
res.stausCode = 304;
res.send();
return;
}
res.setHeader('etag',etagContent); //给实体赋予唯一标识
res.setHeader('Catch-Control','no-cache'); //开启协商缓存
res.send(data)
}else{
res.statusCode = 404
}
}).listen(8080,()=>{
console.log("8080启动成功")
})
从校验流程上来说,协商缓存的修改时间比对和文件指纹比对,几乎是一样的:
缺陷
1、ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能
。显然,ETag在这样的场景下就不是很适合。
2、ETag有强验证
和弱验证
:
- 强验证:ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。
- 弱验证:弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。
总结
强缓存中,能用cache-control
就不要用expiress
。协商缓存中,ETag
并不是last-modified
的完全替代方案。而是last-modified
的补充方案,项目中到底是用ETag
还是last-modified
完全取决于业务场景,这两个没有谁更好谁更坏。
文章参考出处: 中高级前端工程师都需要熟悉的技能--前端缓存