# 说说缓存的那些事
# 前言
缓存是我们前端开发不可避免接触到的,主要分为强制缓存和对比缓存(协商缓存), 下面我们通过缓存机制的介绍和实现缓存来深入了解一下。我们先来看下没有缓存的时候的工作流程。下面起一个node静态服务。
// index.html
<body>
index.html
<script src="./index.js"></script>
</body>
// index.js
console.log('hello world')
// server.js
const http = require('http')
const path = require('path')
const fs = require('fs')
http.createServer((req, res) =>{
let { pathname } = require('url').parse(req.url)
console.log(pathname)
let absPath = path.join(__dirname, pathname)
fs.stat(absPath, (err, statObj) => {
if (err) {
res.statusCode = 404;
res.end()
return
}
if (statObj.isFile()) {
fs.createReadStream(absPath).pipe(res)
}
})
}).listen(5050)
上述三个文件大致上是起一个静态服务,监听5050端口,并且打印当前的pathname
来判断请求资源时候是否走了缓存。index.html
中引入了 index.js
. 运行代码我们发现,控制台打印
/index.html
/index.js
我们来画个图大致理解一下流程:
这也是为什么我们静态服务会打印2次的原因。那现在了解了浏览器请求服务器的大致流程之后,我们尝试一下加入缓存来看看流程会发生什么变化.
# 强制缓存
客户端访问服务器请求资源的时候,请求成功之后客户端会把资源缓存到本地,如果需要再次请求支援的时候不需要请求服务器,直接从本地缓存中取。我们先来实现一个强制缓存:
if (statObj.isFile()) {
// 告诉客户端 表示20秒内如果再发一次请求,就不用请求服务器,直接从缓存中取
res.setHeader('Cache-Control', 'max-age=20')
res.setHeader('Expires', new Date(Date.now() + 20*1000).toGMTString())
fs.createReadStream(absPath).pipe(res)
}
以上代码就实现了一个强制缓存,我们连续刷新2次页面看到的输出如下:
/index.html
/index.js
/index.html
我们会发现, index.js
已经被缓存起来了,但是 index.html
并没有,这是因为浏览器本身对 index.html
也可以理解为入口不进行缓存,不然就乱了。大致流程如下:
强制缓存主要通过设置 Cache-Control
和 Expires
实现,但是我们还是需要注意一些细节:
1、Cache-Control
适用于高版本浏览器,后面设置的是相对时间,也就是多少时间以后(2s以后)
2、Expires
适用于低版本浏览器,后面设置的是绝对时间,所以一般我们两者同时设置做兼容。
3、强制缓存的文件状态码都是 200
4、默认 index.html
不会给缓存起来
设置Cache-Control
的时候, no-cache
和 no-store
的区别
1、如果服务器在响应中设置了 no-cache
,那么说明浏览器在使用缓存前会对比Etag,返回304避免修改
2、如果服务器在响应中设置了 no-store
,那么浏览器不会存储相应的数据。
# 对比缓存(协商缓存)
客户端第一次请求数据时,服务器将缓存的标识与数据一起返回给客户端,客户端拿到缓存的标识。当客户端再次发起IQ那个球的时候带上这个缓存的标识,服务器根据标识进行判断,如果标识匹配上就返回304,通知客户端可以使用缓存数据,如果匹配不上则返回新的数据和缓存的标识。
# Last-Modified 和 if-modified-since
// 获取上一次请求时候文件的修改时间
let clientFileCtime = req.headers['if-modified-since']
if (clientFileCtime) { // 如果修改时间存在,则表示之前访问过这个资源
let currentFileCtime = statObj.ctime.toGMTString() //拿到服务器当前文件的修改时间
if (clientFileCtime === currentFileCtime) {
// 如果2个时间相等,表示文件没有修改,通知客户端直接取缓存
res.statusCode = 304
res.end()
return
}
}
res.setHeader('Cache-Control', 'no-cache')
// 设置 Last-Modified 为当前资源的修改时间
res.setHeader('Last-Modified', statObj.ctime.toGMTString())
fs.createReadStream(absPath).pipe(res)
这种对比缓存的方式主要通过将上一次请求设置响应的 Last-Modified
和当前请求的if-modified-since
进行对比,如果两者一样的话,表示文件没有修改,然后告诉客户端让客户端去读缓存的数据。
但是这样的话也有一些不精确的缺点:
1、精确的时间是秒, 也就是说以1秒以内进行变化的话是监听不到的
2、也存在一种很极端的情况,如果一直修改文件,但是到最后文件内容没有变但是修改时间变了,也不会走缓存
# Etag 和 if-None-Match
通过Last-Modified
和 if-modified-since
的话不够精确,那么有没有一种方式是很精确的判断是否需要缓存呢,我们接下来来看一下:
if (statObj.isFile()) {
// 获取上一次请求时候md5
let client = req.headers['if-none-match']
// 根据目标路径读取文件并且转为md5
let fileContent = fs.readFileSync(absPath,'utf-8')
let md5 = crypto.createHash('md5').update(fileContent).digest('base64')
if (client && client === md5) { //如果2个md5相等,表示文件没有修改
res.statusCode = 304
res.end()
return
}
res.setHeader('Cache-Control', 'no-cache')
// 设置 Etag 为当前资源的md5
res.setHeader('Etag', md5)
fs.createReadStream(absPath).pipe(res)
}
这种对比缓存的方式主要通过将上一次请求设置响应的 Etag
和当前请求的if-none-match
进行对比,如果两者一样的话,表示文件没有修改,然后告诉客户端让客户端去读缓存的数据。
而使用 md5
来做指纹的这种情况虽然很精确,但是在读文件的时候对性能的消耗有点大,在项目中我采用的是 公司名缩写+文件修改时间+文件大小形成指纹。
# 总结
1、浏览器第一次发起一个 http/https请求,读取服务器数据
2、服务器设置响应头(cache-control、Ecpires、last-modified、Etag)给浏览器,其中cache-control、Ecpires属于强制缓存,其他的属于协商缓存
3、浏览器刷新页面再发起一个请求
3.1、如果 cache-control、Ecpires 都没有超过设置的缓存时间,所以资源除了 index.html 以外全部从缓存中取
3.2、如果强制缓存失败,则会走对比缓存服务器会根据 Last-Modified 和 if-modified-since 或者 Etag 和 if-None-Match 进行对比,如果值相等返回304状态码,如果不相等返回对应的资源
TIP
if-None-Match 的优先级比 if-modified-since 高哦