fetch() 的 Referrer 和缓存控制 API

大约一年前,我们写过一篇关于新的fetch() API的文章。WHATWG Fetch API 提供了一种现代化的网络资源获取方式,并允许您对请求和响应的详细信息进行精细控制。如果您不熟悉 Fetch API,建议您在继续之前阅读相关内容。

我们最近在 Fetch API 中添加了一些新功能,本文将概述这些功能,并给出如何利用它们开发 Web 应用程序的示例。

Referrer 控制 API

使用 fetch(),您现在可以控制 HTTP 请求referrerreferrer 策略。HTTP Referer [sic] 标头是一个(拼写错误的!)标头,它允许目标页面知道用户来自哪个源页面(例如,通过点击该页面上的链接)。这对于收集关于您的网站用户来自哪里 的分析数据非常有用。

referrer 策略是一个新的 W3C 规范,我们一直在 Firefox 中实现该规范,它允许页面向浏览器提供一个策略,使页面能够更好地控制 Referer 标头的设置方式。有几种不同的策略状态,每种状态都具有特定的目标。以下是一个总结。

  • “no-referrer” 阻止发送任何 Referer 标头。当您出于隐私原因想要隐藏 Referer 标头时,这可能很有用。例如,一些搜索引擎在 URL 中添加了有关用户搜索词和其他信息的额外信息,他们可能不希望将用户的搜索词泄露给用户点击的搜索结果网站。 “no-referrer” referrer 策略可以用于此目的。
  • “no-referrer-when-downgrade” 类似于 “no-referrer”,区别在于它仅在从安全上下文导航到非安全上下文时才会省略 Referer 标头。例如,在上面的搜索引擎示例中,如果您担心的是人们监视 HTTP 流量而不是目标网站,则可以使用 “no-referrer-when-downgrade” 策略。在这种情况下,如果搜索结果链接到安全上下文,浏览器将发送 Referer 标头,但如果目标网站是非安全 HTTP 网站,浏览器将拒绝以明文发送 Referer 标头。如果未指定显式策略,这是默认策略。
  • “origin” 将使浏览器仅在 Referer 标头中包含引用源,而不包含完整的 URL。例如,如果您希望目标网站能够知道用户来自您的搜索结果页面,而不透露完整的 URL,则可以使用 “origin”。在这种情况下,浏览器将从 Referer 标头中发送的 URL 中删除域名后面的所有内容。
  • “origin-when-cross-origin” 类似于 “origin”,但它仅在跨源导航时才会删除完整的 URL。例如,假设您只想在跨源导航时(例如,如果您网站正在执行正常的网页搜索,我们假设您的搜索结果页面是跨源的)包含您的搜索结果页面的源,而发送完整的 referrer 到您自己的内部页面。这可以让您自己的分析软件了解您的用户如何跨越您网站的页面导航。在这种情况下,“origin-when-cross-origin” 是正确的策略选择。
  • “unsafe-url” 会导致浏览器将完整的 URL(不包含任何相关的用户名、密码或片段)发送到用户导航到的所有页面,无论它们是跨源还是安全。它被称为unsafe 的真正原因是它会将完整的 URL 透露给任何目标网页,这会引发隐私问题,例如上面的例子试图解决的问题。如果可能,您应该考虑使用其他 referrer 策略。

现在,在 Firefox 中,您可以在页面上使用<meta name=referrer> 元素为从页面发起的的所有网络请求设置全局 referrer 策略。我们还正在努力实现逐元素 referrer 策略属性,这在您想对特定元素(例如 <img>)使用不同的 referrer 策略时很有用。借助此处介绍的新 API,您还可以控制使用 fetch() 下载的资源的 referrer 和 referrer 策略。

以下代码示例展示了如何使用这些新的 fetch() 功能的一些示例。

  // Let’s assume that the code below runs on https://example.site/page.html

  // Download a json but don't reveal who is downloading it
  fetch("sneaky.json", {referrerPolicy: "no-referrer"})
    .then(function(response) { /* consume the response */ });

  // Download a json but pretend another page is downloading it
  fetch("sneaky.json", {referrer: "https://example.site/fake.html"})
    .then(function(response) { /* consume the response */ });

  // You can only set same-origin referrers.
  fetch("sneaky.json", {referrer: "https://cross.origin/page.html"})
    .catch(function(exc) {
      // exc.name == "TypeError"
      // exc.message == "Referrer URL https://cross.origin/page.html cannot be cross-origin to the entry settings object (https://example.site)."
    });

  // Download a potentially cross-origin json and don't reveal
  // the full referrer URL across origins
  fetch(jsonURL, {referrerPolicy: "origin-when-cross-origin"})
    .then(function(response) { /* consume the response */ });

  // Download a potentially cross-origin json and reveal a
  // fake referrer URL on your own origin only.
  fetch(jsonURL, {referrer: "https://example.site/fake.html",
                  referrerPolicy: "origin-when-cross-origin"})
    .then(function(response) { /* consume the response */ });

  // Override sending the document global referrer policy set using
  // to send the full referrer URL.
  // Be careful!
  fetch(jsonURL, {referrerPolicy: "unsafe-url"})
    .then(function(response) { /* consume the response */ });

