通过参数进行缓存破坏

157

我们希望在生产发布时进行缓存清除,但不想一开始就浪费很多时间来制定这样的系统。我的想法是在CSS和JS文件的末尾应用一个带有当前版本号的参数:

<link rel="stylesheet" href="base_url.com/file.css?v=1.123"/>

两个问题:这样做会有效地破坏缓存吗?param参数会导致浏览器从该URL永远不会缓存响应,因为param参数指示这是动态内容吗?

12个回答

148

参数?v=1.123 表示一个查询字符串,因此浏览器会认为它是来自于新的路径,比如说?v=1.0。这将导致它从文件中加载,而不是从缓存中加载。根据您的需求。

此外,浏览器将假定下一次调用?v=1.123时源代码将保持不变,并且应该将其与该字符串一起缓存。因此,无论你的服务器设置如何,它都将保留在缓存中,直到你转移到?v=1.124或者更高版本。


6
引用 Steve Souders 的话:“为了从流行的代理缓存中获益,请避免使用查询字符串进行版本控制,而是通过修改文件名来实现版本控制。” 完整的解释可以在这里找到:http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/ - lao
34
那篇博客文章现在已经接近十年了。您认为缓存提供商和CDN是否已经适应了它?Squid似乎现在能够缓存带有查询字符串的文档。(http://serverfault.com/questions/401201/squid-cannot-cache-static-files-with-query-string/401205) - jeteon
1
也许这能帮到某些人:个人而言,我使用文件修改时间戳作为“自动”版本参数,例如 <link rel="stylesheet" href="style.css?v=1487935578" /> - oelna
我个人不太理解为什么,但Lara Hogan(Swanson)(Etsy的工程经理)不建议使用查询参数来进行缓存破坏。我认为这与用户和服务器之间的缓存代理有关。 - Sam Rueby
@lao,这个解释基于Squid不会缓存带查询字符串的资源。但是自2015年以来,情况已经发生了改变,根据https://serverfault.com/questions/401201/squid-cannot-cache-static-files-with-query-string#comment807522_401205的说法,Squid现在可以缓存带查询字符串的静态文件。 - ElBidoule

43
两个问题:这会有效地破坏缓存吗?
是的。即使Stack Overflow也使用这种方法,尽管我记得他们(每天有数百万访问者和无数不同的客户端和代理版本和配置)曾经遇到一些极端情况,即使这样仍然无法打破缓存。但是一般认为这将起作用,并且是打破客户端缓存的适当方法。
另一个问题:该参数是否会导致浏览器从该URL永远不缓存响应,因为该参数表示这是动态内容?
不会。该参数不会改变缓存策略;服务器发送的缓存头仍然适用,如果没有发送任何内容,则使用浏览器的默认值。

1
@spender,很抱歉我现在找不到参考资料了。Jeff Atwood 在一篇长篇博客文章或者 Stack Overflow 的回答中提到过这个问题(如果我没记错的话)。 - Pekka
2
@spender 我已经阅读过一些代理服务器(可能是旧的,或者可以配置)在缓存时忽略查询字符串。 - MrWhite
2
@spender - 我也听说过这个问题,我认为更改文件名或路径是最好的选择。最简单的方法可能是将所有静态文件移动到一个带版本号的文件夹下,例如/static/v22/file.css,因为您可以通过单个文件夹重命名来处理多个文件,例如/static/v23/file.css/static/v23/mystuff.js - Brad Parks
哇,我刚刚查看了这个页面的源代码,即使在2022年,StackOverflow仍然使用查询字符串来清除其资产缓存! - duhaime

25

把版本号放在实际的文件名中更安全。这样可以同时存在多个版本,因此您可以发布新版本,如果任何缓存的HTML页面仍然要求旧版本,则它们将获取与其HTML兼容的版本。

请注意,在互联网上最大的版本化部署之一中,jQuery在实际的文件名中使用版本号,它可以安全地允许多个版本共存,而不需要任何特殊的服务器端逻辑(每个版本只是不同的文件)。

这使得在部署新页面和新链接文件时缓存失效一次(这正是您想要的),从那时起,这些版本可以被有效地缓存(这也是您想要的)。


我同意这一点,但是只需让Sinatra将?v=<%= VERSION %>附加到所有css和js请求中要容易得多,而不是必须单独控制每个文件。最终,我们将切换到sinatra-assetpack,它将预处理和压缩所有文件并实际附加版本号到文件名中,这将使我们更容易单独控制它们。 - Brad Herman
2
如果您想要确保10000%的安全性,我同意在文件名中放置版本号是最终最安全的解决方案,但我不认同“同时存在多个版本”的观点。带有查询参数的URL与具有不同查询参数的相同URL是不同的资源,并且客户端应将它们视为两个不同的资源;如果没有,则客户端存在问题。 - Pekka
2
@Pekka - 版本号可以允许多个版本同时存在,但这需要服务器的合作来将查询参数映射到正确的实际文件。我不认为这是OP在这里所做的,而且在修改文件名更简单且不需要服务器合作的情况下,没有太多理由要求这种复杂性。显然,两者都可以工作。 - jfriend00

13

正如其他人所说,使用查询参数进行缓存破坏通常被认为是一个不好的做法,并且已经有很长时间了。更好的方法是在文件名中反映版本号。Html5 Boilerplate 建议避免使用查询字符串等方式。

尽管如此,在我看过的所有引用来源的建议中,似乎都从Steve Souders 2008年的文章中汲取智慧。他的结论基于当时代理服务器的行为,现在可能或可能不再相关。然而,在没有更多当前信息的情况下,更改文件名是安全的选择。


9

一旦客户端下载了资源,它将进行一次缓存破坏,除非:

  1. v参数被更新。
  2. 客户端清除其缓存

6

一般来说,这应该是没有问题的,但如果有一个中间缓存(代理)被配置为忽略请求参数,则可能无法正常工作。

例如,如果您通过Akamai CDN提供静态内容,则可以配置它忽略请求参数以防止使用此方法进行缓存破坏。


5

这里发现了两种技术(查询字符串与文件名)的比较:

作为查询字符串的版本有两个问题。

首先,可能不是所有实现缓存的浏览器都需要我们破坏查询字符串。据说某些代理(可能是旧的)会忽略与其缓存行为相关的查询字符串。

其次,在某些更复杂的部署方案中,您将拥有多个前端和/或多个后端服务器,而升级则绝非瞬间完成。您需要能够同时提供资产的旧版本和新版本。例如,查看在使用Google App Engine时如何影响您。


5

这在很大程度上取决于您希望缓存的强度如何。例如,Squid代理服务器(可能还有其他服务器)默认不会对带查询字符串的URL进行缓存-至少在写这篇文章时是这样的。如果您不介意某些使用情况导致不必要的缓存未命中,则可以使用查询参数。但是,非常容易设置基于文件名的缓存破坏方案来避免此问题。


7
引用在 Steve Souders 的文章中的Squid代理已更改其默认缓存策略。自版本2.7(2008年5月)和版本3.1(2010年3月)以来,其默认行为是缓存动态内容。 - Josh Rack

3
另一种类似的方法是使用htaccess mod_rewrite来在提供文件时忽略路径的一部分。您的未缓存索引页面引用了文件的最新路径。
从开发角度来看,使用版本号参数非常容易,但与文件名方法一样强大。
使用路径的被忽略部分作为版本号,服务器将忽略它并提供未缓存的文件。 1.2.3/css/styles.csscss/styles.css提供相同的文件,因为htaccess文件会去除并忽略第一个目录。
包含版本化文件
<?php
  $version = "1.2.3";
?>

<html>
  <head>
    <meta http-equiv="cache-control" content="max-age=0" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
    <meta http-equiv="pragma" content="no-cache" />
    <link rel="stylesheet" type="text/css" href="<?php echo $version ?>/css/styles.css">
  </head>
  <body>
    <script src="<?php echo $version ?>/js/main.js"></script>
  </body>
</html>

请注意,这种方法意味着您需要禁用索引页面的缓存 - 使用标签在所有浏览器中关闭缓存?

.htaccess文件

RewriteEngine On

# if you're requesting a file that exists, do nothing
RewriteCond %{REQUEST_FILENAME} !-f 
# likewise if a directory that exists, do nothing
RewriteCond %{REQUEST_FILENAME} !-d 

# otherwise, rewrite foo/bar/baz to bar/baz - ignore the first directory
RewriteRule ^[^/]+/(.+)$ $1 [L] 

你可以在任何允许url重写的服务器平台上采用相同的方法。
(改编自mod_rewrite - rewrite directory to query string except /#!/的重写条件)
... 如果你需要为你的索引页面/站点入口进行缓存破坏,你总是可以使用JavaScript来刷新它。

2
<script type="text/javascript">
// front end cache bust

var cacheBust = ['js/StrUtil.js', 'js/protos.common.js', 'js/conf.js', 'bootstrap_ECP/js/init.js'];   
for (i=0; i < cacheBust.length; i++){
     var el = document.createElement('script');
     el.src = cacheBust[i]+"?v=" + Math.random();
     document.getElementsByTagName('head')[0].appendChild(el);
}
</script> 

在新版本的开发/测试期间,缓存可能是一个问题,因为浏览器、服务器甚至有时候3G电信(如果你进行移动部署)会缓存静态内容(例如JS、CSS、HTML、img)。你可以通过在URL中添加版本号、随机数或时间戳来克服这个问题,例如:JSP:<script src="js/excel.js?time=<%=new java.util.Date()%>"></script>如果你运行的是纯HTML(而不是服务器页JSP、ASP、PHP),那么服务器将无法帮助你。在浏览器中,链接在JS运行之前加载,因此你必须删除链接,并使用JS加载它们。 - Conete Cristian
我认为这不会按顺序同步加载JS文件。 - Stealth Rabbi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接