资讯专栏INFORMATION COLUMN

30 分钟 HTTP 查漏补缺之 Vary

xiangchaobin / 1317人阅读

摘要:所以我们要时刻留意,在使用时,一定要根据缓存命中率作出调整,在不发生缓存错乱的情况之下,尽可能的提高资源的缓存命中率。

写在前面

最近抽空参加了几场大厂的面试,突然发现一个现象,就是不论面试偏服务端的职位还是偏客户端的职位,不论面试的 5 年以上的高级职位,还是 3 年左右的中级职位,面试官开头所问问题必然是关于 HTTP 的。

我记得之前找工作的时候,似乎都是先考察一些职位所需技能领域的基础知识,之后再考察关于 HTTP 的东西,现在大家都将 HTTP 的问题放到面试的开头来问,我觉的应该是越来越多的招聘者意识到,作为一个 Web 开发者,HTTP 真的是太重要了,必须要先考察。

回想起来,这几年我自己对于 HTTP 的学习大多是碎片化的,很多东西无法系统地在脑海中组织起来。虽然感觉 HTTP 整体的学习难度是比较低的,但是各个知识点交杂在一起又变得很复杂很难,相信大家都会有同感。同时有些知识点,如果在实际工作中没有采坑或者刻意深挖的话,很自然地就被忽略了。

由于在之前一次面试中,被狠狠地问了若干关于 Vary 的问题,所以想抽一些时间整理一下那些比较容易让人忽略的知识点,算是查漏补缺吧。

内容协商

首先需要了解的是内容协商这个术语。当我们通过某个 URI 来访问其指向的资源时,HTTP 协议可以通过内容协商机制提供资源的不同的展示形式。

如果缺少服务端开发经验话,对于这个概念可能会感到陌生,但其实我们在工作中几乎都会遇到它,比如在调用接口时,经常会用到 Accept: application/json 这个头部,有时可能会用到 Accept: application/xml,这就是内容协商,前者期望接口返回 json 格式的数据,而后者期望返回 xml 格式的数据。

一般客户端涉及的常见头部有以下几个:

Accept: 声明客户端可以处理的资源格式

Accept-Charset: 声明客户端可以处理的字符集类型

Accept-Language: 声明客户端可以理解的自然语言

Accept-Encoding: 声明客户端支持的编码格式

而服务端涉及的常见头部包括:

Content-Type: 指示资源的 MIME 类型

Content-Language: 指示该资源所期望的自然语言

Content-Encoding: 指示资源使用该编码格式进行内容转换

仔细观察的话,会发现它们其实存在着一定程度的对应关系。原因也很简单,既然是协商,那必然就会和两个人在进行说话一样,如果两者之间的对话内容没有关联,他们还怎么沟通呢?客户端和服务端进行沟通同理。

如果想详细了解该机制,可以参考MDN的文档,很详细,这里就不多说了。

这里顺带说明一下,对于内容协商机制中涉及的头部,从 web 发展历史上来看已经没有什么实质的用途了,原因如下(有兴趣的话可以阅读这篇wiki):

Accept-Charset: 由于 utf-8 成为主流的字符集类型,所以使用其他字符集类型的服务可以将其转换为 utf-8 类型

Accept-Language: 大体包含以下几点

提供多种语言服务的网站往往是基于某种特定语言构建,再提供其他语言支持的,这样每种语言类型的内容在质量上层次不齐,而访问者可能会更倾向于内容质量更高的那一种语言,而内容协商机制无法替代用户的主观判断

实践中,对于切换网站语言的功能,切换方式往往更倾向于主动切换(比如提供一个切换的按钮)而非自动切换

浏览器在用户不提供语言相关配置的情况下,很难猜测用户的自然语言倾向(一般可能会根据地理定位、ip等因素猜测),打个比方,比如我会经常出差去日本,但这不代表我会说日语,同时虽然我挂了加拿大的 vps,但是提供中文内容的网站,我还是倾向于看中文

Accept: 与 Accept-Language 类似,同样因为内容的格式会因用户的主观意识而不同,还有诸多其他因素制约内容协商机制,所以最终失败了。

唯一有些用途的是 Accept-Encoding,但鉴于如今大部分现代浏览器都已支持多种压缩方式(常见的如 gzip、br),因此一定程度上已经不需要额外声明这个头部了,虽然大部分浏览器都会自动发送这个头部,但其实这会造成额外 23 字节的浪费。

Vary 头部

在理解(或者巩固)了内容协商的概念后,就可以介绍 Vary 这个头部了。直接引用 MDN 对于它的描述:

The Vary HTTP response header determines how to match future request headers to decide whether a cached response can be used rather than requesting a fresh one from the origin server.

Vary 是一个HTTP响应头部信息,它决定了对于未来的一个请求头,应该使用一个缓存作为响应还是向源服务器请求一个新的响应。

单纯靠文档对于 Vary 的描述来理解它其实是有些困难的,最起码我会有这种感觉。

这个头部的语法和其他的 HTTP 头部类似,如下:

Vary: , , ...

不同的头部之间使用逗号进行分割,同时可以指定 * 为它的值,这样等价于将资源视为唯一,并不进行缓存,但这并不是最佳实践,因此不建议这么做。

Vary 的工作原理

一句话概括它的工作原理就是,就是它表示某个响应因某个响应头部而不同。举个例子,比如 Vary: Accept 的意思即为,响应因请求资源格式头部而不同,那么通过相同 URI 访问的资源就可以根据这个头上知道其内容格式不同。

但我们已经知道,对于大部分内容协商机制中涉及的头部,已经被看作是失败的,那么 Vary 和这些头部搭配使用还有什么意义呢?话虽如此,但 Vary 还可以与 HTTP 中其他的头部来搭配使用,从而满足很多应用场景下的特殊需求,比如动态服务、防止缓存错乱等。

Vary 的应用场景

以下简单罗列一些常用的应用场景以及采坑指南。

Vary 与 动态服务

关于动态服务,最常见的莫过于 Vary: User-Agent。众所周知,UA 是一段特征字符串,通常包含区分客户端类型、操作系统、版本号等信息,随着移动 web 应用变得越流行,一个应用网站同时提供桌面和移动两种版本的应用是很常见的事情。通过设置 Vary: User-Agent 头部,对于搜索引擎,对于关键字的搜索结果可以提供更加准确的应用版本,对于客户端,可以使其从缓存服务器获取到相应应用类型的缓存版本,而不是错误地将桌面版缓存传递给移动版应用。

web 应用的性能在加载速度这一指标上,很大程度上取决于加载资源的大小,而图片资源是所占比例最大的一块。为了减少图片的大小,除了对常见的图片格式进行压缩以外,chrome 推出的 WebP 格式也是不错的选择。但是这里的问题是,不是所有的浏览器都支持 WebP 图片格式的,所以这里使用 Vary: Accept 来针对浏览器的支持情况返回相应的缓存副本,支持则返回 WebP 格式,不支持则返回缩略图或者原图。

还有其他关于动态服务的场景,比如要针对不同分辨率的屏幕加载不同质量的图片(Client Hints 相关的头部)、针对不同用户身份提供不同的资源(Cookie头部)等等。

Vary 与 缓存错乱

有时候我们会发现响应中存在 Vary: Accept-Encoding 头部信息,我原先按照内容协商机制中所描述的内容来理解,但到后来才发现,其实很大程度上是为了防止缓存错乱的问题。

设想一下,如果没有这个头部,当两个分别支持 gzip 和 不支持 gzip 的客户端对同一份资源进行获取时,结果会变得十分微妙。如果不支持 gzip 的客户端先访问,缓存代理会缓存未压缩的版本,那么当支持 gzip 的客户端再访问时,由于命中缓存,虽然它支持 gzip 但也只能加载未压缩的资源。反过来同样如此,支持 gzip 客户端先访问,则缓存代理会缓存压缩版本,当不支持 gzip 的客户端再访问时,缓存同样命中,但是由于它无法对压缩资源解码,所以会呈现乱码。

通过 Vary: Accept-Encoding 我们可以防止这种情况的发生,因为 Vary 在这里其实是扮演着校验器的角色,它会进一步对命中缓存的资源进行再校验,如果发现头部信息不同,则会将缓存资源视为无效,从而将请求继续转发至源服务器。这对于缓存代理服务器也有一定的益处,因为可以有有依据地针对不同的 Accept-Encoding 缓存不同的资源副本。

Vary 与 缓存命中率

Vary 虽然可以防止缓存错乱,但并不代表可以滥用,盲目的使用会适得其反,比如之前提及的 Vary: *,这样等价于将每个请求视为唯一,并且不缓存其响应资源,除非有意为之,不然没有人会牺牲缓存带来的性能提升。

同时对于一些 Header 的值是开放性的,比如之前提及的 User-Agent,如果单纯从字面量来匹配的话,众多桌面浏览器的值会因各种因素而不同的,如果仅是简单地将 UA 作为区分桌面端和移动端的依据,那么缓存命中率会达到一个很低的水平。如何解决这个问题呢?可以将这些 UA 头部的值进行标准化,比如可以通过正则匹配所有桌面浏览器的 UA 并重新更改为 Desktop,之后再转发至缓存代理和源服务器,这样有利于提高缓存命中率,关于这部分的内容,可以参考这篇文章,其中有很细致的讲解。

所以我们要时刻留意,在使用 Vary 时,一定要根据缓存命中率作出调整,在不发生缓存错乱的情况之下,尽可能的提高资源的缓存命中率。

Vary 与 CORS

对于跨域的有情况,Vary 也包含一些内容。HTTP 协议规定,当服务端响应包含 Access-Control-Allow-Origin 头部,且它的值是一个具体的域名而不是通配符 *,那么这时必须要包含 Vary: Origin 这个头部。

为什么要包含这个头部,因为请求头中的 Origin 头部代表了该请求来源的具体域名信息,那么对于不同域名网站所发起的请求,会使用仅属于它本身的缓存。一般而言,我们很少会遇到这种问题,因为一般都将 Access-Control-Allow-Origin 设置为了 *,至少我自己是这样的。如果想进一步了解 Vary 和 CORS 的内容,可以参考这篇文章。

最后

差不多就这么多内容了,如有错误,还望指正。

参考链接

内容协商

Best Practices for Using the Vary Header

IE 与 Vary

CORS

Vary

Response with Vary Header

Understanding Vary Header

Getting the most out of Vary with Fastly

Why not conneg

条件型 CORS 响应下因缺失 Vary: Origin 导致的缓存错乱问题

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/62052.html

相关文章

  • 查漏补缺 - 收藏集 - 掘金

    摘要:酝酿许久之后,笔者准备接下来撰写前端面试题系列文章,内容涵盖浏览器框架分钟搞定常用基础知识前端掘金基础智商划重点在实际开发中,已经非常普及了。 这道题--致敬各位10年阿里的前端开发 - 掘金很巧合,我在认识了两位同是10年工作经验的阿里前端开发小伙伴,不但要向前辈学习,我有时候还会选择另一种方法逗逗他们,拿了网上一道经典面试题,可能我连去阿里面试的机会都没有,但是我感受到了一次面试1...

    YuboonaZhang 评论0 收藏0
  • HTML查漏补缺

    摘要:规定了标记语言的规则,这样浏览器才能正确地呈现内容不基于,所以不需要引用标签都用来做什么的提供有关页面的元信息,常用于定义页面的说明,关键字,最后修改日期,和其它的元数据。这些元数据将服务于浏览器如何布局或重载页面,搜索引擎和其它网络服务。 HTML规范 HTML规范文档 H4时代被规定为错误的行为,在H5时代全都被合理化了,比如标签不区分大小写、只有开始标签没有结束标签、属性值不...

    daydream 评论0 收藏0
  • 【面试篇】JS基础知识查漏补缺

    摘要:因为在页面加载完成后,引擎维护着两个队列,一个是按页面顺序加载的执行队列,还有一个空闲队列,使用定时函数就是将回调函数加入到空闲队列中,故和其他定时器是并发执行的。 1.window.onload和$(document).ready()的区别: ①执行时间:window.onload会在所有元素,包括图片,引用文件加载完成之后执行,而$(document).ready()则会在HTML...

    myeveryheart 评论0 收藏0
  • 前端面试题-浏览器/服务端/网络

    摘要:同源策略是什么跨域通信同源两个文档同源需满足协议相同域名相同端口相同跨域通信进行操作通信时如果目标与当前窗口不满足同源条件,浏览器为了安全会阻止跨域操作。 同源策略是什么? javascript跨域通信 同源:两个文档同源需满足 协议相同 域名相同 端口相同 跨域通信:js进行DOM操作、通信时如果目标与当前窗口不满足同源条件,浏览器为了安全会阻止跨域操作。跨域通信通常有以下方法 ...

    jsdt 评论0 收藏0

发表评论

0条评论

xiangchaobin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<