如果您的网站使用service worker,那么您可以检查获取资源时附带的 referrer 和 referrer 策略。使用fetch 事件处理程序 内部的referrerreferrerPolicy 属性,检查Request 对象。

此 API 将在 Firefox 47 中可用,该版本目前可在开发者版发布通道 中进行测试。

Fetch 缓存控制 API

通过 fetch() 下载的资源,类似于浏览器下载的其他资源,会受到HTTP 缓存 的影响。这通常是可以的,因为这意味着如果您的浏览器有一个缓存的 HTTP 请求响应副本,它可以使用缓存的副本,而不是浪费时间和带宽从远程服务器重新下载。

但是,在某些情况下,您可能希望对浏览器是否使用 HTTP 缓存进行一些控制。您可以通过清除要获取的资源的 URL 缓存 来确保无论浏览器 HTTP 缓存中有什么,您都能获得最新的响应,将其获取到service worker 控制的缓存 中。这通常通过在下载之前将参数(例如 'cache-bust=' + Date.now())附加到 URL 来完成,但这非常丑陋。现在有一种更好的方法可以使用 fetch 缓存控制 API 来完成。

此 API 的理念是为 fetch 指定一个缓存策略,以明确指示何时以及如何使用浏览器 HTTP 缓存。要有效地使用这些策略,需要充分了解 HTTP 缓存语义。网上有很多关于 HTTP 缓存语义的文章,例如这篇文章,其中详细描述了这些语义。目前,您可以从五种不同的策略中进行选择。

  • “default” 表示在下载资源时使用浏览器的默认行为。浏览器首先查看 HTTP 缓存中是否有匹配的请求。如果有,并且是新鲜的,它将从 fetch() 返回。如果存在但已过时,则会向远程服务器发出条件请求,如果服务器指示响应未更改,则将从 HTTP 缓存中读取。否则,它将从网络下载,并使用新响应更新 HTTP 缓存。
  • “no-store” 表示完全绕过 HTTP 缓存。这将使浏览器在进入网络时不查看 HTTP 缓存,并且永远不会将生成的响应存储在 HTTP 缓存中。使用此缓存模式,fetch() 的行为就像不存在 HTTP 缓存一样。
  • “reload” 表示在进入网络时绕过 HTTP 缓存,但会使用新下载的响应更新它。这将导致浏览器在进入网络时永远不查看 HTTP 缓存,但会使用下载的响应更新 HTTP 缓存。如果合适,将来的请求可以使用该更新的响应。
  • “no-cache” 表示即使浏览器认为响应是新鲜的,也始终验证 HTTP 缓存中的响应。这将导致浏览器在进入网络时查找 HTTP 缓存中是否有匹配的请求。如果找到了这样的请求,浏览器将始终创建一个条件请求来验证它,即使它认为响应应该是新鲜的。如果没有找到匹配的缓存条目,则会发出正常请求。下载响应后,HTTP 缓存将始终使用该响应进行更新。
  • “force-cache” 表示如果在缓存中找到了匹配的条目,浏览器将始终使用缓存的响应,而忽略响应的有效性。因此,即使在缓存中找到了非常旧版本的响应,它也将始终使用它,而不会进行验证。如果在缓存中找不到匹配的条目,浏览器将发出正常请求,并使用下载的响应更新 HTTP 缓存。

让我们看一些关于如何使用这些缓存模式的示例。

  // Download a resource with cache busting, to bypass the cache
  // completely.
  fetch("some.json", {cache: "no-store"})
    .then(function(response) { /* consume the response */ });

  // Download a resource with cache busting, but update the HTTP
  // cache with the downloaded resource.
  fetch("some.json", {cache: "reload"})
    .then(function(response) { /* consume the response */ });

  // Download a resource with cache busting when dealing with a
  // properly configured server that will send the correct ETag
  // and Date headers and properly handle If-Modified-Since and
  // If-None-Match request headers, therefore we can rely on the
  // validation to guarantee a fresh response.
  fetch("some.json", {cache: "no-cache"})
    .then(function(response) { /* consume the response */ });

  // Download a resource with economics in mind!  Prefer a cached
  // albeit stale response to conserve as much bandwidth as possible.
  fetch("some.json", {cache: "force-cache"})
    .then(function(response) { /* consume the response */ });

此 API 计划在 Firefox 48 中发布,目前已在 Firefox Nightly 中提供测试。

关于 Ehsan Akhgari

Ehsan 自 2006 年以来一直致力于 Firefox 的各个部分。现在他大部分时间都花在 Gecko 和 Web API 上。

更多 Ehsan Akhgari 的文章……


