热更新系列(一):浏览器缓存

热更新系列(一):浏览器缓存

之前面试的时候被问到一个问题,还蛮有意思:服务器上代码变化的时候,如何通知浏览器端更新文件?

你可能疑惑:

  • 浏览器进入页面的时候,发起请求,拿到最新的html文件,里面带上最新的js文件link不就好了吗
  • 这个东西在开发的时候,不是改一点他就更新一点的吗?怎么部署上去就不行了呢?

别急,让我们慢慢讲,本次主讲第一个问题

一、浏览器进入页面的时候,不是会get吗?那这个时候不就能进行页面的更新了吗?🧐

首先,这个思路很对,在浏览器第一次进入某个页面的时候,的确会向服务端发起一个GET请求,拿到html文件,但是注意我标红的地方:第一次

没错,在某些情况下,浏览器并不会去向服务端发起请求,那是在什么时候呢?其实,为了浏览器的性能,浏览器会尽量减少网络请求的个数,其他的则采用缓存处理

缓存又是什么呢?

1. 不同种类的缓存

首先,最大的分类,可以分为两种不同类型:私有缓存共享缓存

  • 私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。存储不与其他客户端共享,因此私有缓存可以存储该用户的个性化数据,浏览器缓存主要分为三大类:Cookie SeesionStorage localStorage,感兴趣的可以去了解一下

  • 共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存托管缓存

    • 代理缓存: 简单来说就是存在代理服务器上的缓存,请求是如果满足要求,从代理服务器获取数据,不用向主服务器发起请求
    • 托管缓存: 个人理解与代理缓存区别主要在于代理缓存多由浏览器决定,而托管缓存则是程序员主导

2. 基于age的缓存策略

浏览器对于缓存的处理主要基于ageage可以理解为生命周期,如果age超出某个阈值该缓存就会被判定为stale,即腐烂,那浏览器是如何利用age的呢?如果是你,你会怎么设计呢?

2.1 首先,总得有age

age肯定得从后端传过来吧,那放在哪里呢?

http的策略是在请求中专门开辟一个Cache-Control字段,用于缓存的管理

服务端可以在返回中带上

1
Cache-Control: max-age=10086

这样的字段,这个就声明了这个请求的最大生命周期是10086毫秒,那到现在是不是还觉得有些不靠谱,只有时间长度,那我从哪开始算呢?从我接受到请求开始吗?

肯定不是!

说到这点,就要谈谈http的历史了,http在初期1.0的时候,采用的缓存控制字段是expires,这是个啥呢?这是一个绝对时间,比如说:

1
Expires: Sun, 21 JUL 2024 18:00:00 GMT

但是这样有什么坏处呢?

  • 一是每台机器的时钟可能不一样,因此可能会出现客户端根据expires判断并为过期而资源实际已经发生变化的情况。还有居心叵测之人调整系统时钟来诱发匪夷所思的问题
  • 二是这么一长串,还带日期、字母,难以解析

因此,在http/1.1中,引入了max-age这个概念,现在让我们回到上一个问题:只有时间长度,那我从哪开始算呢?从我接受到请求开始吗?

如果是按照系统的时钟计算,那不就又和之前的expires别无二致了嘛,不安全还不准确,网络波动大了可能会造成严重问题,为此,max-age的计算是根据后端返回中的TimeStamp字段也就是生成时间计算的。

当然,这只是为经过缓存,直接发送请求的结果,最后缓存也是缓存在浏览器缓存中,然而,别忘了,还有一种共享缓存呢!那如果加上共享缓存,会有不同吗?


答案是肯定的

假设现在有一个请求从浏览器发出,它将首先访问缓存服务器,而代理服务器上的数据如果在有效期内,就会返回出去,不把请求转发给目的服务器,但是如果再极致一点,再把从代理服务器过来的缓存再在本地缓存一下呢?代理服务器也很忙,没事别去打扰他!其实浏览器也想到咯

那就是Age字段:他负责计算从生成开始,这个请求已经经过了多少时间,在别人请求该资源时,会把max-ageage一起带上:

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Sun, 21 JUL 2024 18:00:00 GMT
Cache-Control: max-age=600
Age: 400

