热更新系列(一):浏览器缓存
热更新系列(一):浏览器缓存
之前面试的时候被问到一个问题,还蛮有意思:服务器上代码变化的时候,如何通知浏览器端更新文件?
你可能疑惑:
- 浏览器进入页面的时候,发起请求,拿到最新的html文件,里面带上最新的js文件link不就好了吗
- 这个东西在开发的时候,不是改一点他就更新一点的吗?怎么部署上去就不行了呢?
别急,让我们慢慢讲,本次主讲第一个问题
一、浏览器进入页面的时候,不是会get吗?那这个时候不就能进行页面的更新了吗?🧐
首先,这个思路很对,在浏览器第一次
进入某个页面的时候,的确会向服务端发起一个GET
请求,拿到html文件,但是注意我标红的地方:第一次。
没错,在某些情况下,浏览器并不会去向服务端发起请求,那是在什么时候呢?其实,为了浏览器的性能,浏览器会尽量减少网络请求的个数,其他的则采用缓存
处理
那缓存
又是什么呢?
1. 不同种类的缓存
首先,最大的分类,可以分为两种不同类型:私有缓存和共享缓存。
私有缓存
是绑定到特定客户端的缓存——通常是浏览器缓存
。存储不与其他客户端共享,因此私有缓存可以存储该用户的个性化数据,浏览器缓存主要分为三大类:Cookie
SeesionStorage
localStorage
,感兴趣的可以去了解一下共享缓存
位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存和托管缓存。代理缓存
: 简单来说就是存在代理服务器上的缓存,请求是如果满足要求,从代理服务器获取数据,不用向主服务器发起请求托管缓存
: 个人理解与代理缓存区别主要在于代理缓存多由浏览器决定,而托管缓存则是程序员主导
2. 基于age
的缓存策略
浏览器对于缓存的处理主要基于age
,age
可以理解为生命周期,如果age
超出某个阈值该缓存就会被判定为stale
,即腐烂,那浏览器是如何利用age
的呢?如果是你,你会怎么设计呢?
2.1 首先,总得有age
吧
那age
肯定得从后端传过来吧,那放在哪里呢?
http
的策略是在请求中专门开辟一个Cache-Control
字段,用于缓存的管理
服务端可以在返回中带上
1 |
|
这样的字段,这个就声明了这个请求的最大生命周期是10086
毫秒,那到现在是不是还觉得有些不靠谱,只有时间长度,那我从哪开始算呢?从我接受到请求开始吗?
肯定不是!
说到这点,就要谈谈http
的历史了,http
在初期1.0的时候,采用的缓存控制字段是expires
,这是个啥呢?这是一个绝对时间,比如说:
1 |
|
但是这样有什么坏处呢?
- 一是每台机器的时钟可能不一样,因此可能会出现客户端根据
expires
判断并为过期而资源实际已经发生变化的情况。还有居心叵测之人调整系统时钟来诱发匪夷所思的问题 - 二是这么一长串,还带日期、字母,难以解析
因此,在http/1.1中,引入了max-age
这个概念,现在让我们回到上一个问题:只有时间长度,那我从哪开始算呢?从我接受到请求开始吗?
如果是按照系统的时钟计算,那不就又和之前的expires
别无二致了嘛,不安全还不准确,网络波动大了可能会造成严重问题,为此,max-age
的计算是根据后端返回中的TimeStamp
字段也就是生成时间计算的。
当然,这只是为经过缓存,直接发送请求的结果,最后缓存也是缓存在浏览器缓存中,然而,别忘了,还有一种共享缓存
呢!那如果加上共享缓存,会有不同吗?
答案是肯定的
假设现在有一个请求从浏览器发出,它将首先访问缓存服务器,而代理服务器上的数据如果在有效期内,就会返回出去,不把请求转发给目的服务器,但是如果再极致一点,再把从代理服务器过来的缓存再在本地缓存一下呢?代理服务器也很忙,没事别去打扰他!其实浏览器也想到咯
那就是Age
字段:他负责计算从生成开始,这个请求已经经过了多少时间,在别人请求该资源时,会把max-age
和age
一起带上:
1 |
|
这就代表着浏览器接受到这个请求的时候,只有600 - 400 = 200
的有效时间咯,过200ms之后,浏览器会再次向代理服务器发送请求,而代理服务器会向目的服务器发送请求,然而再次发请求,就一定要去换新数据吗?
3. 验证响应
在这一部分之上的部分可以称为
强制缓存
而接下来的内容则属于
协商缓存
过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证。
验证是通过使用包含 If-Modified-Since
或 If-None-Match
请求标头的条件请求完成的。
If-Modified-Since
以下响应在 22:22:22 生成,max-age
为 1 小时,因此你知道它在 23:22:22 之前是有效的。
1 |
|
到 23:22:22 时,响应会过时并且不能重用缓存。因此,下面的请求显示客户端发送带有 If-Modified-Since
请求标头的请求,以询问服务器自指定时间以来是否有任何的改变。
1 |
|
如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified
。
由于此响应仅表示“没有变化”,因此没有响应主体——只有一个状态码——因此传输大小非常小。
1 |
|
收到该响应后,客户端将存储的过期响应恢复为有效的,并可以在剩余的 1 小时内重复使用它。
服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。
为了解决这些问题,ETag
响应标头被标准化作为替代方案。
ETag/If-None-Match
ETag
响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。
举个例子,如果 ETag
标头使用了 hash 值,index.html
资源的 hash 值是 deadbeef
,响应如下:
1 |
|
如果该响应是陈旧的,则客户端获取缓存响应的 ETag
响应标头的值,并将其放入 If-None-Match
请求标头中,以询问服务器资源是否已被修改:
1 |
|
如果服务器为请求的资源确定的 ETag
标头的值与请求中的 If-None-Match
值相同,则服务器将返回 304 Not Modified
。
但是,如果服务器确定请求的资源现在应该具有不同的 ETag
值,则服务器将其改为 200 OK
和资源的最新版本进行响应。
备注: 在评估如何使用 ETag
和 Last-Modified
时,请考虑以下几点:在缓存重新验证期间,如果 ETag
和 Last-Modified
都存在,则 ETag
优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified
是不必要的。然而,Last-Modified
不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS) 系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETag
和 Last-Modified
。
强制重新验证
如果你不希望重复使用响应,而是希望始终从服务器获取最新内容,则可以使用 no-cache
指令强制验证。
通过在响应中添加 Cache-Control: no-cache
以及 Last-Modified
和 ETag
——如下所示——如果请求的资源已更新,客户端将收到 200 OK
响应,否则,如果请求的资源尚未更新,则会收到 304 Not Modified
响应。
1 |
|
max-age=0
和 must-revalidate
的组合与 no-cache
具有相同的含义。
1 |
|
max-age=0
意味着响应立即过时,而 must-revalidate
意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache
相同。
然而,max-age=0
的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache
这一指令——因此为了解决这个限制,max-age=0
被用作解决方法。
但是现在符合 HTTP/1.1 的服务器已经广泛部署,没有理由使用 max-age=0
和 must-revalidate
组合——你应该只使用 no-cache
。
4. 现在知道为什么有时候让用户刷新页面也拿不到新数据了吧
就是因为可能在返回的请求中,带上了Cache-Control: max-age=10086
,然而也确实时常带上,因为html文件基本不会有太大的变化,所以在一段时间内,只要没过max-age
,那你的更新就不会成现在用户的页面上
那你可能又要问了:那我直接no-cache
或者no-cache
不就行了?
实际上也确实可以这么干,我一开始也是这么回答面试官的,但是这样做会有弊端:必须用户刷新页面之后才能看到新的内容,因为只有刷新的时候才会去请求新的html
那又怎么办呢?那我们平常开发的 dev server
又是怎么做到实时更新的呢?那就是另一个故事咯