引言
在开发项目时,我们经常需要向页面添加图片等资源。每次打开项目时,都需要请求这些资源,而网络请求总是耗时的。如果在一个时间段内频繁地访问同一个页面,每次都等待资源请求完成显然是低效的,特别是当这些资源没有变化的时候。为了提高效率,我们可以利用浏览器的缓存机制来存储频繁使用的资源,从而避免每次加载页面都进行网络请求。
HTTP 缓存
将页面上长时间不更新的资源缓存到浏览器上,下次访问页面时该部分资源直接从缓存中获取,从而减少了网络请求的次数,提高了页面的加载速度
例如,考虑以下HTML文档:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>hello 你好 世界</h1>
<img src="assets/img/1.png" alt="">
<h3>test</h3>
</body>
</html>
这是我们打开这个页面所需要进行的资源请求
当加载此页面时,除了HTML文件本身,还需要请求其中包含的图片资源。为了加速这个过程,我们可以使用HTTP缓存中的强缓存机制。
强缓存
强缓存通过设置响应头中的Cache-Control
字段实现,该字段的值为max-age=xxx
,表示缓存的有效期(以秒为单位)。比如,在Node.js服务器端可以这样设置:
res.writeHead(200, {
'content-type': mime.getType(ext),
'cache-control': 'max-age=86400', // 设置缓存一天
});
在开头提到的那个例子,我们修改Cache-Control
字段中的内容,将它时间改为一天,那么在我们第一次打开这个页面后,这个图片资源就会被强缓存下来
此时,请求这个图片资源就直接从缓存中获取,提高了页面的加载速度。
强缓存的一个限制是它对通过浏览器地址栏直接访问的资源无效。因为这类请求会自动携带Cache-Control: max-age=0
头部信息,这意味着无法使用强缓存。
我们可以总结一下强缓存的特点:
-
在响应头中设置 Cache-Control 字段,该字段的值为 max-age=xxx,表示缓存的有效期,单位为秒
-
通过浏览器 url 地址栏请求的资源,请求头中就会自动携带 Cache-Control: max-age=0 字段,也就意味着这种资源无法被强缓存
-
被缓存的资源在浏览器的 cache Storage 中,本质上还是在硬盘上
-
强制刷新浏览器,会清空浏览器的 cache Storage
那既然强缓存对浏览器地址栏访问的资源无效,那我们怎么可以再优化一些呢,那么这个时候我们就要用到协商缓存了
协商缓存
协商缓存用于解决强缓存的不足。其工作原理是在首次请求时,服务端会在响应头中加入
Last-Modified
字段,记录资源的最后修改时间。之后,当客户端再次请求该资源时,会在请求头中携带If-Modified-Since
字段,值为上次收到的Last-Modified
值。服务器根据这两个时间戳是否匹配决定返回304 Not Modified
还是新的资源。
const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www',req.url))
if(fs.existsSync(filePath)){ // 判断文件是否存在
const stats = fs.statSync(filePath) // 获取文件信息
const isDir = stats.isDirectory() // 判断是否为文件夹
if(isDir){
filePath = path.join(filePath, 'index.html') // 如果是文件夹,默认返回index.html
}
if(!isDir || fs.existsSync(filePath)){ // 向前端返回文件
const {ext} = path.parse(filePath)
const timeStamp = req.headers['if-modified-since']
let status = 200
if(timeStamp && Number(timeStamp) === stats.mtimeMs){ // 文件没有发生过更改
status = 304
}
res.writeHead(status, {
'content-type': mime.getType(ext),
'cache-control': 'max-age=86400', // 强缓存一天
'last-modified': stats.mtimeMs, // 最后修改时间
})
if(status === 200){
const readStream = fs.createReadStream(filePath) // 创建可读流
readStream.pipe(res) // 管道流,将可读流中的数据直接输出到res中
}else {
return res.end()
}
}
}
});
server.listen(3000)
但是我们可以看到,为什么
html
资源还是没被缓存下来,这是因为Last-Modified
和If-Modified-Since
机制存在一个问题:即使内容未变但文件被修改后,Last-Modified
的时间戳也会更新,导致不必要的重新请求。
- 文件指纹 etag + if-none-match
为了解决上述问题,可以采用ETag机制。ETag是一个独特的标识符,通常基于文件内容生成的哈希值。只有当文件内容改变时,ETag才会变化。
const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const checksum = require('checksum'); // 计算文件的MD5值
const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www', req.url))
if (fs.existsSync(filePath)) { // 判断文件是否存在
const stats = fs.statSync(filePath); // 获取文件信息
const isDir = stats.isDirectory() // 判断是否为文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html') // 如果是文件夹,默认返回index.html
}
if (!isDir || fs.existsSync(filePath)) { // 向前端返回文件
const { ext } = path.parse(filePath);
const ifNoneMatch = req.headers['if-none-match']
checksum.file(filePath, (err, sum) => {
sum = `"${sum}"`
if (ifNoneMatch === sum) { // 文件没有变化
res.writeHead(304, {
'Content-Type': mime.getType(ext),
'etag': sum,
})
res.end()
} else {
res.writeHead(200, {
'Content-Type': mime.getType(ext),
'Cache-Control': 'max-age=1000000',
'etag': sum,
})
const resStream = fs.createReadStream(filePath)
resStream.pipe(res)
}
})
}
}
})
server.listen(3000);
总结
-
只用强缓存可以把除url地址栏访问的资源缓存起来,但是资源跟新了就无法第一时间让前端获取到,所以还需要协商缓存
-
只要命中了强缓存,就不会走协商缓存,只有强缓存到期,才会走协商缓存
-
为了保证文件资源更新,前端能及时获取到,一般会在文件名后面加上文件指纹(用内容生成hash值),这样文件指就会改变,从而保证资源的更新