Web缓存
我们知道浏览器加载页面时候,除了加载html文件本身,页面引用的资源也会加载。但是这些外部资源并不是每次都会发生变更,如果每次都重新加载这些资源肯定会造成浪费,而且加载时间过长也会影响用户体验。
HTTP缓存技术就是为了解决这个问题而出现的,简单的讲HTTP缓存就是将静态的资源存储在浏览器内部,下次请求相同资源时可以直接使用。
所以说缓存听着很容易,但资源更新了怎么解决,因此需要有一系列的策略用来保证如果资源一旦更新,缓存也要随之更新。
实验环境搭建
为了说明 http缓存的效果,我们可以用node搭建一个简易的web服务来实验一下
// 为了说明资源更新的时间,创建一个时间动态返回当前的时间
// 这么写是为了让资源每隔一秒更新一次
function fn(){
let time=new Date().toString()
//用闭包保存定时器实例
let timer=null
return function(){
//定时器是个单例
timer=timer ||setInterval(()=>{
time=new Date().toString()
},1000)
return time
}
}
//每次执行 updateTime,如果相差指定时间 返回的time才会有变化
const updateTime=fn()
//创建web服务
const http=require('http')
http.createServer((req,res)=>{
//返回html
//返回js
const {url}=req
if('/'===url){
res.end(`
<html>
<meta charset="utf-8">
HTML UPDATE TIME ${updateTime()}
<script src="./main.js"></script>
</html>
`)
}else if('/main.js'==url){
const content=`document.writeln('<br>JS UPDATE TIME:${updateTime()}')`
res.statusCode=200
res.end(content)
}else if('/favicon.ico'==url){
res.end('')
}
})
.listen(4000,()=>{
console.log('http test')
})
现在每次刷新html和js显示的都是当前时间,这里我们没用使用任何缓存策略。接下去我们测试
强缓存
直接从本地副本对比读取,不去请求服务器。那如果资源更新了浏览器还在使用老的静态资源怎么办?答案就是使用定时器的方式设置静态资源有效期。如果超过有效期就认为缓存废除
Http 1.0 expires
expires是http1.0定义的缓存字段,当我们请求一个资源,服务器返回时,可以在 response header 中增加expires字段表示资源的过期时间。
Expires:Mon, 14 Mar 2022 08:41:06 GMT
是一个时间戳(准确的讲是格林尼治时间),当客户端再次请求该资源时会把客户端时间和该时间戳进行对比,如果大于该时间戳则已过期,否则直接使用该缓存资源。
//...
else if('/main.js'===url){
const content=`document.writeln('<br>JS UPDATE TIME:${updateTime()}')`
//设置缓存头部,资源30秒钟后过期
res.setHeader('Expires',new Date(Date.now()+30*1000).toUTCString())
res.statusCode=200
res.end(content)
}
//...
第一次请求 资源html 和 js资源都是从服务器返回,js资源缓存在本地
第二次请求 js从本地缓存中返回(html的时间已经到了35秒,而js 时间还停留在23秒)
HTTP 1.1 cache-control
expires 有个问题,发送请求是使用客户端的时间去对比。一者客户端和服务端的时间不见得一致,另一方面客户端的时间是可以随意修改的,因此缓存未必能满足预期。到了 http 1.1,新增了cache-control 字段来解决该问题,当cache-control和expires都存在时cache-control优先级更高。该字段是一个时间长度,单位秒,表示该资源过了多少秒后失效。当客户端请求资源的时候,如果发现资源还在有效期内则使用该缓存,它不依赖客户端时间。cache-control有 max-age、public和private等值
| Cache-directives | 说明 |
|---|---|
| public | 所有内容都将被缓存(客户端和代理服务器都可以缓存) |
| private | 内容只缓存到私有缓存中(客户端可以缓存) |
| no-cache | 需要使用协商缓存来验证缓存数据 |
| no-store | 所有内容都不会缓存 |
| must-revalidation/proxy-revalidation | 如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证 |
| max-age=xxx | 缓存的内容将在xxx秒后失效,这个选项只在http1.1可用,并如果和Last-Modified一起使用时,优先级较高 |
//...
else if('/main.js'==url){
const content=`document.writeln('<br>JS UPDATE TIME:${updateTime()}')`
//expires 过期时间为10s
res.setHeader('Expires',new Date(Date.now()+10*1000).toUTCString())
//设置强缓存cache-control 30秒过期
res.setHeader('Cache-Control','max-age=30')
res.statusCode=200
res.end(content)
}
//...
从上图的结果可以表明,当请求发生在14秒时,js资源还使用在02秒的缓存。此时expires已经失效,命中的是cache-control,由此可见cache-control的优先级更高。
协商缓存
expires和cache-control 都会访问本地缓存直接验证看是否过期,如果没有过期直接使用本地缓存。但如果设置了no-cache和no-store 则本地缓存会被忽略,会去请求服务器验证资源是否更新,如果没更新才继续使用本地缓存,这时返回的是304,这个被称为协商缓存。常见的协商缓存有 last-modified 和 e-tag。
协商缓存简单的说就是浏览器和服务器之间就是否需要使用缓存在做协商。如果协商的结果是需要更新就会返回200并返回更新内容。如果不需要只需要返回状态码304不用返回内容。
last- modified 和 if- modified-since
这是一组通过协商修改时间为基础的策略
//...
else if('/main.js'==url){
const content=`document.writeln('<br>JS UPDATE TIME:${updateTime()}')`
//协商缓存,每次返回都要带新更新时间
res.setHeader('Cache-Control','no-cache')
res.setHeader('last-modified',new Date().toUTCString())
//设置30秒过期
if(new Date(req.headers['if-modified-since']).getTime()+30*1000>Date.now()){
res.statusCode=304
res.end()
return
}
res.statusCode=200
res.end(content)
}
//...
可以看到当本地有缓存以后
-
静态资源应答时都会通过last-modified来标示修改时间。
-
浏览器下次请求相同资源会将last-modified时间作为if-modified-since字段放在请求报文中用以询问服务器是否该资源过期。
-
服务器需要通过规则判断是否过期
-
过期时直接返回200并在body中放入更新内容
-
如果未过期则直接返回304状态码即可
etag & If-None-Match
除了协商时间,还可以通过内容协商缓存,一般的做法是对返回内容进行摘要(hash),通过摘要对比来判断内容是否有更新。
//为了看到效果我们可以将资源变化的时间间隔增加到30秒
function fn(){
let time=new Date().toString()
let timer=null
return function(){
timer=timer ||setInterval(()=>{
time=new Date().toString()
},30000)
return time
}
}
const updateTime=fn()
//...
else if('/main.js'==url){
const content=`document.writeln('<br>JS UPDATE TIME:${updateTime()}')`
//协商缓存
res.setHeader('Cache-Control','no-cache')
//使用crypto库对内容做hash计算
const crypto=require('crypto')
const hash=crypto.createHash('sha1').update(content).digest('hex')
res.setHeader('Etag', hash)
if(req.headers['if-none-match']===hash){
res.statusCode=304
res.end()
return
}
res.statusCode=200
res.end(content)
}
//...
可以看到,在30秒的时间间隔内,由于时间资源没有发生变化,etag值和第一次返回的一致,缓存命中后直接从缓存中读取资源。
类比last-modified
- 静态资源应答时都会通过etag来标识内容摘要
- 浏览器下次请求相同资源会将 etag的值作为 if-none-match 字段的值放在请求报文中用于询问服务器该资源是否过期
- 服务器需要通过将内容的摘要和if-none-match的值比对以确定是否过期
- 过期时直接返回200并从服务器返回更新内容
- 如果未过期返回304