内容大纲
浏览器缓存机制
缓存规则
三个角色
- 客户端
- 缓存数据库
- 中间服务器+源服务器
三级缓存机制
from memory cache
不请求网络资源,资源在内存当中,一般字体、图片会存在内存当中
不访问服务器,直接读缓存,从内存中读取缓存。此时的数据时缓存到内存中的,当kill进程后,也就是浏览器关闭以后,数据将不存在。
statusCode:200
from disk cache
不请求网络资源,在磁盘当中,一般非脚本会存在内存当中,如css等。
不访问服务器,直接读缓存,从磁盘中读取缓存,当kill进程时,数据还是存在。
statusCode:200
请求网络资源
访问服务期,请求相应的资源
- 未过期:不返回新资源,statusCode:304
- 已过期:返回新资源,statusCode:200
缓存校验
强缓存
请求的资源被标记为
强制缓存涉及到的请求头有:cache-control、expires (有这些请求/响应头的资源被标记为强缓存)
强制缓存涉及到的响应头有:cache-control、expires
只要请求一次,在有效期内
- expires :标记过期时间,格式为绝对时间 (缺陷:客户端时间与服务器时间可能不同步,不能保证资源的新鲜度)
请求头
- cache-control:
- no-cache:告诉中间服务器:我不要过期资源,给我去源服务器去取!
- no-store:告诉中间服务器:不要缓存我的请求和响应
- max-age
响应头
- cache-control:
- no-cache:不要缓存过期资源
- no-store:不要缓存请求或响应的任何内容
- must-revalidate:客户端必须再次向源服务器确认资源的有效性
- max-age:资源最大的有效期
- private:只能客户端缓存
- public:客户端和中间服务器都可以缓存
对比缓存
对比缓存涉及到的请求头有:last-modified-since、if-none-match (有这些请求/响应头的资源被标记为对比缓存)
对比缓存涉及到的响应头有:Last-Modified、etag
无论资源是否过期(对比缓存也没法判断是否过期啊),都会向服务器发起校验请求
服务器校验后发现客户端缓存的资源已过期,则返回新资源,响应码为200
服务器校验后发现客户端缓存的资源没有过期,则返回响应码为304
缓存判断优先级
-
cache-control和expires同时存在时,cache-control优先级高于expires
-
对于对比缓存,同时有etag和last-modified时etag优先级更高(last-modified是一个绝对时间,而时间可能不准,etag一般是md5值)
示例
- 前端主要代码
<script>
const logDom = document.getElementById('log')
const btn = document.getElementById('btn_log')
btn.onclick = function () {
const xhr = new XMLHttpRequest()
xhr.open('get', 'http://localhost:8081/log')
// xhr.setRequestHeader('cache-control','min-fresh=10');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
log.innerText =(new Date()).toLocaleString()+'\r\n'+ xhr.responseText
}
}
xhr.send()
}
</script>
- if-modified-since/last-modified
function onRequest(req, res) {
let { pathname } = url.parse(req.url, true)
if (pathname === '/log') {
let logPath = path.join(__dirname, 'log.txt')
access(logPath).then(_ => {
stat(logPath).then(stats => {
let ifModifiedSince = req.headers['if-modified-since'];
console.log(`ifModifiedSince ${chalk.yellow(ifModifiedSince)}`)
let lastModified = stats.ctime.toGMTString();
console.log(`lastModified ${chalk.red(lastModified)}`)
if (ifModifiedSince == lastModified) {
res.writeHead(304);
res.end('');
} else {
return sendFile(req, res, logPath, stats);
}
}).catch(err => {
sendError(req, res, 500, '', util.inspect(err))
})
}).catch(err => {
sendError(req, res, 500, '', util.inspect(err))
})
}
else {
sendError(req, res, 404, 'NOT FOUND')
}
}
function sendFile(req, res, filepath,stat) {
res.setHeader('Access-Control-Allow-Origin','*')
res.setHeader('Content-Type', mime.getType(filepath));
//发给客户端之后,客户端会把此时间保存起来,下次再获取此资源的时候会把这个时间再发回服务器
res.setHeader('Last-Modified', stat.ctime.toGMTString());
fs.createReadStream(filepath).pipe(res);
}
- If-None-Match/etag
function onRequest(req, res) {
let { pathname } = url.parse(req.url, true)
if (pathname === '/log') {
let logPath = path.join(__dirname, 'log.txt')
access(logPath).then(_ => {
stat(logPath).then(stats => {
// 坑爹的货色 headers的key都是小写的
let ifNoneMatch = req.headers['If-None-Match'.toLowerCase()];
console.log(`If-None-Match ${chalk.yellow(ifNoneMatch)}`)
let out = fs.createReadStream(logPath);
let md5 = crypto.createHash('md5');
out.on('data', function (data) {
md5.update(data);
});
out.on('end', function () {
//1.相同的输入相同的输出 2 不同的输入不同的输入 3 不能从输出反推出输入
let etag = md5.digest('hex');
console.log(`If-None-Match ${chalk.yellow(etag)}`)
// let etag = `${stat.size}`;
if (ifNoneMatch == etag) {
res.writeHead(304);
res.end('');
} else {
return sendFile(req, res, logPath, etag);
}
});
}).catch(err => {
sendError(req, res, 500, '', util.inspect(err))
})
}).catch(err => {
sendError(req, res, 500, '', util.inspect(err))
})
}
else {
sendError(req, res, 404, 'NOT FOUND')
}
}
function sendFile(req, res, filepath,etag) {
res.setHeader('Access-Control-Allow-Origin','*')
res.setHeader('Content-Type', mime.getType(filepath));
//发给客户端之后,客户端会把此时间保存起来,下次再获取此资源的时候会把这个时间再发回服务器
res.setHeader('etag', etag);
fs.createReadStream(filepath).pipe(res);
}
- cache-control
function onRequest(req, res) {
let { pathname } = url.parse(req.url, true)
if (pathname === '/log') {
let logPath = path.join(__dirname, 'log.txt')
access(logPath).then(_ => {
stat(logPath).then(stats => {
sendFile(req, res, logPath,stats);
}).catch(err => {
sendError(req, res, 500, '', util.inspect(err))
})
}).catch(err => {
sendError(req, res, 500, '', util.inspect(err))
})
}
else {
sendError(req, res, 404, 'NOT FOUND')
}
}
function sendFile(req, res, filepath,stats) {
res.setHeader('Access-Control-Allow-Origin','*')
res.setHeader('Access-Control-Allow-Headers','cache-control')
res.setHeader('Content-Type', mime.getType(filepath));
//发给客户端之后,客户端会把此时间保存起来,下次再获取此资源的时候会把这个时间再发回服务器
res.setHeader('Cache-Control', 'max-age=10,must-revalidate');
// res.setHeader('Cache-Control', 'max-age=10');
res.setHeader('Last-Modified', stats.ctime.toGMTString());
fs.createReadStream(filepath).pipe(res);
}
总结
相关header
- expires
- cache:no-cache,no-store,max-age,must-revalidate,private,public
- if-modified-since/last-modified
- if-none-match/etag
相关响应吗
- 304 资源未修改
- 200
- 流程图: