Cache-Control在请求头和响应头里的区别

12,687 阅读9分钟

1、问题的由来

笔者曾在掘金上浏览技术文章,来学习http cache的知识。讲到的最多的当然是强缓存和协商缓存。关于各个字段的含义,我们可以找到这张图: 0c82d0049c3f4f57bf66d8effcb25ed5_tplv-k3u1fbpfcp-watermark.image

多数文章会告诉我们Response Header里的Cache-Control各个取值,代表着不同的含义。但很少有文章提及到Request Header里的Cache-Control。有一天发现:

image.png

这是chrome刷新中,选择硬性重新加载,浏览器请求一个静态js文件的请求头。当时并不知道和响应头里的Cache-Control有啥区别。曾一度认为请求头里的Cache-Control是无效的。直到某一天,自己用nodejs实现了一次缓存设置。

2、背景知识

浏览器处理http cache的优先级

这里简单概括下顺序

  1. 先判断资源是否命中强缓存,命中则直接从disk里拿到资源;
  2. 如果没有命中强缓存,判断是否命中协商缓存,命中则走协商缓存;
  3. 如果命中了协商缓存,会发起请求,服务端根据Request Header里的If-None-Match(对应Etag)和If-Modified-Since(对应Last-Modified)判断资源是否过期,没过期则返回304状态码,浏览器依旧用disk里的资源。如果资源过期,则服务端会返回新的资源;
  4. 如果也没有命中协商缓存,则这个请求不走缓存策略,发起真实的请求,从服务端拿资源;

chrome的Network面板

一个典型的截图如下:

1620714357(1).png

Initiator理解为发起请求的对象,有以下值:

  • Parser :请求是由页面的html解析时发送,一般显示html的名称
  • Redirect:请求是由页面重定向发送,暂时没遇到过
  • script :请求是由script脚本处理发送,一般显示script脚本文件的名称
  • Other :请求是由其他过程发送的,比如页面里的Link链接点击。一般请求结果是html内容 Size的值理解为请求内容的大小,Time理解为请求消耗的时间:
  • 174B表示发起了实际的请求,这种请求一般时间不会是0;
  • memory cache表示是从内存中读取的缓存资源,速度极快,一般都是0ms;
  • disk cache表示从磁盘中读取的缓存资源,速度比memory cache慢,但比发起实际的请求要快; 需要注意的是,memory cache和disk cache虽然没有发起实际的请求,但都会有200的状态码,如下图:

微信图片_20210511145346.png

微信图片_20210511145427.png

3、浏览器访问页面的操作

刷新按钮的三个选项

右键点击刷新按钮会出现三个选项,如图:

image.png

访问页面的4种方式

地址栏回车、页面链接跳转、打开新窗口/标签页、history前进后退

这种方式会从强缓存开始判断,是用户浏览网页中最常用的方式。

点击刷新按钮、页面右键重新加载、f5、ctrl+R

这种方式会跳过强缓存,直接从协商缓存开始判断。

但需要注意的是Initiator值为Other的内容才会走协商缓存(通常只有一个,是html内容)。其他的内容,因为是从html里引入的(如script,link,img等),或者从script文件动态引入的。他们的Initiator通常是一个html文件,或者script文件,这些资源还是会依照自己的规则,从强缓存开始判断;

这种方式会在Request Header里添加Cache-Control:max-age=0,这是浏览器自己的行为

硬性重新加载、Ctrl+f5、Ctrl+Shift+R、勾选Disable cache后点刷新

这种方式,所有的资源(不论Initiator的值),都会跳过缓存判断,发起真实的请求,从服务端拿资源。但本地的缓存资源(如disk里的缓存)并没有删除。 这种方式会在Request Header里添加Cache-Control:no-cache和Pragma: no-cache,也是浏览器自己的行为

清空缓存并硬性重新加载

这种方式,相当于先删除缓存(如disk里的缓存),再执行硬性重新加载