14条评论

  1. 陈志翔

    我最近实现了一个基于 chromium 的功能,该功能强制缓存 http 响应数据,当解析器在 html 中看到一个时。 (所有子资源将强制存储在磁盘缓存中)

    所以我的问题是,为什么没有“force-store”,既然有“force-cache”?

    2016 年 3 月 23 日 下午 3:09

    1. Ehsan Akhgari

      “force-store” 会做什么,而你不能用现有的缓存模式实现?

      2016 年 3 月 23 日 下午 8:42

      1. 陈志翔

        我的意思是,“force-store” 是“no-store” 的反面,它绕过默认的 http 缓存策略,将响应数据缓存在本地磁盘缓存中。

        使用“force-store” 和“force-cache”,我们可以实现一个简单的“离线 Web 应用”模式,其中所有内容都由用户自己控制。

        2016 年 3 月 23 日 下午 8:06

        1. Ehsan Akhgari

          我认为您所要求的缓存模式是“reload”。它将绕过 HTTP 缓存,到达网络,并将新获取的数据存储在 HTTP 缓存中以备将来使用。这说得通吗?

          2016 年 3 月 24 日 上午 8:38

          1. 陈志翔

            不,“reload” 与我在这里建议的“force-store” 含义不同。

            “reload” 表示从网络重新加载,但如果 http 服务器响应 Cache-Control 标头说“no-store”,“reload” 无法根据 http 缓存策略进行磁盘缓存。

            而我在这里建议的“force-store” 将绕过默认的 http 缓存策略,强制将响应数据(标头和正文)存储到磁盘缓存中,即使服务器端可能说“Cache-Control: no-store”。

            2016 年 3 月 24 日 下午 7:01

          2. Ehsan Akhgari

            好的,我现在明白了区别。如果您有此用例,请在此处提交问题:https://github.com/whatwg/fetch/issues

            2016 年 3 月 28 日 上午 7:44

  2. Eric Lawrence

    酷,谢谢你的写作!

    重复句子

    “但是,在某些情况下,您可能希望对浏览器是否使用 HTTP 缓存进行一些控制”

    2016 年 3 月 23 日 下午 10:02

    1. Ehsan Akhgari

      谢谢,已修复!

      2016 年 3 月 24 日 上午 8:36

  3. Koemsie Ly

    谢谢你的信息!

    2016 年 3 月 24 日 上午 7:06

  4. Gerben

    将“reload” 选项也执行 If-Modified-Since,就像“no-cache” 一样,难道不合理吗?

    2016 年 3 月 26 日 下午 12:47

    1. Ehsan Akhgari

      我不这么认为。发送 If-Modified-Since 标头是 HTTP 缓存验证的一部分,这是一个旨在确定 HTTP 缓存响应是否足够新,可以用来响应请求的过程,但“reload” 就像网络上根本没有 HTTP 缓存一样,因此将没有响应进行验证。

      我认为您想要的行为可以更好地通过使用“default” 或“no-cache” 来支持,前者在需要时会验证响应(如果找到匹配的响应),而后者会在使用响应之前无条件地验证响应。

      2016 年 3 月 28 日 上午 7:48

      1. Gerben

        “足够新” 我认为不是一个合适的词。If-modified-since 检查本地副本是否仍为最新版本。
        “足够新” 更适合解释“Expires” 标头。

        “no-cache” 方法更好,因为它会检查本地副本是否仍为最新版本,但据我推测,它不会将其存储在本地缓存中。

        如果是这样,就没有办法让 fetch 忽略“Expires” 标头,但仍执行“If-modified-since” 并将任何更改写入本地缓存。

        例如,一个网络摄像头网站。您想检查是否有新图像,但不想浪费带宽下载两次相同的图像。(虽然在这种情况下,服务器也必须发送一个“expires” 标头,该标头在未来很长一段时间内有效)

        2016 年 3 月 29 日 上午 8:04

        1. Ehsan Akhgari

          以下是默认行为(我忽略了 ETag 和相应的 If-None-Match 检查,它们是 HTTP 缓存验证的另一半):如果缓存的资源具有未来有效的 Expires 标头,则认为它足够新。如果 Expires 标头日期已过去,则会生成 If-Modified-Since 请求,以验证缓存的条目是否“足够新”。这就是“default” 行为。

          “no-cache” 改变了这一点,因此无论 Expires 标头指示什么,浏览器都会*始终*发送 If-Modified-Since 请求。

          对于配置为发送正确 Expires 标头的服务器,“default” 行为始终足以确保您从缓存或网络中获取新的响应,如果服务器在响应 If-Modified-Since 请求时指示响应已修改。但是,如果服务器已知设置不切实际的 Expires 标头(例如,一个发送未来过远的 Expires 标头的网络摄像头图像服务器,如您所述),您需要使用“no-cache” 来确保服务器有机会发送新的响应,即使之前响应的 Expires 标头会导致浏览器认为其缓存的响应足够新。

          关于您关于“no-cache” 不用下载的响应更新 HTTP 缓存的观点,这是不正确的,如文章中的说明所示。如果响应是响应“no-cache” 请求而下载的,则它总是存储在 HTTP 缓存中。

          希望这能澄清一些事情!

          2016 年 3 月 29 日 上午 8:39

          1. Gerben

            现在我明白了。感谢您抽出时间解释。

            2016 年 3 月 29 日 上午 8:51

本文的评论已关闭。