整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

好文翻译|给 Web 开发者与管理员的缓存指南

好文翻译|给 Web 开发者与管理员的缓存指南

击右上方,关注开源中国OSC头条号,获取最新技术资讯

参与翻译 (3人) : Tocy, 边城, 李Sir迷路了

这是一个信息文档。虽然本质上来说本文在讲技术,但它尝试将复杂的概念讲简单而又实用。为了更容易理解,本文会简化甚至省略某些方面的材料。如果你想深入了解这些主题,请阅读最后的参考资料。

Web 缓存是什么?为什么要使用缓存?

Web 缓存处于服务器(也称为源服务器)和客户端之间,监视请求并保存响应的副本,比如 HTML 页面,图片和文件等(统称为表述)。如果之后有对同一个 URL 的新请求,它会使用自己保存的内容来响应,而不是再次请求源服务器来获取内容。

使用 Web 缓存主要有下面两个原因:

  • 减少延迟 —— 因为响应请求的内容来自缓存(距客户端较近)而不是源服务器,它会花较少的时间来获得表述并将他们呈现出来。这使得 Web 看起来具有良好的响应速度。
  • 减少网络传输 —— 由于复用了表述,它可以减少客户端使用的带宽总量。如果客户需要为流量付费,这就意味着省钱。缓存会降低对带宽的要求,也降低处理难度。

Web 缓存的种类

浏览器缓存

你在查看现代 Web 浏览器(比如 IE、Safari 或 Mazilla)选项的时候,可能会看到“缓存”设置。这个选项让你配置一部分硬盘空间来保存你看过的表述。浏览器缓存的规则相当简单。它通常会在一次会话(即当前浏览器中第一次调用)中检查表述是否最新。

这个缓存在用户使用“回退”按钮或者点击一个浏览过的链接时会特别有用。而且,如果你在网站的各个页面中浏览相同的图片,他们几乎能马上从缓存中加载出来。

代理缓存

Web 代理缓存的工作原理相同,但规模更大。代理以同样的方式为成百上千的用户服务;大公司和 ISP 常常把代码缓存建立在防火墙之上,也可能是以独立设备的形式存在(也称为中间设备)。

代理缓存即不是客户端的一部分,也不是服务器的一部分,而是在网络之外,必须以某种方式把请求路由过去。其中一种方式是手工修改浏览器代理设备,指定要使用的代码;另一种方式是拦截。拦截式代理会根据其自身的基础网络重定向 Web 请求,不需要在客户端配置,客户端甚至不知道它们的存在。

代理缓存是一种共享缓存,通常不只是一个用户,而是大量用户在使用代理缓存。正因为如此,他们特别擅长降低延迟和网络传输量。这是因为众人都需要的表述会被多次重复使用。

网关缓存

网关缓存又名“反向代理缓存”或“替代缓存”。网关缓存也是一种中介,它他们不是由网络管理员部署以节约带宽,而是由网站管理员自己部署,使其站点更具伸缩性、可靠性以及拥有更好的性能。

很多方法都可以把请求路由到网关缓存,但常见的方法是使用负载均衡器让他们对于客户来说,看起来就跟源服务器一样。

内容分发网络(CDN)在整个 Internet(或它的一部分)中分发网关缓存,并将其出售给对此感兴趣的网站。Speedera 和 Akamai 都是 CDN。

本教程主要关注浏览器和代理缓存,不过其中一些信息也适用于对网关缓存有兴趣的人。

Web缓存对我有坏处么?我为什么要帮助它们?

Web缓存是互联网中误解最深的技术之一。因为代理缓存可以隐藏使用网站的用户,所以网站管理员特别害怕失去对他们的站点的控制,这会使得他们很难去知道是谁在使用他们的站点。

然而不幸的是,即使没有Web缓存,网络上也有非常多的因素可以保证管理员精确的知道一个用户如何使用他们的站点。如果这是你非常关注的问题的话,这篇手册将会指导你如何在站点没有不友好的缓存机制的情况下获取你需要的统计信息。

另一个问题是,缓存可以提供已经失去时效性或者无效的数据给请求方。这篇手册将会演示如何配置你的服务端的服务来控制数据内容的缓存方式。

另一方面,如果你把站点规划的非常好,那么缓存将会使得站点的加载速度更快,同时也会减轻服务端和网络线路的负担。这两者之间的区别是非常显而易见的:一个没有使用缓存的站点可能需要数秒时间来完成页面的加载和展示,而另一个借助了缓存机制的网站可能在瞬间就完成了页面加载和展示。而用户会更喜欢加载速度迅速的站点,同时也会更加频繁的访问这些加载迅速的站点。

内容传递网络 (CDNs) 是一个非常有意思的发展产物,和通常的代理缓存不同的是,CDNs 的网关缓存和被缓存站点的关注点是一致的,所以上述的问题都得到了处理和解决。然而,即便你使用了 CDNs ,你仍然要知道在下游还是会存在代理和浏览器缓存的。

我们可以这样想一下:许多大型的互联网公司为了让他们的用户可以在使用其提供的服务时得到最快速的响应,会斥资数百万美元在世界范围内建立许多服务器机群来复制存储他们的数据内容。其实,缓存也能做这些事情,而且,相对来说缓存离最终的使用用户会更近。最好的一点是,你根本不用为此支付任何费用。

而事实是,不管你愿意与否,代理和浏览器缓存都会被使用在某个环节中。如果你没有正确的配置站点的缓存相关配置,站点数据将会按照默认的缓存管理员的配置被缓存下来。

Web缓存是如何工作的?

所有的缓存都有一系列用来决定什么时候从缓存中提供内容的规则。如果可能的话,其中的一些规则被放置在了协议中(HTTP 1.0和1.1),而另一些则由缓存的管理员(诸如浏览器缓存的用户,或者代理管理员)来设置。

通常情况下,下面列出的这些规则是最常用到的规则集(不用担心你不了解规则的详细嘻嘻,之后会详细地对这些规则作出解释):

  1. 如果响应的头部通知缓存不要保存当前响应内容,那么缓存就不会缓存当前响应。
  2. 如果是一个授权的或者加密的请求(例如HTTPS),那么共享缓存将不会保存相关数据内容。
  3. 在下述场景中,我们认为被缓存的内容是最新的(意味着不需要源服务端的检查就可以被发送给客户端),故而数据内容会直接从缓存中提供且不需要源服务端的校验:
  • 缓存内容由过期时间或者其他的生存期控制机制,且缓存内容仍在生存有效期内;
  • 如果缓存服务近期对外提供了数据内容,且该内容在很久之前就被修改了。
  1. 如果内容已经过时了,源服务端会要求对其进行验证,或者通知缓存服务这份缓存的内容是否仍然有效。
  2. 在类似于网络中断这样的场景中,缓存可以对外提供过时的响应数据而不必和源服务器进行校验和确认。

如果在响应中没有相应的验证器( ETag 或者 Last-Modified 头部),且也没有明确的刷新信息,则这种数据通常但不总是的会被视为不可缓存的数据。

综合来看,刷新和验证是缓存可以正常有效的保存内容的最重要途径。新的数据内容可以可靠的快速的从缓存中得到,与此同时一个经过验证的表述则避免了在没有发生变更的情况下被再次完整的发送出去。

如何(以及如何不)控制缓存

有一些工具可以让网站设计人员和网站管理员来调整缓存对其站点的操作,这可能会使得服务器的配置发生变更,但是这些变更都是有价值的。对于如何在服务器上使用这些工具,在实现这一章节会详细阐述。

HTML Meta 标签和 HTTP 头信息

HTML 作者可以在文档的 <HEAD> 段中放置标签来描述一些属性。其中 meta 标签常用于确保文档不被缓存,或者在一定时间后过期。

meta 标签很容易使用,但效果不怎么样。这是因为只有部分浏览器缓存会遵从约定,代理缓存却不会(代理基本上不会去分析文档中的 HTML)。我们可以在 Web 页面中放置 Pragma: no-cache 这样的 meta 标签,但不要指望他一定会保持刷新。

另一方面,真正的 HTTP 头能让你很好的控制浏览器缓存和代理缓存对表述内容的处理。HTTP 头在 HTML 中看不到,它们通常由 Web 服务自动生成。不过你可以在一定程度上控制他们,这取决于你用的是什么服务器。你会在下面的部分看到 HTTP 头是多么有趣,还会了解到该如何把他们应用到网站上。

如果你的站点托管在 ISP 或者专门的托管服务商那里,他们不允许你自己设置到头重要的 HTTP 头(比如 Expires 和 Cache-Control),那就勇敢地提出抗议,因为你的工作需要这些功能。

HTTP 头在服务器发送 HTML 这前发送给浏览器,只有浏览器和中间的缓存能够看到。典型的 HTTP 1.1 响应头像下面这样:

HTTP/1.1 200 OK
Date: Fri, 30 Oct 1998 13:19:41 GMT
Server: Apache/1.3.3 (Unix)
Cache-Control: max-age=3600, must-revalidate
Expires: Fri, 30 Oct 1998 14:19:41 GMT
Last-Modified: Mon, 29 Jun 1998 02:28:12 GMT
ETag: "3e86-410-3596fbbc"
Content-Length: 1040
Content-Type: text/html

HTML 会在紧跟在这些头信息之后,他们之间用一个空行隔开。在实现部分你可以了解到设置 HTTP 头相关的信息。

Pragma HTTP 头(以及为什么它没用)

很多人认为指定了 Pragma: no-cache HTTP 头可以避免表述被缓存。这并不一定是真的。HTTP 规范中没有任何关于 Pragma 响应头的规定,不过 Pragma 请求头(就是浏览器发送给服务器的头信息)却正在商讨中。虽然有一小部分缓存会遵从这个头信息,但大多数不会。你应该使用下面这些头信息代替它。

使用 Expires HTTP 头控制新近程度

Expires HTTP 头是控制缓存的基础方法,它告诉所有缓存与之相关的表述存在多久的保鲜期。那保鲜期之后,缓存应该检查源服务器,看文档是否被改变。几乎各种缓存都支持 Expires 头。

多数服务器允许你通过多种方法来设置 Expires 响应头。一般来说,他们可以设置绝对的过期时间,根据上次客户端取回表述时(最近访问时间)计算的时间,或者根据上次服务器文档修改时间计算的时间(最近修改时间)。

Expires 头特别适合缓存静态图像(比如导航栏和按钮),因为他们不会经常变化,你可以为他们设置一个非常长的过期时间,使你的站点具有更优势的响应性能。对于一些更新比较规律遥页面来说,他们也很有用。举例来说,如果你每天早晨 6:00 更新新闻页面,那就可以把表述内容设置在那个时间过期,然后缓存会知道什么时候该去获取更新的内容,而不需要用户去点击“刷新”。

Expires 头支持 HTTP 日期值,任何其它值都会被认为“过去时”,结果表述不会被缓存。还要注意,HTTP 日期是格林威治(GMT)时间,而不是本地时间。

比如:

Expires: Fri, 30 Oct 1998 14:19:41 GMT

虽然 Expires 头很有用,但它也有一些局限。首先日期就是个麻烦事,Web 服务器和缓存上的时钟就必须同步。如果他们对时间的看法不一致就达不到预期的效果,因为缓存有可能认为已经过期的内容还在有效期内。

如果使用Expires 头就必须保证服务器时钟的准确性,这非常重要。要达到这个条件,有一个办法是使用网络时间协议(NTP),请向你身边的系统管理员了解关于 NTP 的知识。

使用 Expires 的另一个问题是容易忘记为某些内容设置了特定的过期时间。如果你没有在那之前更新 Expires,那么每个请求最终都会到服务器上去获取内容,既增加延迟,也会增加负载。

Cache-Control HTTP 头

HTTP 1.1 引入了一类新的头信, Cache-Control 响应头,让 Web 可以更方便地控制内容,避免 Expires 所具有的限制。

Cache-Control 响应头有如下一些:

  • max-age=[秒] — 指定表述内容的最大有效期。跟 Expires 类似,这个指令的时间是相对于请求时间,而不是绝对时间。[秒] 是从请求开始你期望表述过期前保持有效的总秒数。
  • s-maxage=[秒] — 和 max-age 相似,但它只对共享(例如代理)缓存有效。
  • public — 把通过认证的响应标记为可缓存。一般情况下,如果需要 HTTP 认证,响应会自动标记为私有的。
  • private — 允许用户(比如一个浏览器)缓存响应,不允许共享缓存(比如代理)进行缓存。
  • no-cache — 强制缓存将请求提交到原服务器进行验证后释放缓存副本,一次不落。这可以确保谨慎地对待认证(结合 public 标记),严谨地更新,同时又不牺牲缓存所带来的各种好处。
  • no-store — 指示缓存在任何情况下都不保留表述的副本。
  • must-revalidate — 告诉缓存,他们必须遵守你给他们关于内容更新的每一项信息。HTTP 允许缓存服务在一些特殊情况下认为表述过期,你可以通过指定这个头参数告诉缓存你希望它严格遵守你的规则。
  • proxy-revalidate — 与 must-revalidate 相似,但它只对代理缓存有效。

举例说明:

Cache-Control: max-age=3600, must-revalidate

当 Cache-Control 和 Expires 都存在时,Cache-Control 优先。如果你准备使用 Cache-Control 头,你应该查看 HTTP 1.1 中的优秀文档; 参见"引文及更多信息"一节。

校验器和校验

在 Web 缓存的工作原理一文中,我们说过在表示层发生变化时服务器和缓存使用校验进行通信。通过使用它,缓存可以避免在本地已有副本时下载整个表示层,但他们不确定它是否仍然是最新的。

校验器是非常重要的; 如果它不存在,并且没有任何新的信息( Expires 或 Cache-Control )可用,则缓存将根本不存储表示层。

最通用的校验器是头部的 Last-Modified,用来标识文档最近一次修改的时间。如果缓存存储了一个带有Last-Modified 头部的表示层,缓存可以借助一个 If-Modified-Since 请求向服务端确认当前缓存的表示层在最近一次修改后是否发生了变更。

HTTP1.1引入了一个新的叫做 ETag 的校验器。ETags是一个由服务端生成的唯一标识符,并且每当表示层发生变更时ETags的值都会发生变化。由于ETag是由服务端生成的,所以当缓存通过 If-None-Match 请求得知ETag在服务端匹配成功时,便可以确认缓存存储的表示层和服务端的内容是一致的,没有发生任何变化。

几乎所有的缓存都使用了最近一次修改时间来作为校验器,同时ETag校验器的使用也在逐步增长。

大多数的现代网站服务器会自动地为静态内容(例如文件)同时生成 ETag 和 Last-Modified 这两个校验器,这个过程不需要任何人为参与。然而,服务器在为诸如CGI、ASP或者数据库站点这样的动态内容生成ETag 和 Last-Modified 校验器时就显得力不从心了,具体可浏览编写缓存可感知的脚本。

关于构建一个缓存感知的站点的忠告

除了使用新鲜度信息和校验,还有一些其他的处理可以使得你的站点更利于缓存。

  • 始终使用URL - 这一条是缓存的黄金原则。如果向不同的页面、不同的用户提供同样的数据内容,或者同样的内容来自于不同的站点,这时应该使用一致的URL。这是最简单、最有效的让站点更利于缓存的方法。例如,如果一旦在HTML代码段中使用"/index.html"作为一个资源引用,那么就一直按照这种方式坚持下去。
  • 使用一个包含图片和其他元素的公共库,并在不同的地方引用他们。
  • 使用缓存来存储图片和很少变更的页面,这个的实现可以借助一个设置了很大的值的 Cache-Control: max-age 头部信息。
  • 通过一个精确的最大存活时间或者过期时间让缓存识别出更新频繁的页面。
  • 如果资源(特别是可下载的文件)发生了变更,改变其名字。通过这种方式,可以让资源在未来的某个时间过期,同时也能保证当前版本仍然是有效的。唯一需要设置一个短的过期时间的部分就是链接到这些资源的页面。
  • 在必要的情况下再修改文件。如果你这么做了,那么每个文件都会有一个不真实的距离当前时间更近的 Last-Modified 值。举个例子,当准备更新站点时,不要复制整个站点文件进行更新,仅选择那些确实修改了的文件去执行更新操作。
  • 只在有需要的情况下使用cookie。cookies很难被缓存存储起来,并且在大多数场景都是没有必要的。如果必须用到cookie的话,那么也只在动态页面中使用cookie。
  • 用REDbot检查页面文件。这个工具可以协助站点开发管理人员应用上文中讨论过的诸多原则。

编写缓存可感知的脚本

默认情况下,大多数脚本不会返回校验器(一个 Last-Modified 或 ETag 响应头)或新鲜度信息( Expires 或 Cache-Control )。虽然一些脚本确实是动态的(意味着它们为每个请求返回不同的响应),但许多(如搜索引擎和数据库驱动的网站)可以从此类缓存友好中受益。

一般来说,如果脚本生成的输出在以后的某个时间(无论是几分钟还是几天后)都可以使用相同的请求重现,那么它应该是可缓存的。如果脚本的内容仅根据 URL 中的内容而变动,则它是可缓存的; 如果其输出取决于cookie、身份认证信息或其他外部标准,则它可能不是可缓存的。

  • 让脚本对缓存友好(同时有更好的表现)的最佳方式是只要脚本发生变化,就将其内容转储到一个普通文件中。Web 服务器会把这个文件跟其它 Web 页面同等对待,为其生成验证器,让一切变得简单。记住,只写内容变动过的文件,避免刷新新没有内容变动文件的 Last-Modified 时间。
  • 还有一种方法可以让脚本在一定的限制条件下被缓存,即设置一个跟寿命相关的头。虽然用 Expires 可以做到,但用 Cache-Control: max-age 可能更简单,它会按一定时间间隔刷新请求。
  • 如果这些办法都不适合你,那你需要用脚本生成一个验证器,然后响应 If-Modified-Since 或 If-None-Match 请求。这个操作可以通过解析 HTTP 头之后,适当的响应 304 Not Modified 来实现。不过操作起来似乎不简单。

其它技巧:

  • 不使用 POST,除非确有必要。多数缓存不会保存 POST 响应。如果你通过路径或查询(通过 GET)发送信息,缓存会保存这些信息以备将来使用。
  • 不要在 URL 中嵌入用户特定的信息,除非生成的内容对每个用户都不同。
  • 不要以为所有用户请求都来自同一台主机,因为也有可能来自缓存。
  • 生成 Content-Length 响应头。这很容易做到,而且它会允许通过长连接来响应脚本。这样一来,客户端可以在一个 TCP/IP 连接上请求多个表述,而不是为每个请求建立连接。这样你的网站看起来会更快。

请阅读实现说明来了解更具体的信息。

FAQs (常见问题)

实现可缓存最重要的事情是什么?

一个好的策略是识别最流行、最大的表示层(尤其是图像)并首先在它们上使用。

我如何使用缓存尽可能快地加载页面?

最可缓存的表示层是具有较长新鲜时间集的那一部分。校验确实有助于减少查看表示层所花费的时间,但缓存仍然必须联系原服务器以查看它是否是最新的。 如果缓存已经知道它是最新的,它将被直接提供。

我知道缓存很棒,但我需要统计有多少人访问了我的页面!

如果在每次访问页面时你都必须知悉,在页面(或页面本身)上选择 ONE 小项,并通过为其提供适当的头信息使其不可缓存。例如,你参考每个页面上的 1x1 透明的不可缓存图像的逻辑。Referer 头将包含有关哪个页面调用它的信息。

请注意,即使这样也无法提供有关你用户的真实准确的统计信息,并且对互联网和你的用户是不友好的; 它会产生不必要的流量,并强迫人们等待下载完成未缓存项。有关此内容的更多信息,请参阅引文中的对访问统计信息的解读。

如何获取一个表示层的HTTP头部信息?

许多浏览器会通过一个“page info”或者一个小型的接口等方式向使用者提供 Expires 和 Last-Modified 的头部信息。如果可以的话,你会得到一个页面的菜单和关联在这个页面上的任何表示层(比如图像),以及这些表示层的详细信息。

如果想要看到表示层的所有HTTP头部信息,可能需要使用Telnet客户端手动连接到Web服务器上。

要想通过Telnet连接到服务器上,需要把端口号(默认80)作为一个独立的字段输入到连接到请求中,需要以如下的形式连接到服务器上: www.example.com:80 或者 www.example.com 80(以空格分隔) 。具体格式可以参考你使用的客户端文档。

一旦连接成功建立,就可以对任意表示层发起请求。例如,如果你想要获得 http://www.example.com/foo.html的所有头部信息,连接到 www.example.com,端口号是 80,并且输入:

GET /foo.html HTTP/1.1 [return]
Host: www.example.com [return][return]

每次当你看到 [return] 提示时,就按下回车键,确保在结束之前要按够两次回车。这个操作就会打印出对应的头部信息,紧随其后的是整个完成的表示层信息。如果只是想头部信息的话,用 HEAD 代替 GET 即可。

我的页面是受密码保护的,代理缓存怎么处理这种情况?

默认情况下,受HTTP身份认证保护的页面被视为是私有的,这些页面不会被共享缓存保存下来。然而,可以通过缓存控制(Cache-Control): public 头部使受身份认证保护的页面变成共有资源,适用于HTTP1.1的缓存就会将这些页面缓存下来。

如果你想让这些页面既能被缓存存储的同时又能继续保持其对每个用户的身份认证特性,就需要将 Cache-Control: public 和 no-cache 结合起来使用。这两个头部会要求缓存取出表示层数据返回给请求之前首先将用户的身份信息提交给服务器进行认证。命令格式如下:

Cache-Control: public, no-cache

不管上面的处理执行与否,最好的方式是最小化认证保护的使用范围。例如,如果图片数据是非敏感数据,那么将这些图片放在一个独立的文件夹目录下,然后在服务端配置不对这个文件夹目录做身份认证保护。这样,这些图片就可以被缓存存储起来了。

如果人们通过缓存访问我的站点,我需要担心安全性吗?

https:// 页面是不会被代理缓存缓存(或解密)的,因此你不必担心这一点。然而,因为缓存存储了从获取的 http:// 响应和 URL ,所以你应该对不安全的站点小心一些; 一个不道德的管理员可以随意地收集有关其用户的信息,尤其是在 URL 中。

实际上,在服务器和客户端之间的网络上的任何管理员都可以收集此类信息。一个特别的问题是 CGI 脚本将用户名和密码放在 URL 本身中; 这使得其他人找到并使用这些登录信息变得很简单。

如果你知道常见的 Web 安全性所存在的问题,那么代理缓存不应该有任何例外。

我正在寻找一个集成 Web 发布解决方案。哪一个是缓存可感知的?

这要看具体情况。一般来说,解决方案越复杂,缓存起来就越困难。最糟糕的是动态生成所有内容并不提供校验器的解决方案; 它们可能根本是不可缓存的。请与供应商的技术人员联系以获取更多信息,并参阅下面的实现说明。

我的图片在一个月以后才失效,但是我需要现在就在缓存中更新它们!

过期(Expires)头部是没有办法绕过去的,缓存内容会一直被使用直到缓存(不管是浏览器缓存还是代理缓存)耗尽了存储空间并且必须删除掉所有的表示层。

最有效的解决方法是改变任何指向它们的链接,这样,全新的表示层会从源服务端加载到客户端。记住,任何引用了这些表示层的页面也会被缓存保存下来。鉴于此,最好将静态图片和相似的表示层全都缓存下来,同时严格把控引用这些资源的HTML页面。

如果想要从某个指定的缓存中重新加载一个表示层,你既可以在使用缓存的时候强制加载(在Firefox中,按下shift的同时按下‘reload’将发出一个 Pragma: no-cache请求头部来完成这个操作),也可以让缓存管理员通过他们的接口删除掉这个表示层。

如何让用户在我负责维护的网站托管服务上发布缓存友好的页面?

如果你使用的的Apache,考虑下容许他们使用 .htaccess文件并且提供对应的文档。

否则,你可以建立一个事先决定好的区域,这个区域用来对应每个虚拟服务上的各种缓存属性。例如,可以指定一个 /cache-1m 目录来维护需要在访问后缓存一个月的缓存内容的相关信息,一个 /no-cache 目录通过使用头部信息标识那些不需要缓存存储来自其自身的表示层。

不管你能做什么,最好首先在缓存方面和你的最大的客户一起合作。大部分的节省(带宽和服务器负载方面)会从高流量站点中产生。

页面是可缓存的,但是浏览器在每次请求时都会重新请求这些页面。怎么样可以强制缓存存储这些表示层?

缓存不需要保留或者重用一个表示层,它只需要在某些情况下不保留或者使用它们。基于表示层的文件大小、类型(图片或者HTML文件)、以及它们需要占用的存储空间大小,缓存会决定哪些表示层会被保存下来。相对于更受欢迎或者更大的表示层,缓存认为你的内容可能不值得被缓存存储起来。

一些缓存机制允许管理员决定哪些表示层可以优先被缓存,而另外一些缓存机制则允许表示层可以被固定在缓存中,这样它们就总是有效的表示层。

实施说明 — Web 服务器

一般来说,不管你想部署到哪个 Web 服务器,最好都选择它最新的版本。新版本可能会包含更易于处理缓存的特性,而且通常还会在非常重要的安全和性能方面所有改进。

Apache HTTP 服务器

Apache 使用可选的模块来包含 Expires 和 Cache-Control 等头参数。这两个模块都是从 1.2 开始加入的。

模块需要设置到 Apache 中。虽然模块被包含在发行包中,但默认情况下并未设置可用。想知道某个模块是否在服务器中可用,找到 https 二进制程序然后运行httpd -l。这个命令会打印出已经生效的模块列表(注意这只包含了随 Apache 编译的模块。在以后的 Apache 版本中,可以使用 httpd -M 把动态加载的模块也打印出来)。我们要找 expires_module 和 headers_module 这两个模块。

  • 如果他们还未生效,你在有管理员权限的情况下可以重新编译 Apache 以包含他们。只需要在配置文件中找到恰当的行,取消掉他们的注释就行,也可以使用 -enable-module=expires 和 -enable-module=headers 这两个参数来配置(1.3 或更高的版本)。欲知详情,请查阅 Apache 发行的 INSTALL 文件。

一旦你的 Apache 包含了正确的模块,就可以使用 mod_expires 来指定表述到期的时间,这个配置写在 .htaccess 文件或者服务器的 asccess.conf 文件中都行。你可以按访问时间或修改时间来指定过期时间,可以将其应用于某个文件类型,也可以用作默认配置。请阅读模块文档来了解更多信息,如果发现问题请找你附件的 Apache 专家帮忙。

要应用Cache-Control头,你需要使用mod_headers模块,该模块允许你为某资源指定任意HTTP头。 请参阅mod_headers文档。

这是一个示例.htaccess文件,展示了一些头的使用。

  • .htaccess文件允许Web发布者使用通常仅在配置文件中可找到的命令。它们会影响它们所在目录及其子目录的内容。与服务器管理员沟通下,了解它们是否已被启用。
### activate mod_expires
ExpiresActive On
### Expire .gif's 1 month from when they're accessed
ExpiresByType image/gif A2592000
### Expire everything else 1 day from when it's last modified
### (this uses the Alternative syntax)
ExpiresDefault "modification plus 1 day"
### Apply a Cache-Control header to index.html
<Files index.html>
Header append Cache-Control "public, must-revalidate"
</Files>
  • 注意mod_expires会在合适时机自动计算并插入一个Cache-Control:max-age头。

Apache 2的配置和1.3版本的非常类似;更多信息请参考2.2 mod_expires和mod_headers文档。

微软的IIS

Microsoft的Internet Information Server以一种灵活的方式使得设置头信息变得非常容易。请注意,这仅适用于服务器的第4版,该版本仅在NT Server上运行。

要指定站点区域的头信息,请在“Administration Tools”界面中选择它,然后调出其属性。在选择HTTP Headers选项卡之后,你应该看到两个有趣的区域; Enable Content Expiration和Custom HTTP headers。第一个区域应该是不言自明的,第二个可以用于适用于Cache-Control头。

有关在Active Server Pages中设置头的资料,请参阅下面的ASP部分。也可以从ISAPI模块设置头; 有关详细信息,请参考MSDN。

Netscape/iPlanet企业服务器

从版本3.6开始,企业服务器不再提供任何显式的方法来设置Expires头。但是,它从3.0开始就支持HTTP 1.1功能。这意味着HTTP 1.1缓存(代理和浏览器)将能够利用你所做的缓存控制配置。

要使用Cache-Control头,请选择管理服务器中的Content Management | Cache Control Directives。 然后,使用资源选择器,选择要设置头的目录。设置完头信息后,单击“确定”。更多信息,请参阅NES手册。

实现附注— 服务器端脚本

由于服务器端脚本的重点在于动态内容,因此即使是对可缓存的内容,也不会生成可缓存的页面。如果你的内容经常变动,但不是每次点击都会变动,请考虑设置Cache-Control:max-age头; 大多数用户在相对较短的时间内再次访问该页面。例如,当用户点击“后退”按钮时,如果没有任何校验器或新鲜度信息可用,则他们必须等到从服务器重新下载页面之后才能看到它。

要记住的一件事是,使用Web服务器时设置HTTP比在脚本语言中设置HTTP标头更容易。都尝试下。

CGI

CGI脚本是最常用的生成内容的方式之一。你可以通过在发送正文之前添加HTTP响应头轻松得附加到HTTP响应头上;大多数CGI实现已经要求你为Content-Type头执行此操作。例如,在Perl中;

#!/usr/bin/perl
print "Content-type: text/html\n";
print "Expires: Thu, 29 Oct 1998 17:04:19 GMT\n";
print "\n";
### the content body follows...

既然它全是文本,你可以简单地使用内置函数生成Expires以及其他日期相关的头信息。如果你使用Cache-Control: max-age将会更简单;

print "Cache-Control: max-age=600\n";

这将使此脚本在请求后可缓存10分钟,因此如果用户点击“后退”按钮,他们将不会重新提交请求。

CGI规范还使得在脚本环境中客户端所发送的请求头可用; 每个标头的名称前面都有“HTTP_”。因此,如果客户端发出If-Modified-Since请求,它将显示为HTTP_IF_MODIFIED_SINCE。

Server Side Includes

SSI(通常与扩展名为.shtml的一起使用)是Web发布者能够将动态内容添加到页面中的首选方式之一。通过在页面中使用特殊标记,可以使用有限形式的HTML内脚本。

大多数SSI实现都没有设置校验器,因此SSI是不能缓存。 但Apache的实现允许用户通过在适当的文件上设置组执行权限并结合XbitHack full指令来指定可以缓存哪些SSI文件。更多相关信息,请参阅mod_include文档。

PHP

PHP是一种服务器端脚本语言,当内置到服务器中时,可用于在页面的HTML中嵌入脚本,就像SSI一样,但具有更多的可选项。 PHP可以在任意Web服务器(Unix或Windows)上用作CGI脚本,也可以作为Apache模块。

默认情况下,PHP处理的表示层不会分配验证器,因此是不可缓存。但是,开发人员可以使用Header()函数设置HTTP头。

例如,下列代码将创建一个Cache-Control头和一个三天之后过期的Expires头:

<?php
 Header("Cache-Control: must-revalidate");
 $offset=60 * 60 * 24 * 3;
 $ExpStr="Expires: " . gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
 Header($ExpStr);
?>

注意 Header() 函数必须置于所有输出前。

如你所见,你必须手动为Expires头创建HTTP日期; PHP没有提供为你完成此操作的函数(尽管最新版本使其更容易;请参阅PHP的date文档)。当然,设置Cache-Control: max-age头是很容易,这对大多数情况来说已经不错了。

有关更多信息,请参阅header的手册条目。

Cold Fusion

由Macromedia发布的Cold Fusion是一个商业版服务器端脚本引擎,支持在Windows、Linux 以及Unix变体上的多种web服务器。

Cold Fusion 使用CFHEADER标签使得设置任意HTTP头相对简单。遗憾的是,他们设置Expires 头的示例,如下所示,有一些误导。

<CFHEADER NAME="Expires" VALUE="#Now()#">

它不像你想象的那样工作,因为其中的时间值(在这种情况下,发出请求的时间)不会转换为HTTP有效的日期; 相反,它仅作为Cold Fusion的日期/时间对象的表示打印出来。大多数客户端将忽略这样的值,或将其转换为默认值,如1970年1月1日。

然而,Cold Fusion确实提供了可以完成日期格式化的函数;GetHttpTimeString。与DateAdd配合使用,可以轻松设置过期失效日期; 在此,我们设置一个头信息,来声明此页面的表示在一个月后失效;

<cfheader name="Expires" 
 value="#GetHttpTimeString(DateAdd('m', 1, Now()))#">

你也可使用CFHEADER 标签来设置Cache-Control: max-age 以及其他头信息。

请记住,Web服务器的头信息是通过Cold Fusion的某些部署进行传递的(例如CGI); 通过在服务器上而不是在Cold Fusion中设置头信息,验证下你是否可以使用此功能。

ASP和ASP.NET

Active Server Pages,内置于IIS中,在其他Web服务中也可用,也允许你设置HTTP头。例如,要设置失效时间,可以使用Response对象的属性;

<% Response.Expires=1440 %>

从ASP设置HTTP头时,请确保在任意HTML生成之前放置了对Response方法的调用,或者使用Response.Buffer来缓冲输出。另请注意,默认情况下,某些版本的IIS在ASP上设置了一个Cache-Control: private的头,并且必须声明为公开,才能通过共享缓存进行缓存。

指定从请求到表示层超时的分钟数。可以像这样添加到 Cache-Control 头中:

<% Response.CacheControl="public" %>

在 ASP.NET 中,Response.Expires 已不推荐使用了;设置缓存相关头信息的正确方法是使用 Response.Cache ;

Response.Cache.SetExpires ( DateTime.Now.AddMinutes ( 60 ) ) ;
Response.Cache.SetCacheability ( HttpCacheability.Public ) ;

引用及更多

HTTP 1.1规范

HTTP 1.1 规范中有很多关于实现网页可缓存的扩展,同时它也是实现此协议权威指南。请查看 13, 14.9, 14.21 以及 14.25 部分内容。

Web-Caching.com

对缓存概念的一篇很棒的介绍,包含其他在线资源的链接。

关于访问信息的解读

Jeff Goldberg 对你不应该依赖访问统计和点击计数器的原因提供了丰富的信息。

REDbot

检查 HTTP 资源以确定它们与 Web 缓存的交互方式,以及常规情况下它们对协议协议的使用是否得当。

开源社区OSC「好文翻译」栏目,旨在每天为用户推荐并翻译优质的外网文章。再也不用怕因为英语不过关,被挡在许多技术文章的门外。关注开源社区OSC,每日获取翻译好文推荐,点击“了解更多”,阅读原文章。

TML基础


  1. HTML基本知识与结构
  2. HTML常见标签
  3. 标签写法与嵌套的讨论

HTML、CSS、javascript三者的关系


  1. HTML是网页内容的载体。内容就是网页制作者放在页面上想要让用户浏览的信息,可以包含文字、图片、视频等。
  2. CSS样式是表现。就像网页的外衣。比如,标题字体、颜色变化,或为标题加入背景图片、边框等。所有这些用来改变内容外观的东西称之为表现。
  3. JavaScript是用来实现网页上的特效效果。如:鼠标滑过弹出下拉菜单。或鼠标滑过表格的背景颜色改变。还有焦点新闻(新闻图片)的轮换。可以这么理解,有动画的,有交互的一般都是用JavaScript来实现的。

<!DOCTYPE HTML>

指示 web 浏览器关于页面使用哪个 HTML 版本进行编写,必须写在所有代码的第一行!

如果你的页面添加了<!DOCTYPE html>,那么就等同于开启了标准模式,那么浏览器就得老老实实的按照W3C的标准解析渲染页面,这样一来,你的页面在所有的浏览器里显示的就都是一个样子了。

这个属性会被浏览器识别并使用,但是如果你的页面没有DOCTYPE的声明,浏览器按照自己的方式解析渲染页面,那么,在不同的浏览器就会显示不同的样式。

这就是<!DOCTYPE html>的作用。

固定结构


结构如下:

<html>
 <head>...</head>
 <body>...</body>
</html>

代码讲解:

  • <html></html>称为根标签,所有的网页标签都在<html></html>中。
  • <head> 标签用于定义文档的头部,它是所有头部元素的容器。头部元素有<title>、<script>、 <style>、<link>、 <meta>等标签,头部标签在下一小节中会有详细介绍。
  • 在<body>和</body>标签之间的内容是网页的主要内容,如<h1>、<p>、<a>、<img>等网页内容标签,在这里的标签中的内容会在浏览器中显示出来。
  • 为 html 文档加上使用的语言类型说明

在很多国际化的网站中会使用到!<html lang="zh-CN"> </html>告诉浏览器等设备,语言使用以中文为显示和阅读基础!英文使用 en

head标签


下面我们来了解一下<head>标签的作用。文档的头部描述了文档的各种属性和信息,包括文档的标题等。绝大多数文档头部包含的数据都不会真正作为内容显示给读者。

下面这些标签可用在 head 部分:

<head>
 <title>...</title>
 <meta>
 <link>
 <style>...</style>
 <script>...</script>
</head>

<title>标签:

  • 在<title>和</title>标签之间的文字内容是网页的标题信息,它会出现在浏览器的标题栏中。网页的title标签用于告诉用户和搜索引擎这个网页的主要内容是什么,搜索引擎可以通过网页标题,迅速的判断出网页的主题。每个网页的内容都是不同的,每个网页都应该有一个独一无二的title。

meta标签

  • meta是html中的元标签,其中包含了对应html的相关信息,客户端浏览器或服务器端的程序会根据这些信息进行处理。
  • HTTP-EQUIV类似于HTTP的头部协议,它回应给浏览器一些有用的信息,以帮助正确和精确地显示网页内容。
  • content(内容类型):重要!!这个网页的格式是文本的,网页模式
  • charset(编码):特别重要!!!这个网页的编码是utf-8,中文编码,需要注意的是这个是网页内容的编码,而不是文件本身的,其他类型的编码中文可能会出现乱码。
  • http-equiv="Content-Type" 表示描述文档类型

content="text/HTML; 文档类型,这里为html,如果JS就是text/javascript,

charset=utf-8 页面字符集,编码,eg:gb2312,iso-8859-1,utf-8

  • meta标签

meta是html语言head区的一个辅助性标签。几乎所有的网页里,我们可以看到类似下 面这段的html代码:

<head> 
 <meta http-equiv="content-Type" content="text/html; charset=gb2312"> 
</head>

也许你认为这些代码可有可无。其实如果你能够用好meta标签,会给你带来意想不到的效果,例如加入关键字会自动被大型搜索网站自动搜集;可以设定页面格式及刷新等等。

  • meta标签的组成

meta标签共有两个属性,它们分别是http-equiv属性和name属性,不同的属性又有不同的参数值,这些不同的参数值就实现了不同的网页功能。

1、name属性

name属性主要用于描述网页,与之对应的属性值为content,content中的内容主要是便于搜索引擎机器人查找信息和分类信息用的。

meta标签的name属性语法格式是:

<meta name="参数" content="具体的参数值"> 

其中name属性主要有以下几种参数:

1)Keywords(关键字)
  说明:keywords用来告诉搜索引擎你网页的关键字是什么。
  举例:
 <meta name="keywords" content="science, education,culture,politics,ecnomics,relationships, entertaiment, human">
2)description(网站内容描述)
  说明:description用来告诉搜索引擎你的网站主要内容。
  举例:
 <meta name="description" content="This page is about the meaning of science, education,culture.">
3)robots(机器人向导)
  说明:robots用来告诉搜索机器人哪些页面需要索引,哪些页面不需要索引。
  content的参数有all,none,index,noindex,follow,nofollow。默认是all。
  举例:
 <meta name="robots" content="none">
4)author(作者)
  说明:标注网页的作者
  举例:
 <meta name="author" content="root,root@21cn.com">

2、http-equiv属性

http-equiv顾名思义,相当于http的文件头作用,它可以向浏览器传回一些有用的信息,以帮助正确和精确地显示网页内容,与之对应的属性值为content,content中的内容其实就是各个参数的变量值。

meta标签的http-equiv属性语法格式是:

 <meta http-equiv="参数" content="参数变量值">

其中http-equiv属性主要有以下几种参数:

1)Expires(期限)
  说明:可以用于设定网页的到期时间。一旦网页过期,必须到服务器上重新传输。
  用法:
 <meta http-equiv="expires" content="Fri, 12 Jan 2001 18:18:18 GMT">
  注意:必须使用GMT的时间格式。
2)Pragma(cache模式)
  说明:禁止浏览器从本地计算机的缓存中访问页面内容。
  用法:
 <meta http-equiv="Pragma" content="no-cache">
  注意:这样设定,访问者将无法脱机浏览。
3)Refresh(刷新)
  说明:自动刷新并指向新页面。
  用法:
 <meta http-equiv="Refresh" content="2;URL=http://www.root.net">(注意后面的引号,分别在秒数的前面和网址的后面)
  注意:其中的2是指停留2秒钟后自动刷新到URL网址。
4)Set-Cookie(cookie设定)
  说明:如果网页过期,那么存盘的cookie将被删除。
  用法:
 <meta http-equiv="Set-Cookie" content="cookievalue=xxx; expires=Friday, 12-Jan-2001 18:18:18 GMT; path=/">
  注意:必须使用GMT的时间格式。
5)Window-target(显示窗口的设定)
  说明:强制页面在当前窗口以独立页面显示。
  用法:
 <meta http-equiv="Window-target" content="_top">
  注意:用来防止别人在框架里调用自己的页面。
6)content-Type(显示字符集的设定)
  说明:设定页面使用的字符集。
  用法:
 <meta http-equiv="content-Type" content="text/html; charset=gb2312">
7)content-Language(显示语言的设定)
  用法:
 <meta http-equiv="Content-Language" content="zh-cn" />

meta标签的功能

  • 帮助主页被各大搜索引擎登录;
  • 定义页面的使用语言
  • 自动刷新并指向新的页面
  • 实现网页转换时的动画效果
  • 控制页面缓冲
  • 控制网页显示的窗口

3、style中的属性

  • font-size:数值px; 文字大小控制
  • color:#六进制的颜色值; 文字颜色控制
  • text-align:left/center/right; 文字的居左、居中、居右控制。

标题标签


文章的段落用<p>标签,那么文章的标题用什么标签呢?下面我们将使用<hx>标签来制作文章的标题。

标题标签一共有6个,h1、h2、h3、h4、h5、h6分别为一级标题、二级标题、三级标题、四级标题、五级标题、六级标题。并且依据重要性递减。<h1>是最高的等级。

语法:

<hx>标题文本</hx> (x为1-6)

文章的标题前面已经说过了,可以使用标题标签,另外网页上的各个栏目的标题也可使用它们。

例如:

<body>
 <h1>一级标题</h1>
 <h2>二级标题</h2>
 <h3>三级标题</h3>
 <h4>四级标题</h4>
 <h5>五级标题</h4>
</body>

HTML注释


代码注释的作用是帮助程序员标注代码的用途,过一段时间后再看你所编写的代码,就能很快想起这段代码的用途。代码注释不仅方便程序员自己回忆起以前代码的用途,还可以帮助其他程序员很快的读懂你的程序的功能,方便多人合作开发网页代码。

<!--注释文字 -->

语义化


标签的用途:我们学习网页制作时,常常会听到一个词,语义化。那么什么叫做语义化呢,说的通俗点就是:明白每个标签的用途(在什么情况下使用此标签合理)比如,网页上的文章的标题就可以用标题标签,网页上的各个栏目的栏目名称也可以使用标题标签。文章中内容的段落就得放在段落标签中,在文章中有想强调的文本,就可以使用 em 标签表示强调等等。

讲了这么多语义化,但是语义化可以给我们带来什么样的好处呢?

  1. 更容易被搜索引擎收录。
  2. 更容易让屏幕阅读器读出网页内容。

后面会带领大家学习了解html中每个标签的语义(用途)。

喜欢前端的小伙伴们可以在评论区留言,寻找和小冯童鞋一样热爱前端的友人,让我们一起玩转前端的世界!

者:kevinylzhao,腾讯音乐前端开发工程师

浏览器缓存策略对于前端开发同学来说不陌生,大家都有一定的了解,但如果没有系统的归纳总结,可能三言两语很难说明白,甚至说错,尤其在面试过程中感触颇深,很多候选人对这类基础知识竟然都是一知半解,说出几个概念就没了,所以重新归纳总结下,温故而知新。


Web 缓存介绍

  • Web 缓存是指一个 Web 资源(如 html 页面,图片,js,数据等)存在于 Web 服务器和客户端(浏览器)之间的副本。
  • 缓存会根据进来的请求保存输出内容的副本;当下一个请求来到的时候,如果是相同的 URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。

Web 缓存的好处

  • 减少网络延迟,加快页面打开速度
  • 减少网络带宽消耗
  • 降低服务器压力
  • ...

HTTP 的缓存机制

简化的流程如下

根据什么规则缓存

  1. 新鲜度(过期机制):也就是缓存副本有效期。一个缓存副本必须满足以下条件,浏览器会认为它是有效的,足够新的:
  • 含有完整的过期时间控制头信息(HTTP 协议报头),并且仍在有效期内;
  • 浏览器已经使用过这个缓存副本,并且在一个会话中已经检查过新鲜度;
  1. 校验值(验证机制):服务器返回资源的时候有时在控制头信息带上这个资源的实体标签 Etag(Entity Tag),它可以用来作为浏览器再次请求过程的校验标识。如果发现校验标识不匹配,说明资源已经被修改或过期,浏览器需求重新获取资源内容。

HTTP 缓存的两个阶段

浏览器缓存一般分为两类:强缓存(也称本地缓存)和协商缓存(也称弱缓存)。

本地缓存阶段

浏览器发送请求前,会先去缓存里查看是否命中强缓存,如果命中,则直接从缓存中读取资源,不会发送请求到服务器。否则,进入下一步。

协商缓存阶段

当强缓存没有命中时,浏览器一定会向服务器发起请求。服务器会根据 Request Header 中的一些字段来判断是否命中协商缓存。如果命中,服务器会返回 304 响应,但是不会携带任何响应实体,只是告诉浏览器可以直接从浏览器缓存中获取这个资源。如果本地缓存和协商缓存都没有命中,则从直接从服务器加载资源。

启用&关闭缓存

按照本地缓存阶段和协商缓存阶段分类:

  1. 使用 HTML Meta 标签    Web 开发者可以在 HTML 页面的节点中加入标签,如下:

上述代码的作用是告诉浏览器当前页面不被缓存,事实上这种禁用缓存的形式用处很有限:

a. 仅有 IE 才能识别这段 meta 标签含义,其它主流浏览器仅识别“Cache-Control: no-store”的 meta 标签。

b. 在 IE 中识别到该 meta 标签含义,并不一定会在请求字段加上 Pragma,但的确会让当前页面每次都发新请求(仅限页面,页面上的资源则不受影响)。

  1. 使用缓存有关的 HTTP 消息报头 这里需要了解 HTTP 的基础知识。一个 URI 的完整 HTTP 协议交互过程是由 HTTP 请求和 HTTP 响应组成的。有关 HTTP 详细内容可参考《Hypertext Transfer Protocol — HTTP/1.1》、《HTTP 权威指南》等。

在 HTTP 请求和响应的消息报头中,常见的与缓存有关的消息报头有:

上图中只是常用的消息报头,下面来看下不同字段之间的关系和区别:

  1. Cache-Control 与 Expires
  2. Cache-Control:HTTP1.1 提出的特性,为了弥补 Expires 缺陷加入的,提供了更精确细致的缓存功能。详细了解详细看几个常见的指令:_ max-age:功能和 Expires 类似,但是后面跟一个以“秒”为单位的相对时间,来供浏览器计算过期时间。_ no-cache:提供了过期验证机制。(在 Chrome 的 devtools 中勾选 Disable cache 选项,发送的请求会去掉 If-Modified-Since 这个 Header。同时设置 Cache-Control:no-cache Pragma:no-cache,每次请求均为 200)
    • no-store:表示当前请求资源禁用缓存;
    • public:表示缓存的版本可以被代理服务器或者其他中间服务器识别;
    • private:表示只有用户自己的浏览器能够进行缓存,公共的代理服务器不允许缓存。
  • Expires:HTTP1.0 的特性,标识该资源过期的时间点,它是一个绝对值,格林威治时间(Greenwich Mean Time, GMT),即在这个时间点之后,缓存的资源过期;优先级:Cache-Control 优先级高于 Expires,为了兼容,通常两个头部同时设置;浏览器默认行为:其实就算 Response Header 中沒有设置 Cache-Control 和 Expires,浏览器仍然会缓存某些资源,这是浏览器的默认行为,是为了提升性能进行的优化,每个浏览器的行为可能不一致,有些浏览器甚至没有这样的优化。
  1. Last-Modified 与 ETag
  2. Last-Modified(Response Header)与 If-Modified-Since(Request Header)是一对报文头,属于 http 1.0。If-Modified-Since 是一个请求首部字段,并且只能用在 GET 或者 HEAD 请求中。Last-Modified 是一个响应首部字段,包含服务器认定的资源作出修改的日期及时间。当带着 If-Modified-Since 头访问服务器请求资源时,服务器会检查 Last-Modified,如果 Last-Modified 的时间早于或等于 If-Modified-Since 则会返回一个不带主体的 304 响应,否则将重新返回资源。(注意:在 Chrome 的 devtools 中勾选 Disable cache 选项后,发送的请求会去掉 If-Modified-Since 这个 Header。)
  • ETag 与 If-None-Match 是一对报文头,属于 http 1.1。ETag 是一个响应首部字段,它是根据实体内容生成的一段 hash 字符串,标识资源的状态,由服务端产生。If-None-Match 是一个条件式的请求首部。如果请求资源时在请求首部加上这个字段,值为之前服务器端返回的资源上的 ETag,则当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的时候,服务器才会返回带有所请求资源实体的 200 响应,否则服务器会返回不带实体的 304 响应。
  • ETag 能解决什么问题?

a. Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度;

b. 某些文件也许会周期性的更改,但是它的内容并不改变(仅仅改变的修改时间),但 Last-Modified 却改变了,导致文件没法使用缓存;

c. 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形。

  • 优先级:ETag 优先级比 Last-Modified 高,同时存在时会以 ETag 为准。
缓存位置

浏览器可以在内存、硬盘中开辟一个空间用于保存请求资源副本。我们经常调试时在 DevTools Network 里看到 Memory Cache(內存缓存)和 Disk Cache(硬盘缓存),指的就是缓存所在的位置。请求一个资源时,会按照优先级(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找缓存,如果命中则使用缓存,否则发起请求。这里先介绍 Memory Cache 和 Disk Cache。

200 from memory cache

表示不访问服务器,直接从内存中读取缓存。因为缓存的资源保存在内存中,所以读取速度较快,但是关闭进程后,缓存资源也会随之销毁,一般来说,系统不会给内存分配较大的容量,因此内存缓存一般用于存储较小文件。同时内存缓存在有时效性要求的场景下也很有用(比如浏览器的隐私模式)。

200 from disk cache

表示不访问服务器,直接从硬盘中读取缓存。与内存相比,硬盘的读取速度相对较慢,但硬盘缓存持续的时间更长,关闭进程之后,缓存的资源仍然存在。由于硬盘的容量较大,因此一般用于存储大文件。

下图可清晰看出差别:

200 from prefetch cache

在 preload 或 prefetch 的资源加载时,两者也是均存储在 http cache,当资源加载完成后,如果资源是可以被缓存的,那么其被存储在 http cache 中等待后续使用;如果资源不可被缓存,那么其在被使用前均存储在 memory cache。

CDN Cache

以腾讯 CDN 为例:X-Cache-Lookup:Hit From MemCache 表示命中 CDN 节点的内存;X-Cache-Lookup:Hit From Disktank 表示命中 CDN 节点的磁盘;X-Cache-Lookup:Hit From Upstream 表示没有命中 CDN。

整体流程

从上图能感受到整个流程,比如常见两种刷新场景:

  • 当 F5 刷新网页时,跳过强缓存,但是会检查协商缓存;
  • 当 Ctrl + F5 强制刷新页面时,直接从服务器加载,跳过强缓存和协商缓存

其他 Web 缓存策略

IndexDB

IndexedDB 就是浏览器提供的本地数据库,能够在客户端存储可观数量的结构化数据,并且在这些数据上使用索引进行高性能检索的 API。

异步 API 方法调用完后会立即返回,而不会阻塞调用线程。要异步访问数据库,要调用 window 对象 indexedDB 属性的 open() 方法。该方法返回一个 IDBRequest 对象 (IDBOpenDBRequest);异步操作通过在 IDBRequest 对象上触发事件来和调用程序进行通信。

常用异步 API 如下:

在 16 年曾基于 IndexDB 做过一整套缓存策略,有不错的优化效果:

Service Worker

SW 从 2014 年提出的草案到现在已经发展很成熟了,基于 SW 做离线缓存,让用户能够进行离线体验,消息推送体验,离线缓存能力涉及到 Cache 和 CacheStorage 的概念,篇幅有限,不展开了。

LocalStorage

localStorage 属性允许你访问一个 Document 源(origin)的对象 Storage 用于存储当前源的数据,除非用户人为清除(调用 localStorage api 或者清除浏览器数据), 否则存储在 localStorage 的数据将被长期保留。

SessionStorage

sessionStorage 属性允许你访问一个 session Storage 对象,用于存储当前会话的数据,存储在 sessionStorage 里面的数据在页面会话结束时会被清除。页面会话在浏览器打开期间一直保持,并且重新加载或恢复页面仍会保持原来的页面会话。

定义最优缓存策略

  • 使用一致的网址:如果您在不同的网址上提供相同的内容,将会多次获取和存储该内容。注意:URL 区分大小写!
  • 确定中继缓存可以缓存哪些资源:对所有用户的响应完全相同的资源很适合由 CDN 或其他中继缓存进行缓存;
  • 确定每个资源的最优缓存周期:不同的资源可能有不同的更新要求。审查并确定每个资源适合的 max-age;
  • 确定网站的最佳缓存层级:对 HTML 文档组合使用包含内容特征码的资源网址以及短时间或 no-cache 的生命周期,可以控制客户端获取更新的速度;
  • 更新最小化:有些资源的更新比其他资源频繁。如果资源的特定部分(例如 JS 函数或一组 CSS 样式)会经常更新,应考虑将其代码作为单独的文件提供。这样,每次获取更新时,剩余内容(例如不会频繁更新的库代码)可以从缓存中获取,确保下载的内容量最少;
  • 确保服务器配置或移除 ETag:因为 Etag 跟服务器配置有关,每台服务器的 Etag 都是不同的;
  • 善用 HTML5 的缓存机制:合理设计启用 LocalStorage、SessionStorage、IndexDB、SW 等存储,会给页面性能带来明显提升;
  • 结合 Native 的强大存储能力:善于利用客户端能力,定制合适的缓存机制,打造极致体验。

结语

通过了解浏览器各种缓存机制和存储能力特点,结合业务制定合适的缓存策略,善用缓存是基本功,可以用于时常审查负责的业务,可能就会发现个别业务并没有运用到位,共勉。