4、不同之处

从访问页面的4种方式里,可以看出。在点刷新按钮、或者硬性重新加载等时,因为浏览器自己的行为,会在Request Header里加入对应的Cache-Control值。

需要弄清楚的是: 请求头里的Cache-Control影响的是当前这一次请求,响应头里的Cache-Control是告诉浏览器这样存储,下次依照这样来。影响的是下一次请求。 一般以上边的方式1(如url回车)访问时,默认是按照上次给的规则来。但浏览器在真正的发起这次请求时,可以有自己的行为,比如硬性重新加载,不走缓存

Request Header里Cache-Control的取值

Cache-Control:max-age=0

这个值表示,这个请求按照协商缓存的规则走,一定会发出真实的请求。这里和响应头里的max-age=0有不同

Cache-Control:no-cache

这个值一般会附带Pragma: no-cache,是为了兼容http1.0。表示这次请求不会读缓存资源,即便缓存没有过期,或者资源并没有修改,这样的请求不会有返回304的可能。这一点和响应头里的Cache-Control:no-cache是有区别的。

Request Header里Cache-Control只有这两个值可取,设置其他的值无效,比如设置Cache-Control:no-store是没有用的,这一点要和响应头里的Cache-Control区分开。

Request Header里的Cache-Control只有在浏览器刷新,硬性重新加载。这两种浏览器自己的行为中会被添加。 如果是一个常规的,设置了协商缓存(响应头里Cache-Control:no-cache),和不缓存(响应头里Cache-Control:no-cache)的请求,它在正常的,通过上文方式1访问时,是不会在请求头里添加Cache-Control值的。

5、前端对缓存策略的一个应用

单页面应用中,

  • 入口文件index.html设置为协商缓存,每次都向服务器发起请求,确定资源是否过期。
  • 其他的资源,css,js这些都会设置成强缓存。因为这些文件名在打包之后会带上hash值,如果修改了内容,那么打包之后因为hash值变化,所以文件名也是会变化的。这些文件在index.html里引入

6、如何设置请求头里的Cache-Control

在前边的介绍里,请求头里Cache-Control:max-age=0和Cache-Control:no-cache都是浏览器自己的行为,因为加载这个静态资源,如html、js、image等,用户是无法设置响应的Request Header的。

<meta>标签

开发者能否自己设置html等静态资源的请求头呢,目前没有发现这类方法。值得一提的是html里的<meta>标签,在旧版本里或许可以设置响应的请求头如:

<meta http-equiv="cache-control" content="max-age=180" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />

但这些方法在html5之后,是不会生效的。

setRequestHeader

有没有能设置请求头的请求呢,我们会想到js发起的ajax请求,这个请求的Initiator一般是一个js文件。我们可以通过xhr.setRequestHeader()设置一些请求头,比如Cache-Control的值。另外,浏览器对js设置请求头的功能是有限制的,比如hostcookieuser-agent这些是无法被js修改的。

经过笔者的测试

  • post这种请求,是无法设置Cache-Control和Etag这些值的,在响应头里设置无效,相应的在请求头设置Cache-Control也是没啥用的,不会有缓存。
  • 只发现get请求可以设置,可以在请求头里设置Cache-Control:max-age=0和Cache-Control:no-cache,来实现对应的操作。get请求时可以缓存的,只不过这样的操作并不常见。

7、附上测试用的文件

node环境运行的http.js的内容:

const http = require('http'); //引入http模块
const fs = require('fs'); //引入文件模块
const TIME = [60, 120];//第一个时间是Etag改变的时间,第二个max-age过期时间
const MAP = new Map([
    [0, '123456789'],
    [1, '987654321'],
]); //生成两个可选的Etag值

let FLAG = false; //状态,是否修改
const timer = setTimeout(() => {
    FLAG = !FLAG; //一定时间后,修改状态
}, TIME[0] * 1000);

