前言
你是否好奇:
- 服务器接收到请求,是否需要检查缓存?
- 检查什么字段?
- 什么样的缓存需要服务器端检查?
- 强制缓存和协商缓存的顺序?
- 设置max-age:0跟浏览器缓不缓存有关系吗?
- s-max-age的作用?
- 浏览器如何检查比较缓存是否过期?这些字段的优先级是怎么样的?
- Last-Modified和Etag有什么区别?
- Etag这个字符串是怎么生成的?
- 什么是from disk cache和from memory cache?什么时候触发?
- 什么是启发式缓存?在什么条件下触发?
如果你不能肯定的回答上上面的问题,那么通过本文的7个小nodejs小实战,你会有自己的答案。
项目地址:koa-cache
本文概览如下:
在进行实战之前,我们先了解了解http对资源的缓存方式都有那些。话不多说,现在开始。
缓存方式
如下图所示,一个网页通常需要加载各种类型的资源,如图片,css,js文件等。
缓存方式就是用来控制这些资源:
- 是否会被缓存?
- 如果资源会被缓存
- 缓存资源可以在哪里缓存?
- 缓存资源的到期时间?
- 缓存资源到期后,它们如何更新?
控制这些缓存方式的http字段主要有:Cache-Control,Expires,Last-Modifed/Etag。其中以Cache-Control字段优先级最高。所以,下面我们就通过这几方面:
- 资源的可缓存性
- 缓存存储策略
- 缓存过期策略
- 缓存更新策略
来主要了解了解Cache-Control字段的工作方式,以及Cache-Control字段和Expires,Last-Modifed/Etag这些字段的关系。
缓存存储策略
缓存资源的存储策略主要用来决定,该资源是否会被缓存,以及在哪里缓存。资源的可缓存性可由Cache-Control的no-store值来控制。如下:
Cache-Control: no-store
设置了该响应头的资源,不会被任何客户端和代理缓存。资源存储在哪里,则由Cache-Control的private和public值来控制,他们分表代表了两种不同的资源存储方式:一种是私有(浏览器)缓存,一种是共享(代理)缓存。
私有(浏览器)缓存
私有缓存只能用于单独的用户。也就是说客户端发送的请求只会存储于各个用户自己的浏览器私有缓存中。如下图所示:
我们可以通过如下方式设置资源的私有缓存方式:
Cache-Control: private
共享(代理)缓存
共享缓存则可以被多个用户使用。通过设置共享缓存,资源可以被任何中间人缓存,如中间代理,CDN等。如下图所示
我们可以通过如下方式设置资源的共享缓存方式:
Cache-Control: public
缓存到期策略
缓存的到期策略决定了缓存资源的到期时间以及客户端可以接收哪些未过期和过期的缓存资源。
缓存资源到期时间设置
通过Expires字段以及Cache-Control的如下字段可以设置缓存的到期时间。
- max-age: 表示资源能够被缓存的最大时间。超过这个时间缓存被认为过期(单位秒)。
- s-max-age:仅适用于共享缓存(比如各个代理),私有缓存会忽略它。s-maxage的优先级高于max-age。如果存在s-max-age,则会覆盖掉max-age和Expires header。
其中max-age的时间是相对于请求的时间。是一个相对时间。而Expires是一个绝对时间,设置了Expires相当于设置了缓存资源的截止日期(deadline)。不推荐使用 Expires,因为很多服务器时间都不同步。另外,如果同时在响应头设置了Expires和Cache-Control的max-age以及s-max-age,那么Expires头会被忽略。
客户端设置可接受的缓存资源
如下对Cache-Control的值设置可以决定客户端能够接收那些缓存资源。
- min-fresh:只能出现在请求中,min-fresh要求缓存服务器返回min-fresh时间内的缓存数据。例如Cache-Control:min-fresh=60,这就要求缓存服务器发送60秒内的数据。
- max-stale:只能出现在请求中,表示客户端会接收缓存数据,即使过期也照常接收。例如Cache-Control:max-stale=60,这就意味着缓存服务器过期不超过60秒的资源,客户端都会接收。
缓存更新策略
缓存的更新策略的作用是,提供方法让客户端或者缓存服务器确定是否需要更新我们的本地缓存。
缓存的更新方式有如下3种,可以分别通过Cache-Control的如下值进行设置:
- no-cache: 这个值不是不缓存的意思,使用no-cache依然会缓存资源。只是不会直接读取缓存。在读取缓存之前,需要先发送请求到服务端确认资源是否是最新的。这个确认过程称为协商缓存或者对比缓存。
- must-revalidate: 告诉浏览器、缓存服务器,本地缓存过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器校验。
- proxy-revalidate: 和must-revalidate很相似,但仅适用于为许多用户提供服务端代理服务器,私有缓存不受影响。
这里需要说明的是,缓存过期就revalidate(revalidate就是重新校验的意思),并不需要专门的指令。从上面我们可以知道must-revalidate的,对于must-revalidate来说,只有缓存过期后才会生效,也就是说在缓存没有过期之前,是不会发送请求来检验缓存是否有更新,而是会直接读取本地缓存。既然缓存过期就会自动revalidate,那么为什么还需要must-revalidate呢?
must-revalidate与缓存服务器
这是因为各种缓存服务器,比如NGINX、Vanish、Squid都或多或少的允许通过Cache-Control的指令或者修改软件配置的方式返回过期缓存,所以,加上must-revalidate能阻止返回过期缓存的行为,因为带有must-revalidate的缓存,在任何情况下,都必须成功revalidate后才能使用,没有例外。所以must-revalidate更适合的名字是never-return-stale。
must-revalidate与浏览器
那浏览器有没有return statle的情况呢,也就是说浏览器会不会使用过期缓存呢?答案是有,当我们使用浏览器前进后退功能的时候,浏览器会尽量使用本地缓存来重新打开页面,即使缓存已经过期了,也不会重新revalidate。即使是must-revalidate也不会强迫浏览器revalidate。
缓存实战
为了大家对http缓存有一个更深刻的理解,现在,我们实现一个静态服务器,开始实战吧!
- 初始化项目
# 创建项目
mkdir koa-cache
cd koa-cache
# 初始化
git init
yarn init
# 安装依赖
yarn add koa
- 项目目录
项目目录如图所示:
- 代码编写
前端代码编写
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test cache</title>
<link rel="stylesheet" href="/static/css/index.css">
</head>
<body>
<div id="app">缓存测试</div>
<img src="/static/image/cat.jpeg" alt="">
</body>
</html>
// index.css
#app {
color: #FBDC5C
}
服务端代码编写
现在我们的目的就是要通过在浏览器访问localhost:3000获取到index.html页面和相应的前端请求资源(图片和css文件)。 要实现这一点,我们需要实现一个静态资源服务。如下:
// index.js
const Koa = require('koa')
const app = new Koa()
// 资源类型表
const mimes = {
css: 'text/css',
less: 'text/less',
html: 'text/html',
txt: 'text/plain',
xml: 'text/html',
gif: 'image/gif',
ico: 'image/x-icon',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
png: 'image/png',
svg: 'image/svg+xml',
tiff: 'image/tiff',
json: 'application/json',
pdf: 'application/pdf',
swf: 'application/x-shockwave-flash',
wav: 'audio/x-wav',
wma: 'audio/x-ms-wma',
wmv: 'video/x-ms-wmv'
}
// 解析请求的资源类型
const parseMime = (url) => {
let extName = path.extname(url);
extName = url == '/' ? extName.slice(1) : 'html'
return mimes[extName]
}
// 获取请求资源的内容
const parseStatic = (url) => {
let filePath = path.resolve(__dirname, `.${url}`);
if (url == '/') {
filePath = `${filePath}/index.html`
}
return fs.readFileSync(filePath)
}
app.use(async(ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
ctx.set.body = parseStatic(url)
})
app.listen(3000, () => {
console.log('starting at port 3000')
})
- 启动项目
node index.js
我们打开浏览器,访问
localhost:3000,便可以看到我们的index.html页面以及加载的相应资源了。
现在,我们的静态服务器已经准备好了,接下来,我们就通过实现强缓存和协商缓存这两种资源缓存策略,来深入http缓存。 **
强缓存
首先,强缓存策略的定义是:强缓存会为资源设置一个过期时间,在强缓存阶段也就是资源没有过期时,不会向服务器发送请求,而是直接从缓存中读取资源,资源过期后再重新请求资源以更新。那么,现在我们根据缓存方式的3个方面,来解析如何实现强缓存。
- 缓存的存储策略:不设置,请求资源默认存储在浏览器本地
- 缓存的过期策略:通过Expires或者max-age进行设置
- 缓存的更新策略:不设置,缓存过期默认revalidate
实战1:Expires
// index.js
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
// 设置过期时间是现在的30000毫秒后,也就是现在的30秒后
const deadline = new Date(Date.now() + 30000).toGMTString()
ctx.set('Expires', deadline)
ctx.body = parseStatic(url)
});
注: 如下所说的浏览器都指的是chrome
- 打开浏览器,访问localhost:3000
第一次请求页面以及其资源,返回200。
以css资源为例,请求响应的response header如下图所示:
- 在30s内,刷新页面
第二次请求页面及其资源,会直接从内存缓存(memory cache)中读取资源,花费时间为0ms
- 过了30s后,刷新页面
结论: 设置了expires字段的资源,会在设置的expires过期后,重新请求资源,过期前,则直接读取浏览器的本地缓存
实站2:max-age
// index.js
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
// 设置过期时间为30秒后
ctx.set('Cache-Control', 'max-age=30')
ctx.body = parseStatic(url)
});
- 打开浏览器,访问localhost:3000:如下所示
以css资源为例,请求响应的response header如下图所示:
- 在30s内,刷新页面
- 过了30s后,刷新页面:如下所示
结论: 设置了max-age字段的资源,会在设置的max-age过期后,重新请求资源,过期前,则直接读取浏览器的本地缓存
实战3:Expires vs max-age
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
// 同时设置max-age和Expires
ctx.set('Cache-Control', 'max-age=20');
const deadline = new Date(Date.now() + 60000).toGMTString()
ctx.set('Expires', deadline);
ctx.body = parseStatic(url)
});
- 打开浏览器,访问localhost:3000
- 在20s内,刷新页面
- 在20s后,60s内重新刷新页面
结论:同时设置了max-age和expires的资源,以max-age为主。
协商缓存
首先,协商缓存的定义是:每次请求资源,都需要向服务端发送请求,并通过本地资源和服务端资源对比的方式,来确认资源是否发生变化。如果资源发生变化,则返回200和最新的资源到客户端。如果资源未发生变化,则返回304后客户端读取本地缓存。要实现协商缓存需要通过Etag或者If-Modified字段来实现。当服务端为响应资源加上了Etag或者If-Modified字段时,下次请求,客户端会自动带上If-None-Match字段或者If-Modified-Since字段。
不为资源设置过期时间,浏览器会自动启动启发式缓存。启发式缓存会默认为资源设置一个过期时间,该时间为**Date - Last-Modifed的值的10%作为缓存时间。**所以在使用Last-Modified实现协商缓存策略时,需要通过Cache-Control的no-cache值关闭启发式缓存。
实现协商缓存分为下面两种情况:
- 情况1:不为资源设置过期时间,浏览器会自动启动启发式缓存,资源过期后,通过协商缓存的方式验证资源是否更新
- 情况3:为资源设置过期时间,资源过期后,通过协商缓存的方式验证资源是否更新
- 情况3:不为资源设置过期时间,且关闭启发式缓存。每次请求都需要向服务器发送请求验证资源是否过期。
实战4:Etag&If-None-Match
代码如下:
const md5 = (data) => {
let hash = crypto.createHash('md5');
return hash.update(data).digest('base64');
}
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
// 计算设置etag,并进行对比验证
const buffer = parseStatic(url)
const fileMd5 = md5(buffer); // 生成文件的md5值
const noneMatch = ctx.request.headers['if-none-match']
if (noneMatch === fileMd5) {
ctx.status = 304;
return;
}
console.log('Etag 缓存失效')
ctx.set('Etag', fileMd5)
ctx.body = buffer
});
- 打开浏览器,访问localhost:3000:如下所示
- 紧接着再次刷新页面
我们在来看看请求css资源的响应头:etag和If-None-Match相等
所以资源没有发生变化,返回304
- 修改css后,再次刷新页面
同样的,我们来看看响应头:
因为css资源发生了变化,所以返回200,并返回最新的资源
- 紧接着,再次刷新页面
实战5:LastModified&If-Modified-Since
代码如下:
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
const filePath = path.resolve(__dirname, `.${url}`)
const stat = fs.statSync(filePath)
// 文件的最后修改时间
const mtime = stat.mtime.toGMTString();
const ifModifiedSince = ctx.request.header['if-modified-since']
if (mtime === ifModifiedSince) {
ctx.status = 304
return
}
console.log('协商缓存 Last-Modifed失效')
// 关于启发式缓存
ctx.set('Cache-Control', 'no-cache')
ctx.set('Last-Modified', mtime)
ctx.body = parseStatic(url)
});
通过如上代码可以实现和如上例子相同的效果。你可以通过如上例子同样的方式进行验证。
强缓存+协商缓存
强缓存加上协商缓存。意味着我们会为资源设置一个过期时间,只有资源过期后,才会向服务端发送请求,来对比本地资源和服务端资源,以 确认资源是否发生变化。如果资源发生变化,则返回200和最新的资源到客户端。如果资源未发生变化,则返回304,并从本地缓存中读取资源。
实战6:Last-Modified + 启发式缓存
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
const filePath = path.resolve(__dirname, `.${url}`)
const stat = fs.statSync(filePath)
// 文件的最后修改时间
const mtime = stat.mtime.toGMTString();
const ifModifiedSince = ctx.request.header['if-modified-since']
if (mtime === ifModifiedSince) {
ctx.status = 304
return
}
console.log('协商缓存 Last-Modifed失效')
ctx.set('Cache-Control', 'must-revalidate')
ctx.set('Last-Modified', mtime)
ctx.body = parseStatic(url)
});
- 打开浏览器,访问localhost:3000:如下所示
- 再次刷新页面
此时,资源的缓存时间是由启发式缓存算法进行计算而得。再次刷新页面,由于资源的缓存时间还未过期,所以浏览器会直接从memory cache中读取该资源。
- 修改css文件
#app {
color: #FBDC5C;
font-size: 50px;
}
- 再次刷新页面
如上所示,根据启发式缓存的计算方法所计算出来的资源缓存时间表示该资源的已经过期。所以会重新发出请求,因为css文件已经被更改,因此服务端会返回最新的css文件资源。
- 再次刷新页面
由于资源早已过期。所以直接进入协商缓存阶段,由于css文件没有更改,因此返回304后,浏览器直接从本地缓存中读取该文件。
实战7:max-age + Etag/Last-Modified
const md5 = (data) => {
let hash = crypto.createHash('md5');
return hash.update(data).digest('base64');
}
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
// 为资源设置30s的缓存时间
ctx.set('Cache-Control', 'max-age=60')
// 计算设置etag,并进行对比验证
const buffer = parseStatic(url)
const fileMd5 = md5(buffer); // 生成文件的md5值
const noneMatch = ctx.request.headers['if-none-match']
if (noneMatch === fileMd5) {
ctx.status = 304;
return;
}
console.log('Etag 缓存失效')
ctx.set('Etag', fileMd5)
ctx.body = buffer
});
- 打开浏览器,访问localhost:3000:如下所示
- 在60s内,再次刷新页面
- 在60s后,再次刷新页面
- 修改css文件后,紧接着在60s内,再次刷新页面
这时候不会返回最新的css资源。
- 在60s后,再次刷新页面
由于资源的缓存已过期,所以会向服务器发送请求,验证本地缓存资源是不是最新的。由于本地缓存css资源不是最新的,所以服务器会返回最新的css资源
Memory Cache&Disk Cache
在上面的例子中,缓存没有过期的时候,会直接从memory cache中读取缓存。memory cache就是内存缓存,在浏览器中,一旦关闭tab,内存中的缓存也就被释放了。disk cache是磁盘缓存,就算关闭tab,也不会被释放。现在,我们就通过一个小实验来看看什么时候浏览器会读取memory cache,什么时候会读取disk cache。
代码如下:
// index.js
app.use(async (ctx) => {
const url = ctx.request.url;
ctx.set('Content-Type', parseMime(url))
// 设置过期时间为1天
ctx.set('Cache-Control', 'max-age=86400')
ctx.body = parseStatic(url)
});
- 打开浏览器,访问localhost:3000
- 继续刷新页面
- 现在我们关闭该tab,重新在浏览器中新开一个tab,访问localhost:3000
从上图,我们可以看出,资源是从disk cache中读取的。所以,可以证明,当关闭tab后,memory cache会被清除,memory cache被清除后,浏览器就会从disk cache中读取资源。
总结
- 对资源只使用强缓存策略:资源没有过期之前,直接从本地缓存中读取缓存资源。资源过期后,则会向服务端请求资源,无论该资源是否是最新的,都会返回。
- 对资源只使用协商缓存策略:则每次请求资源都会向服务端发送请求。如果经过服务端对比后资源没有发生改变,则返回304,然后客户端从本地缓存中读取资源并返回。如果经过服务端对比后资源发生变化,则返回200以及最新资源。
- 对资源使用强缓存+协商缓存策略。资源没有过期之前,直接从本地缓存中读取缓存资源。资源过期后,则会向服务端请求资源,如果经过服务端对比后资源没有发生改变,则返回304,然后客户端从本地缓存中读取资源并返回。如果经过服务端对比后资源发生变化,则返回200以及最新资源。