# 浏览器缓存
浏览器缓存是一种常见的web性能优化手段,
HTTP控制缓存的方式有两种:Cache-Control
和Expires
Cache-Control
Cache-Control
有以下几个值:no-store
:禁止进行缓存no-cache
:(强制确认缓存)服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回304),则缓存才使用本地缓存副本。private
:Cache-Control
默认值,响应只会被某个用户缓存,中间人(CDN、代理等)不缓存publish
:响应除了被某个用户缓存,还可以被中间人(CDN、代理等)缓存max-age={seconds}
:表示资源能够被缓存的最大时间。相对Expires
给的是具体的绝对时间,max-age
是距离发起时间的秒数。must-revalidate
:当使用了must-revalidate
指令,那就意味着缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用。
Expires
expires
设置是一个具体的绝对时间,在这个绝对时间内都只使用缓存的数据
# 缓存存储策略
说白了就是如何缓存,从上文的对Cache-Control
的介绍中可以知道除了no-store
表示不缓存响应内容,其它的属性no-cache
、max-age
、Publish
、Private
、must-revalidate
,
都是指明对响应内容会做缓存
那么问题来了,服务器上的资源是会更新的,客户端如果一直使用缓存的资源,那么就不即时更新服务端上的新资源了,所以客户端什么时候用缓存的资源,什么时候拉取新的资源。这就是下面要讲的缓存更新策略。
缓存更新策略主要两大类:强缓存和协商缓存
# 强缓存
就是给资源指定一个过期时间,超过这个时间就重新获取资源,要不然就从缓存中获取。从上文中可以知道Headers
中的expires
字段和Cache-Control
中的max-age
,都可以用来设置这个过期时间。他们区别如下:
expires
设置是一个具体的绝对时间,Cache-Control
中的max-age
设置是一个从请求发起时间开始算的相对秒数,如当值为max-age=300
时,300秒内都能缓存中获取资源当它们同时存在时,使用
Cache-Control
中的max-age
Expiress
是http1.0的产物,Cache-Control
是http1.1的产物如果有浏览器不支持
Cache-Control
,则会使用Expiress
# 强缓存例子
例子使用Node+koa
构建服务,ehs
渲染页面
// app.js
const Koa = require('koa');
const app = new Koa();
const views = require('koa-views')
const path = require('path')
const router = require('./router/index')
const resource = require('koa-static');
app.use(views(path.join(__dirname, './view'), {
extension: 'ejs'
}))
app.use(async (ctx, next) => {
// 使用一个中间件设置缓存
// 设置响应头Cache-Control 设置资源有效期为300秒
ctx.set({
'Cache-Control': 'max-age=300'
});
await next();
});
app.use(resource(path.join(__dirname, './img')));
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
// view/index.ejs
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<img src="./下载2.jpeg"/>
<p id="test">点击获取数据</p>
</body>
<script>
window.onload = function () {
document.getElementById('test').onclick = function () {
console.log('fetch', fetch)
fetch('/data')
.then(response => {
return response.json();
})
.then(res => {
console.log('re', res)
})
.catch(e => {
console.log('e', e)
})
}
}
</script>
// router/index.js
const Router = require('koa-router');
const router = new Router()
const data = {
data: 'koa-demo'
}
router.get('/', async (ctx, next) => {
console.log('pah')
let title = 'hello koa22'
await ctx.render('index', {
title,
})
})
router.get('/data', (ctx, next) => {
ctx.set("Content-Type", "application/json")
ctx.body = JSON.stringify(data)
})
module.exports = router
运行之后会发现首次加载的图片或者点击获取数据,返回的Status Code
都是200 OK
然后重新刷新页面,图片的返回code变成了200 OK (from memory cache)
,点击获取数据Status Code
也变成了200 OK (from disk cache)
修改缓存方式使用expires
试下
app.use(async (ctx, next) => {
ctx.set('Expires', new Date(Date.now() + 30 * 1000).toUTCString())
await next();
});
运行时,会发布图片没有效果,我们查看一下图片请求的返回头,可以发现在response header
除了有我们设置的Expires
属性还有
Cache-Control:max-age=0
属性,Cache-Control
属性优先级大于Expires
,所以才导致图片缓存没有效果
至于为什么图片的返回头部默认会带有Cache-Control
和Last-Modified
等属性,我们这里先不深究
继续回到例子,我们点击获取数据按钮,效果跟使用Cache-Control
的例子是一样的
如果
max-age
和expires
属性都没有,找找头里的Last-Modified
信息。如果有,缓存的寿命就等于头里面Date
的值减去Last-Modified
的值除以10(注:根据rfc2626其实也就是乘以10%)
缓存失效时间计算公式如下:(DownloadTime - LastModified) * 10%
DownloadTime:浏览器获取到响应的时间
LastModified:服务端资源上次修改时间
# from memory cache
和from disk cache
from memory cache
MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。
目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。
from dist cache
DiskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。
- | from memory cache | from dist cache |
---|---|---|
相同点 | 只能存储一些派生类资源文件 | 只能存储一些派生类资源文件 |
不同点 | 退出进程时数据会被清除 | 退出进程时数据不会被清除 |
存储资源 | 一般脚本、字体、图片、CSS会存在内存当中 | 一般非脚本会存在内存当中,如json数据等 |
但使用强缓存的方式不够灵活,因为就算缓存时间到了,资源也不一定是更新了的,而且资源也可以是在缓存时间内更新的,这样就不能即使更新资源了,此时就要使用协商缓存
# 协商缓存
协商缓存就是,请示资源时,先由服务器去确认资源是否有更新,确认了更新了才返回数据,否则就返回304
告诉浏览器从取缓存中的资源
那么服务器怎么知道资源是否更新了呢?配合Headers中的两个字段Last-Modified
和 if-modified-since
或者ETag
字段。
# Last-Modified/if-Modified-Since
协商缓存的思路如下:
- 第一次请求时,服务器返回资源时,会传递一个
Last-Modified
字段,其内容是这个资源的修改时间。之后再次请求这个资源时,会自带一个if-Modified-Since
字段,它的值是上一次传递过来的Lost-Modified
的值,服务器将与这个值与现在的文件的最后修改时间做对比,如果相等,就不用重新拉取这个资源,返回304告诉浏览器读缓存里的资源,否则就重新拉取
# 协商缓存例子(Last-Modified)
node实现可缓存的服务
http.createServer(function(req,res){
var pathname = url.parse(req.url).pathname
var fsPath = __dirname + pathname
fs.access(fsPath, fs.constants.R_OK, function(err){ //fs.constants.R_OK - path 文件可被调用进程读取
if(err) {
console.log(err) //可返回404,在此简略代码不再演示
}else {
var file = fs.statSync(fsPath) //文件信息
var lastModified = file.mtime.toUTCString()
var ifModifiedSince = req.headers['if-modified-since']
//传回Last-Modified后,再请求服务器会携带if-modified-since值来和服务器中的Last-Modified比较
var maxAgeTime = 3 //设置超时时间
if(ifModifiedSince && lastModified == ifModifiedSince) { //客户端修改时间和服务端修改时间对比
res.writeHead(304,"Not Modified")
res.end()
} else {
fs.readFile(fsPath, function(err,file){
if(err) {
console.log('readFileError:', err)
}else {
res.writeHead(200,{
"Cache-Control": 'max-age=' + maxAgeTime,
"Last-Modified" : lastModified
})
res.end(file)
}
})
}
}
})
}).listen(3030)
Last-Modifily
和if-Modified-Since
是根据最后修改时间来判断是否需要重新拉取数据,还有一个使用Etag
的方式,根据内容是否变化来判断是否需要重新资源
# ETag/If-None-Match
ETag一般是由文件内容hash
生成的,也就是说它可以保证资源的唯一性,资源内容改变了那么hash
也会改变
有If-None-Match
字段将这个ETag带回服务器中,服务器跟这个ETag与当前资源的hash
进行比较。来判断是否需要重新返回该资源
# 协商缓存例子(Etag)
router.get('/data', (ctx, next) => {
const hash = md5(data);
const ifNoneMatch = ctx.get("If-None-Match")
console.log('ifNoneMatch', ifNoneMatch)
if(ifNoneMatch && ifNoneMatch === hash) {
ctx.status = 304
return
}
ctx.set('Etag', hash)
ctx.set('Cache-Contrl', 'no-store')
ctx.set("Content-Type", "application/json")
ctx.body = JSON.stringify(data)
})
第一次点击获取数据是返回的status code
是200 OK
, 再次点击返回的是304 Not Modified
# 总结
当浏览器访问一个已经访问过的资源时,它会这样做
看看是否命中强缓存,如果命中,它直接使用缓存
如果没有命中强缓存,就发请求到服务器是否命中协商缓存
如果协商缓存命中,则返回304,告诉浏览器使用本地缓存,否则返回最新资源