记录自己所学,直接进入主题。
强缓存
Expires和Cache-Control
Expires: Wed, 22 Nov 2019 08:41:00 GMT
Expires即过期时间,http1.0使用的字段;告诉浏览器在这个时间之前可以直接从缓存里边获取数据,无需再次请求;就比如上边的,表示资源在2019年11月22号8点41分过期,过期之后就需要重新请求资源;
这个方式的问题在于,服务器时间和客户端时间可能不一致,导致缓存混乱。因此这种方式在HTTP/1.1中被抛弃了
Cache-Control
在HTTP/1.1中采取了一个非常关键的字段Cache-Control,采用过期时长来控制缓存,对应的字段是max-age.比如这个例子:
Cache-Control:max-age=3600
它采用过期时长来控制缓存,表示一个小时内可以直接使用缓存,
它其实可以组合非常多的指令,完成更多场景的内存判断,将一些关键的属性列举如下:
- public 客户端和代理服务器都可以缓存;
- private 只有浏览器能够缓存了,中间代理服务器不能缓存
- no-cache 跳过当前的强缓存,发送HTTP请求,即直接进入
协商缓存阶段 - no-store 非常粗暴,不进行任何形式的缓存
- s-maxage 和max-age长的很像,区别在于是代理服务器的缓存时间
值得注意的是,当Expires和Cache-Control同时存在的时候,Cache-Control会优先考虑。
当然还有另外一种情况,当资源缓存时间超时了,也就是强缓存失效了,这样就进入到第二级屏障———协商缓存了;
协商缓存
Last-Modified
即最后修改时间。在浏览器第一次向服务器发送请求时,会在响应头中加入这个字段。
浏览器接收到后,如果再次请求会在请求头中携带If-Modified-Since这个字段,这个字段的值就是服务器返回的Last-Modified的值
服务器拿到If-Modified-Since这个字段的值和服务器相应资源最后修改时间对比:
- 如果请求头中的这个值小于最后修改时间,证明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
- 否则返回304,告诉浏览器直接使用缓存
ETag
ETag是服务器根据当前文件的内容,给文件生成唯一标识。只要文件有改动,这个值就会变。服务器通过响应头把这个值传给浏览器;
浏览器在接收到ETag这个值后,会在下次请求,把这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。
服务器收到If-None-Match这个值后,会和ETag这个值做对比:
- 如果相同,则返回304,直接使用缓存。
- 否则,说明要更新了。返回新的资源,跟常规的HTPP请求响应的流程一样。
两者对比
- 在精度上ETag,优于Last-Modified,ETag是按照内容给资源上标识,因此能准确感知资源的变化。而Last-Modified在一些特殊的情况并不能准确感知资源变化,主要有两种情况:
- 编辑了文件资源,但是文件内容没有更改,这样也会造成缓存失效
- Last-Modified的感知单位是秒,如果内容改动小于1秒则不能准确感知。
- 在性能上Last-Modified优于ETag,每一次文件改动ETag都要根绝文件的内容生成哈希值,而Last-Modified仅仅只是记录一个时间点;
如果Last-Modified和ETag同时存在,服务器会优先考虑ETag;
缓存位置
前边强缓存我们说到,如果缓存命中我们直接从内存中读取资源,那这些资源又存储在什么位置呢?
浏览器中缓存位置一共四种,按优先级从高到低排列依次是:
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
Service Worker
Service Worker借鉴了Web Worker的思路,即让JS运行在主线程之外,因为脱离了浏览器的窗体,因此无法直接访问DOM;即使如此,依然可以帮助我们完成很多有用的功能,比如:离线缓存 消息推送 和 网络代理等功能;其中的 离线缓存 就是 Service Worker Cache.
Memory Cache和Disk Cache
Memory Cache指的是内存缓存,从效率上来讲是最快的。从存活时间来说又是最短的,当渲染进程结束后,内存缓存也就不存在了。
Disk Cache就是磁盘中的缓存,存取效率比内存慢,优势在于存储容量和时长。
既然两者各有优劣,浏览器缓存位置该怎么选择呢?
- 比较大的js css文件放进磁盘,反之丢进内存
- 内存使用率比较高的时候,文件优先进入磁盘
Push Cache
即推送缓存,这是浏览器最后一道防线。他是HTTP/2的内容,随着HTTP/2的推广,它的应用会越来越广泛,关于Push Cache请参考扩展文章
总结
先对浏览器缓存做个总结
首先通过Cache-Control验证强缓存是否命中,如果命中直接使用缓存,否则进入协商缓存通过请求头中的 If-Modified-Since和If-None-Match来判断是否命中
- 命中,返回304状态码,告诉浏览器从缓存中读取资源
- 否则,返回200状态码,资源更新。
实践
建一个小demo,项目目录
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>前端缓存</title>
</head>
<style>
.image_box{
width: 800px;
margin: 200px auto;
}
</style>
<body>
<div class="image_box"><img style="width: 100%;" src="./images/cat.jpeg" /></div>
</body>
</html>
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
index.js
require('@babel/register');
require('./server.js');
package.json
{
"name": "webcache",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"server": "nodemon ./index.js"
},
"author": "webfansplz",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"@babel/register": "^7.0.0",
"koa": "^2.6.2",
"koa-conditional-get": "^3.0.0",
"koa-etag": "^4.0.0",
"koa-static": "^5.0.0"
},
"dependencies": {
"koa-router": "^10.1.1",
"nodemon": "^1.18.9"
}
}
server.js
import Koa from 'koa';
import path from 'path';
//静态资源中间件
import resource from 'koa-static';
const app = new Koa();
import conditional from 'koa-conditional-get';
import etag from 'koa-etag';
const host = 'localhost';
const port = 3000;
// etag works together with conditional-get
// app.use(conditional());
// app.use(etag());
app.use(resource(
path.join(__dirname, './static'),
{
// maxage: 10*1000
}
));
app.listen(port, () => {
console.log(`server is listen in ${host}:${port}`);
});
接下来,npm run server启动
浏览器 localhost:3000打开页面
验证强缓存
打开图片的请求我们看到,
Cache-Control:max-age=0
默认不缓存,我们将max-age设置为10s,再来请求,放开server.js中的注释
maxage: 10*1000
刷新页面,
可以看到设置max-age=10,后改为从 内存中读取,说明设置强缓存成功了,那么Cache-Control,是什么时候加上去的呢,
koa-static源码中引入了koa-send库。截取部分koa-send源码,只要传入maxage,就会设置Cahche-Control的max-age;
另外我们发现
也就是html强缓存无效每次都会重新请求,
可以看到请求头中每次浏览器都强制加上了Cache-Control:max-age=0,这可能是保证每次拿到最新的资源。
验证协商缓存
- Last-Modified/If-Modified-Since
不知大家注意到没,响应头中已经包含了Last-Modified这个字段
Last-Modified是什么时候加上去的呢,截取koa-send部分源码可以看到,
响应头中如果没有Last-Modified这个字段就添加上去,注意:这里只是针对静态资源,并不是所有的响应头都会加上这个字段,比如ajax请求就没有这个字段,下边会展开说。
请求和响应中的时间是一致的,为什么没有命中协商缓存呢,放开
app.use(conditional());
再来刷新下页面,
返回状态码304,命中了协商缓存,那么 conditional()做了什么,截取源码,
可以看到查看请求的新鲜度(下边会说到),如果新鲜可用,就返回304状态码。
偷偷更换下图片,
换过图片之后If-Modified-Since的值小于Last-Modified中的值,可以看到200 OK的状态码,已经重新请求了。
- ETag/If-None-Match
怎样给请求加上呢,使用koa-etag这个模块,放开
app.use(etag());
同时我们注消掉Last-modified,
返回和请求中都已携带ETag,且没有Last-Modified/If-Modified-Since的干扰,ETag中的值和If-None-Match中的值相同,命中缓存返回304.
接下来改变图片,重新请求
看到ETag中的值和If-None-Match中的值不同,重新请求。和Last-Modified/If-Modified-Since逻辑基本上是一样的,条件不同。
新鲜度检测
- koa-conditional-get
上边看到
koa-conditional-get可以让缓存生效,返回304,body为null,
- 在koa模块
request.js中看到
表示200到300的状态码和304就行行新鲜度检测,
- fresh 主要代码
function fresh (reqHeaders, resHeaders) {
// 分别获取 if-modified-since if-none-match的值
var modifiedSince = reqHeaders['if-modified-since']
var noneMatch = reqHeaders['if-none-match']
// 如果两个都没有直接放回 false 表示不新鲜 重新请求
if (!modifiedSince && !noneMatch) {
return false
}
// 2. 给端对端测试用的,因为浏览器的Cache-Control: no-cache请求
// 是不会带if条件的 不会走到这个逻辑
var cacheControl = reqHeaders['cache-control']
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
return false
}
// 比较 ETag 和 if-none-match
if (noneMatch && noneMatch !== '*') {
var etag = resHeaders['etag']
if (!etag) {
return false
}
var etagStale = true
var matches = parseTokenList(noneMatch)
for (var i = 0; i < matches.length; i++) {
var match = matches[i]
if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
etagStale = false
break
}
}
if (etagStale) {
return false
}
}
// 比较 ETag 和 if-modified-since
if (modifiedSince) {
var lastModified = resHeaders['last-modified']
var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
if (modifiedStale) {
return false
}
}
return true
}
ajax请求的缓存
首先要明白ajax请求需要我们手动设置响应头Cache-Control,Last-Modified,ETag,要根据业务实际场景去定义:比如说max-age,Etag的生成方式,生成这些之后,浏览器会自动帮我们带上这些请求头,conditional会进行协商缓存的控制,是否会返回304.
总结
- 发出请求后,会现在本地查找缓存
- 请求首先验证强缓存
Cache-Control是否命中,max-age是否过期 - 过期重新请求和缓存到本地,命中直接置用缓存
- 强缓存过期后进入协商缓存,首先对比
ETag字段是否一致,一致,返回304使用本地缓存 - 不一致,重新获取数据返回200
- 没有
ETag,对比Last-Modified字段,和ETag同理。
对于前端来说,我们能做的其实很有限,因为关于缓存的响应头主要是后端来控制,浏览器会自动携带响应头中关于缓存的字段,当然我们也可根据实际场景自定义携带缓存相关请求头,比如说:no-cache等.
参考文章