const server = http.createServer((req, res) => {
    console.log(req.url);
    //浏览器默认还会请求/favicon.ico这个ico资源,所以需要对url做监听限制
    if(['/', '/get1'].includes(req.url)) {
        res.setHeader('Cache-Control', `public, max-age=${TIME[1]}`);
        //设置reponse header的Cache-Control,失效时间
        console.log(req.headers['if-none-match'], '请求头里的Etag');
        const nowEtag = MAP.get(FLAG ? 1 : 0); //当前的Etag值

        if (req.headers['if-none-match'] === nowEtag) {
            console.log(123,867);
            //检查request header里的if-none-match和当前的Etag是否一致
            //简单处理
            res.statusCode = 304;
            res.end();
        } else {
            res.setHeader('Etag', nowEtag); //设置协商缓存的Etag值
            if(req.url === '/'){
                const nowHtml = FLAG ? 'tempNew.html' : 'temp.html'; //当前应该渲染的页面
                fs.createReadStream(nowHtml).pipe(res); //指定返回temp.html文件
            }else if(req.url === '/get1'){
                res.end(JSON.stringify({
                    "a": Date.now(),
                    "b": "qwer"
                }))
            }
        }
    }
})

server.listen(3333, '127.0.0.1', () => {
    console.log(`Server running at http://127.0.0.1:3333/`);
})

node中处理一个post请求,在这里做一个备份,以后学习会用到

if (req.url === '/post1') {
    if (req.method == 'POST') {
        let postData = ''
        req.on('data', chunk => {
            postData += chunk.tostring()
        })
        req.on('end', () => {
            res.end(JSON.stringify({
                "a": Date.now(),
                "b": "qwer"
            }))
        })
    }
}

temp.html的内容:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <META HTTP-EQUIV="Pragma" CONTENT="no-cache">
        <meta http-equiv="cache-control" content="no-cache">
        <title>test page</title>
    </head>
    <body>
        <div>这是一个指定的首页</div>
        <ul>
            <li><button id="btn">GET</button></li>
            <li><button id="btn1">GET no-cache</button></li>
            <li><button id="btn2">GET max-age=0</button></li>
            <li><button id="btn3">GET no-store</button></li>
        </ul>
        <script type="text/javascript">
            window.onload = function() {
                const GET = ty => {
                    const options = { method: 'GET', };
                    ty && (options['headers'] = new Headers({ 'Cache-Control': ty }));
                    return fetch('/get1', options);
                    //使用fetch主要是简洁,XMLHttpRequest要写太多代码
                }

                document.addEventListener('click', function(e) {
                    if(e.target.id === 'btn'){
                        //no Cache-Control
                        GET()
                        .then(res => {
                            console.log(res.json());
                        })
                    }else if (e.target.id === 'btn1') {
                        //no-cache
                        GET('no-cache')
                        .then(res => {
                            console.log(res.json());
                        })
                    }else if(e.target.id === 'btn2') {
                        //max-age=0
                        GET('max-age=0')
                        .then(res => {
                            console.log(res.json());
                        })
                    }else if(e.target.id === 'btn3') {
                        //no-store
                        GET('no-store')
                        .then(res => {
                            console.log(res.json());
                        })
                    }
                })
            }
        </script>
    </body>
</html>

启动服务后,可以分别做下列两种操作:

  • 点一次普通GET的btn,然后做一次硬性重新加载,再点一次普通GET的btn,
  • 点一次普通GET的btn,然后做一次清空缓存并硬性重新加载,再点一次普通GET的btn, 这两组操作效果是不一样的,可验证这两种刷新的区别。 另外,点no-store对应的GET的btn,效果是和普通的btn一致,因为在请求头里设置Cache-Control:no-store是无效的。请求头里的Cache-Control:no-cache已经可以做到这次请求不走缓存了。

node.js方便了前端开发调试问题,在没有后端的情况下,进行mock数据,缓存设置等,确实很方便。