浏览器缓存
浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,主要是依据http头中相关字段进行的。
第一次请求
浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。
第二次请求
浏览器第二次向服务器发起该请求可以分为3种情况:
- 如果浏览器缓存中不存在缓存资源和缓存标识,强制缓存失效,直接向服务器发起请求。
- 存在该缓存结果和缓存标识(Expires和Cache-Control),且该结果尚未失效,强制缓存生效,直接返回该结果且状态码为200。
- 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存。
demo 验证
Talk is cheap, show me the code.
不对响应头做任何修改
创建一个简单的 server
index.js
const http = reqire('http')
http.createServer((req, res) => {
const {url, headers} = req // 从请求数据中解构出 url 和 headers
if (url === '/') {
res.end(`
<html>
html update Time ${new Date()}
<script src = '/main.js'></script>
</html>
`)
} else if (url === '/main.js') {
const content = `document.writeln('<br /> JS Update Time: ${new Date()}')`
res.statusCode = 200
res.end('content')
}
}).listen(3000, () => {
console.log('Http is running at 3000 port')
})
通过 node index.js 启动服务, 并在浏览器中访问 localhost:3000, 打开 network, 取消 Disable cache 并刷新浏览器, 会发现 html 和 js 的更新时间是同步的, 右侧请求返回的状态码是 200
强缓存
控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高
Expires
想要触发浏览器的缓存, 只需要在响应头中添加 Expires 字段就可以, Expires 的值是格林尼治时间,可以是未来时间, 也可以是过去时间。
const getGMTTime = (time = Date.now()) => {
return new Date(time).toGMTString()
}
index.js
const http = reqire('http')
http.createServer((req, res) => {
const {url, headers} = req // 从请求数据中解构出 url 和 headers
if (url === '/') {
res.end(`
<html>
html update Time ${getGMTTime()}
<script src = '/main.js'></script>
</html>
`)
} else if (url === '/main.js') {
const content = `document.writeln('<br /> JS Update Time: ${getGMTTime()}')`
// 返回数据时,加上响应头
res.setHeader('Expires', getGMTTime(Date.now() + 10 * 1000)) // 设置10秒后过期
res.statusCode = 200
res.end('content')
}
}).listen(3000, () => {
console.log('Http is running at 3000 port')
})
- 我们可以看到响应头中多了一个Expires字段,并且可以看到通过刷新浏览器后, JS 的更新一直保持在14:57:49 而 html 的更新时间是 14:57:56, 并且可以看到 js 的请求状态码是200并且数据来源是 from memory cache。
- 10 秒之后再次刷新, 浏览器会再次发送请求
注意
- Expires 字段是 http 1.0 的东西, 现在浏览器多使用 http 1.1, 可以使用 Cache-Control 字段
- Expires 的值是一个绝对时间,如果浏览器时间和服务器时间不同步, 浏览器时间设置了一个很未来的时间, 那么较短的过期时间是没有用的
- 缓存过期后, 不管服务器上的资源是否有改动, 服务器都会重新读取资源并返回给浏览器。
Cache-Control
针对上面的前两个问题, 我们用 Cache-Control 来解决
Cache-Control 有很多可选值, 比较常用的有 max-age, no-cache, no-store, must-revalidate
max-age: max-age 的值是相对时间, 单位是秒。如 max-age=10 相当于告诉浏览器资源10秒后过期。有一点需要注意,max-age 的时间计算起点是响应报文的创建时刻(即Date 字段, 也就是离开服务器的时间),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。 no-store: 不允许缓存, 用于某些变化非常频繁的数据 no-cache: 可以缓存,但是使用之前必须要去服务器验证是否过期, 是否有最新的版本。 must-revalidate: 如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
const http = reqire('http')
http.createServer((req, res) => {
const {url, headers} = req // 从请求数据中解构出 url 和 headers
if (url === '/') {
res.end(`
<html>
html update Time ${getGMTTime()}
<script src = '/main.js'></script>
</html>
`)
} else if (url === '/main.js') {
const content = `document.writeln('<br /> JS Update Time: ${getGMTTime()}')`
// 返回数据时,加上响应头
res.setHeader('Cache-Control', 'max-age=10, no-cahce') // 设置10秒后过期
res.statusCode = 200
res.end('content')
}
}).listen(3000, () => {
console.log('Http is running at 3000 port')
})
下面这两种情况,自己测试一下。
> Ctrl + F5 :强制刷新其实时发了一个 'Cache-Control: no-cache'
> 点击浏览器的'前进', '后退' 按钮,再看开发者工具,会发现 'from disk cache', 是因为这么做浏览器只用最基本的请求头, 没有 'Cache-Control' ,所以就会检查缓存,直接利用之前的资源,不再进行网络通信。
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
与协商缓存相关的字段有Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match优先级比Last-Modified / If-Modified-Since高
Last-Modified
http 协议定义了一系列 'If' 开头的 '条件请求'
常用的有: if-modified-since 和 if-none-match, 需要第一次的响应报文预先提供 'Last-Modified' 和 'Etag', 然后第二次请求时就可以带上缓存里的原值, 验证资源是否是最新的。 如果资源没有变, 服务器就回应一个 '304 Not Modified', 表示缓存依然有效, 浏览器就可以更新一下有效期, 然后放心大胆地使用缓存了。
Last-Modified 就是文件的最后修改时间,。
const http = reqire('http')
http.createServer((req, res) => {
const {url, headers} = req // 从请求数据中解构出 url 和 headers
if (url === '/') {
res.end(`
<html>
html update Time ${getGMTTime()}
<script src = '/main.js'></script>
</html>
`)
} else if (url === '/main.js') {
const content = `document.writeln('<br /> JS Update Time: ${getGMTTime()}')`
// 返回数据时,加上响应头
res.setHeader('Cache-Control', 'max-age=5, no-cahce') // 设置10秒后过期
res.setHeader('Last-Modified', new Date())
if(new Date(headers['if-modified-since']).getTime() + 10 * 1000 > Date.now()) {
res.statusCode = 304 // 如果命中缓存, 则返回 304
res.end()
return
}
res.statusCode = 200
res.end('content')
}
}).listen(3000, () => {
console.log('Http is running at 3000 port')
})
使用Last-Modified 会有以下问题
- 一个文件在一秒内修改了多次
- 文件定期更新,但内容没有变化
ETag
更精准地识别资源变动的情况
ETag 有 "强" "弱" 之分。 强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
Etag的工作原理 Etag在服务器上生成后,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改.我们常见的是使用If-None-Match.请求一个文件的流程可能如下: 新的请求 客户端发起HTTP GET请求一个文件(css ,image,js);服务器处理请求,返回文件内容和一堆Header(包括Etag,例如"2e681a-6-5d044840"),http头状态码为为200.
同一个用户第二次这个文件的请求 客户端在一次发起HTTP GET请求一个文件,注意这个时候客户端同时发送一个If-None-Match头,这个头中会包括上次这个文件的Etag(例如"2e681a- 6-5d044840"),这时服务器判断发送过来的Etag和自己计算出来的Etag,因此If-None-Match为False(none match 为 false, 就是 match 了, 就是两个 Etag相同, 也就是说文件没改变),不返回200,返 回304,客户端继续使用本地缓存;
注意.服务器又设置了Cache-Control:max-age和Expires时,会同时使用,也就是说在完全匹配If-Modified-Since和If-None-Match即检查完修改时间和Etag之后,服务器才能返回304。