<!doctype html>


这就代表着浏览器接受到这个请求的时候,只有600 - 400 = 200的有效时间咯,过200ms之后,浏览器会再次向代理服务器发送请求,而代理服务器会向目的服务器发送请求,然而再次发请求,就一定要去换新数据吗?

3. 验证响应

在这一部分之上的部分可以称为强制缓存

而接下来的内容则属于协商缓存

过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证

验证是通过使用包含 If-Modified-SinceIf-None-Match 请求标头的条件请求完成的。

If-Modified-Since

以下响应在 22:22:22 生成,max-age 为 1 小时,因此你知道它在 23:22:22 之前是有效的。

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>

到 23:22:22 时,响应会过时并且不能重用缓存。因此,下面的请求显示客户端发送带有 If-Modified-Since 请求标头的请求,以询问服务器自指定时间以来是否有任何的改变。

1
2
3
4
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified

由于此响应仅表示“没有变化”,因此没有响应主体——只有一个状态码——因此传输大小非常小。

1
2
3
4
5
HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

收到该响应后,客户端将存储的过期响应恢复为有效的,并可以在剩余的 1 小时内重复使用它。

服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。

为了解决这些问题,ETag 响应标头被标准化作为替代方案。

ETag/If-None-Match

ETag 响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。

举个例子,如果 ETag 标头使用了 hash 值,index.html 资源的 hash 值是 deadbeef,响应如下:

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "deadbeef"
Cache-Control: max-age=3600

<!doctype html>

如果该响应是陈旧的,则客户端获取缓存响应的 ETag 响应标头的值,并将其放入 If-None-Match 请求标头中,以询问服务器资源是否已被修改:

1
2
3
4
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "deadbeef"

如果服务器为请求的资源确定的 ETag 标头的值与请求中的 If-None-Match 值相同,则服务器将返回 304 Not Modified

但是,如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将其改为 200 OK 和资源的最新版本进行响应。

备注: 在评估如何使用 ETagLast-Modified 时,请考虑以下几点:在缓存重新验证期间,如果 ETagLast-Modified 都存在,则 ETag 优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified 是不必要的。然而,Last-Modified 不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS) 系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETagLast-Modified

强制重新验证

如果你不希望重复使用响应,而是希望始终从服务器获取最新内容,则可以使用 no-cache 指令强制验证。

通过在响应中添加 Cache-Control: no-cache 以及 Last-ModifiedETag——如下所示——如果请求的资源已更新,客户端将收到 200 OK 响应,否则,如果请求的资源尚未更新,则会收到 304 Not Modified 响应。

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache

<!doctype html>

max-age=0must-revalidate 的组合与 no-cache 具有相同的含义。

1
Cache-Control: max-age=0, must-revalidate

max-age=0 意味着响应立即过时,而 must-revalidate 意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache 相同。

然而,max-age=0 的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache 这一指令——因此为了解决这个限制,max-age=0 被用作解决方法。

但是现在符合 HTTP/1.1 的服务器已经广泛部署,没有理由使用 max-age=0must-revalidate 组合——你应该只使用 no-cache

4. 现在知道为什么有时候让用户刷新页面也拿不到新数据了吧

就是因为可能在返回的请求中,带上了Cache-Control: max-age=10086,然而也确实时常带上,因为html文件基本不会有太大的变化,所以在一段时间内,只要没过max-age,那你的更新就不会成现在用户的页面上

那你可能又要问了:那我直接no-cache或者no-cache不就行了?

实际上也确实可以这么干,我一开始也是这么回答面试官的,但是这样做会有弊端:必须用户刷新页面之后才能看到新的内容,因为只有刷新的时候才会去请求新的html

那又怎么办呢?那我们平常开发的 dev server 又是怎么做到实时更新的呢?那就是另一个故事咯


热更新系列(一):浏览器缓存
http://baidu.com/2024/07/21/HMR-storage/
作者
KB
发布于
2024年7月21日
许可协议