如何强制浏览器重新加载缓存的CSS和JS文件?

1150
我注意到一些浏览器(尤其是Opera和Firefox)在使用缓存的.css.js文件时非常热衷,甚至在浏览器会话之间也是如此。当您更新其中一个文件但用户的浏览器继续使用缓存副本时,这会导致问题。
强制用户的浏览器在文件更改后重新加载文件的最优雅方法是什么?
理想情况下,解决方案不会在每次访问页面时都强制浏览器重新加载文件。

我发现John Millikinda5id的建议很有用。原来这个术语叫做自动版本控制

我在下面发布了一个新答案,它是我的原始解决方案和John的建议的结合。

另一个想法是由SCdF提出的,即将虚假查询字符串附加到文件中。(一些Python代码,可以自动使用时间戳作为虚假查询字符串,被pi.提交。)

然而,对于浏览器是否会缓存带查询字符串的文件存在一些讨论。(记住,我们希望浏览器缓存文件并在以后的访问中使用它。我们只希望在文件更改时再次获取它。)


我在我的 .htaccess 文件中有这个设置,从未出现过缓存文件的问题:ExpiresActive On ExpiresDefault "modification" - Frank Conijn - Support Ukraine
2
我绝对同意将版本信息添加到文件的URL中是迄今为止最好的方法。 它可以始终为每个人正常工作。 但是,如果您没有使用它,并且只需要偶尔重新加载一个CSS或JS文件以在自己的浏览器中运行...只需在其自己的选项卡中打开它,然后按SHIFT-reload(或CTRL-F5)! 您可以通过在(隐藏的)iframe中加载文件,等待加载完成,然后调用 iframe.contentWindow.location.reload(true) 来有效地执行相同的操作。请参见 https://dev59.com/6XNA5IYBdhLWcg3wIqPr#22429796 的方法(4)-那是关于图片的,但同样适用。 - Doin
6
我很欣赏这个问题的提问方式,也很感谢提问者对问题进行了更新。它完整地描述了我应该在回答中期待什么。从现在开始,我会采用这种方法来提问。干杯! - rd22
2
参考:da5id's's deleted answer 是这样说的:“如果更新足够大/重要,我通常会更改文件的名称。”。 - Peter Mortensen
如果更改不是很频繁,我有一个建议。只需更改文件名并编辑源代码以包含新的文件名。然后浏览器就没有缓存文件可读取。 - SK-the-Learner
我写了一篇关于使用GitHub进行CDN、自动版本控制、自动更新等方面的方法的文章,这个方法真的很有效。https://dany1980.medium.com/cdn-e-aggiornamenti-automatici-degli-assets-che-restano-in-cache-d362a99f054d(这是从意大利语翻译过来的) - Daniele Rugginenti
58个回答

494

这个解决方案是用PHP编写的,但是很容易适应其他语言。

原来的.htaccess正则表达式可能会在像json-1.3.js这样的文件中出现问题。解决方法是只有当结尾恰好有10位数字时才进行重写。(因为10位数字涵盖了从2001年9月9日到2286年11月20日的所有时间戳。)

首先,在.htaccess中使用以下重写规则:

RewriteEngine on
RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]

现在,我们编写以下PHP函数:
/**
 *  Given a file, i.e. /css/base.css, replaces it with a string containing the
 *  file's mtime, i.e. /css/base.1221534296.css.
 *
 *  @param $file  The file to be loaded. works on all type of paths.
 */
function auto_version($file) {
  if($file[0] !== '/') {
    $file = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', dirname($_SERVER['PHP_SELF'])), '/') . '/' . $file;
  }
  
  if (!file_exists($_SERVER['DOCUMENT_ROOT'] . $file))
  return $file;
  
  $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);
  return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $file);
}

现在,无论您在哪里包含您的CSS,请将其从这个更改:

<link rel="stylesheet" href="/css/base.css" type="text/css" />

转换成这样:
<link rel="stylesheet" href="<?php echo auto_version('/css/base.css'); ?>" type="text/css" />

这样,您就永远不必再修改链接标签了,用户将始终看到最新的CSS。浏览器将能够缓存CSS文件,但是当您对CSS进行任何更改时,浏览器将把它视为新的URL,因此它不会使用缓存副本。

这也适用于图像、网站图标和JavaScript。基本上任何非动态生成的内容都可以这样做。


18
我自己的静态内容服务器与你的完全相同,只是我使用参数进行版本控制(base.css?v=1221534296),而不是更改文件名(base.1221534296.css)。但我怀疑你的方式可能会更有效率。非常酷。 - Jens Roland
4
@Kip:非常巧妙的解决方案。重写URL显然比仅仅美化URL更有价值。 - James P.
40
我看到一个问题,它需要多次访问文件系统——确切地说,是链接数量乘以每秒请求数量的次数…这可能对你有或没有问题。 - Tomáš Fejfar
3
@AlixAxel说:不,浏览器在参数改变时会重新获取它,但一些公共代理可能不会缓存带有URL参数的文件,因此最佳做法是将版本包含在路径中。而mod_rewrite的开销与WPO中的任何其他性能瓶颈相比都微不足道。 - Jens Roland
8
第一个 file_exists 检查真的有必要吗?filemtime 在失败时会返回 false,那么为什么不将 filemtime 的值赋给一个变量,并在重命名文件之前检查它是否为 false 呢?这样可以减少一个不必要的文件操作,从而提高效率。 - Gavin
显示剩余31条评论

229

简单的客户端技巧

一般情况下,缓存是有益的...因此,根据您是在开发网站时为自己解决问题,还是在生产环境中控制高速缓存的不同情况,有几种技术可供选择。

普通访问者来到您的网站时,他们不会像您在开发网站时那样有相同的体验。由于普通访问者可能每个月只访问几次该网站(除非您是Google或hi5 Networks),所以他们很可能没有缓存您的文件,这已经足够了。

如果您想要强制浏览器加载新版本,您可以在请求中添加查询字符串,并在进行重大更改时增加版本号:

<script src="/myJavascript.js?version=4"></script>

这将确保每个人都获得新文件。它的工作原理是因为浏览器查看文件的URL来确定它是否有副本在缓存中。如果您的服务器没有设置任何查询字符串操作,它将被忽略,但对于浏览器而言,文件名看起来像是一个新的文件。

另一方面,如果您正在开发网站,您不希望每次保存开发版本时都更改版本号。那将很繁琐。

因此,在您开发网站时,一个好的技巧是自动生成查询字符串参数:

<!-- Development version: -->
<script>document.write('<script src="/myJavascript.js?dev=' + Math.floor(Math.random() * 100) + '"\><\/script>');</script>

添加查询字符串到请求是一种很好的资源版本控制方式,但对于简单网站可能并不必要。请记住,缓存是一件好事。

值得注意的是,浏览器并不一定会很吝啬地保持文件在缓存中。浏览器有这方面的策略,并且通常遵守HTTP规范中制定的规则。当浏览器向服务器发出请求时,响应的一部分是一个Expires头...一个日期,告诉浏览器应该将其保存多长时间。下次浏览器遇到相同文件的请求时,它会看到自己的缓存中有一个副本,并查看Expires日期来决定是否使用它。

所以信不信由你,实际上是你的服务器使那个浏览器缓存如此持久。你可以调整服务器设置并更改Expires头,但我上面写的小技巧可能是一个更简单的方法。由于缓存是好的,你通常希望将日期设置得很远("远期过期头"),并使用上面描述的技术来强制更改。

如果你对HTTP或这些请求的制作方式更感兴趣,那么推荐一本好书是Steve Souders的《高性能网站》。它是这个领域非常好的入门读物。


3
使用JavaScript生成查询字符串的快速技巧在开发过程中非常好用。我也用PHP做了同样的事情。 - Alan Turing
2
这是实现原帖作者所需结果最简单的方法。如果您想在每次加载页面时强制重新加载.css或.js文件,则mod_rewrite方法非常有效。该方法仍允许缓存,直到您实际更改文件并真正希望强制重新加载为止。 - scott80109
2
当我使用以下代码时,我的CSS似乎无法工作:<link href='myCss.css?dev=14141'...> - Noumenon
6
这并不是一个可行的解决方案。许多浏览器会拒绝缓存任何带查询字符串的内容。这就是为什么Google、GTMetrix和类似的工具会在静态内容引用中有查询字符串时发出警告的原因。虽然这对开发来说肯定是一个不错的解决方案,但它绝对不是生产环境下的解决方案。此外,浏览器控制着缓存,而不是服务器。服务器只是建议何时刷新缓存,浏览器不一定要听从服务器(通常也不会)。移动设备就是一个很好的例子。 - Nate I
1
document.write的解决方案太好了,现在我无法在Chrome中设置断点,因为URL不断变化,从而不断刷新并且失去我的断点! - CoderSteve
显示剩余8条评论

115

谷歌的mod_pagespeed插件适用于Apache,可以为您进行自动版本控制。它非常流畅。

它在从Web服务器输出时解析HTML(适用于PHP,Ruby on Rails,Python,静态HTML-任何内容),并重新编写链接到CSS、JavaScript和图像文件,以便包含ID代码。 它以修改后的URL提供文件,并对其进行非常长时间的缓存控制。 当文件更改时,它会自动更改URL,这样浏览器必须重新获取它们。 它基本上只需运行,无需更改您的代码。 它甚至可以在输出时最小化您的代码。


28
这是错误的(自动篡改源代码),明显是浏览器问题。给我们(开发人员)一个真正的脑部刷新:按下<ctrl>+F5。 - T4NK3R
26
mod_pagespeed的功能等同于针对您的html/css/js进行完全自动化的构建/编译步骤。我认为很难找到任何认为构建系统本质上是错误的严肃开发人员,或者觉得完全自动化有什么问题的人。清除mod_pagespeed的缓存类似于一个干净的构建:http://code.google.com/p/modpagespeed/wiki/FAQ#How_do_I_clear_the_cache_on_my_server? - Leopd
4
@T4NK3R mod_pagespeed并不需要与您的源代码有任何关系来进行缓存管理,它只是简单地提到它可以在诸如缩小文件等方面帮助您。至于它是否“WRONG”,那完全是主观的。它可能对您来说是错误的,但这并不意味着它本质上是“坏”的。 - Madbreaks
2
它也可以与nginx一起使用,但您必须从源代码构建:https://developers.google.com/speed/pagespeed/module/build_ngx_pagespeed_from_source - Rohit
1
@T4NK3R,不仅仅是我们开发人员使用网站。因此,对于普通用户来说,Ctrl+F5并不是一个好的解决方案,我们通常不希望他们看到旧版本。 - Eoin
显示剩余4条评论

101

我不确定你们为什么要花这么多力气来实现这个解决方案。

你只需要获取文件的修改时间戳并将其作为查询字符串附加到文件中即可。

在PHP中,我会这样做:

<link href="mycss.css?v=<?= filemtime('mycss.css') ?>" rel="stylesheet">

filemtime() 是一个 PHP 函数,它可以返回文件的修改时间戳。


5
非常优雅,尽管我稍微修改了它为<link rel="stylesheet" href="mycss.css?<?php echo filemtime('mycss.css') ?>" />,以防一些关于使用GET变量(按照建议格式)缓存URL的参数在此线程上是正确的。 - luke_mclachlan
1
很好的解决方案。此外,我发现filemtime对于完全限定域名(FQDN)无效,因此我在href部分使用了FQDN,在filemtime部分使用了$_SERVER["DOCUMENT_ROOT"]。例如:<link rel="stylesheet" href="http ://theurl/mycss.css?v=<?php echo filemtime($_SERVER["DOCUMENT_ROOT"] . '/mycss.css') ?>"/> - rrtx2000
非常感谢。简单而好。这是Python代码:progpath = os.path.dirname(sys.argv[0]) def versionize(file): timestamp = os.path.getmtime('%s/../web/%s' % (progpath, file)) return '%s?v=%s' % (file, timestamp) print ' \ % versionize('css/main.css') - dlink
1
这种方法有多个问题。首先,它完全消除了对该文件的缓存。问题要求在资产更改时强制刷新,而不是完全防止缓存(这通常是一个非常糟糕的想法)。其次,静态文件上的查询字符串是一个糟糕的想法,因为某些浏览器根本不会缓存它们,而其他浏览器无论查询字符串是什么都会缓存它们。总的来说,这是一个非常初级的解决方案,质疑人们为什么要考虑一个正确的解决方案(而不是一个hack)只是显示了对此事的一般理解不足。 - Nate I
1
绝对完美的快速解决方案。 - Matt
显示剩余5条评论

99

建议您使用实际 CSS 文件的 MD5 哈希值,而不是手动更改版本。

因此,您的 URL 应该类似于

http://mysite.com/css/[md5_hash_here]/style.css

您仍可使用重写规则来删除哈希,但优势在于现在您可以将缓存策略设置为“永久缓存”,因为如果 URL 相同,则表示文件未更改。

然后,您可以编写一个简单的 shell 脚本来计算文件的哈希值并更新您的 <link> 标签(可能需要将其移动到一个单独的文件中进行包含)。

每次更改 CSS 时运行该脚本即可。当文件发生更改时,浏览器将仅重新加载您的文件。如果您进行编辑,然后撤消它,无需费心找出需要返回哪个版本以使访问者不必重新下载。


1
不幸的是,我不知道如何实现它。请给予建议...更多细节... - Michael Phelps
使用Shell、Ruby等语言进行实现将是非常棒的。 - Peter
3
非常好的解决方案.. 但是我认为在每次文件请求(css、js、图片、html等)和每个页面访问时计算文件哈希值会消耗很多资源。 - DeepBlue
这是一个标准解决方案,适用于使用 gulp、grunt 或 webpack 进行 js 或 css 打包的人群。每个解决方案的实现方式不同,但在构建步骤中对文件进行哈希处理是现代打包应用程序的常见做法和建议。 - Brandon Culley
@DeepBlue - 回答中说“每次CSS更改时运行该脚本”。这不是在每次页面访问时运行。另一方面,回答遗漏了重要细节 - 更改的哈希如何成为URL的一部分?我不知道... - ToolmakerSteve
使用文件时间戳代替MD5哈希。你只需要一个唯一的值:File.GetLastWriteTime(filePath).Ticks.ToString() - Katie Kilian

58
你可以在CSS/JavaScript导入的末尾加上?foo=1234,将1234更改为任何你喜欢的数字。可以查看Stack Overflow的HTML源代码作为示例。
这样做的想法是请求中的?参数会被丢弃/忽略,而当你推出新版本时,你可以更改该数字。

注意:关于这对缓存的影响存在一些争议。我认为总体意思是GET请求,无论是否带参数,都应该可以被缓存,因此上述解决方案应该有效。

然而,这取决于Web服务器是否遵循规范的部分以及用户使用的浏览器,因为它仍然可以要求获取最新版本。


9
HTTP 1.1规范(关于带有查询参数的GET和HEAD请求)规定:缓存在没有服务器提供明确的过期时间时,不得将对这些URI的响应视为新鲜。详见http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9 - Michael Johnson
4
我尝试过使用查询字符串类型的版本控制,并在主要浏览器中测试,无论是否符合规范,它们都会缓存文件。但是,我认为最好还是使用 style.TIMESTAMP.css 格式来避免滥用查询字符串,因为仍有可能缓存代理软件不会缓存该文件。 - Tomas Andrle
36
需要翻译的内容:Worth noting, for whatever reason, that Stackoverflow itself uses the query string method.值得注意的是,出于某种原因,Stackoverflow本身使用查询字符串方法。 - jason
2
已经验证,使用?=参数不会在参数更改时使浏览器重新获取缓存文件。唯一的方法是通过服务器端编程方式更改文件名,正如Kip所回答的那样。 - arunskrish
@boscharun:尝试使用 ?v=parameter 参数。 - Marcel Burkhard
显示剩余4条评论

43

3
谢谢,我想这又是一个讨论过我的想法的案例,只是我不知道它被称为什么,所以在谷歌搜索中找不到它。 - Kip

34

现有的30多个答案都是关于2008年左右的网站的好建议。但是,当涉及到现代的单页应用程序(SPA)时,可能是时候重新思考一些基本假设了……特别是只为了让Web服务器仅提供单个、最新版本的文件这一想法是否可取。

想象一下,你是一个已经加载了SPA版本M的用户:

  1. 您的CD管道将新版本N的应用程序部署到服务器上
  2. 您在SPA中导航,它向服务器发送XMLHttpRequest(XHR)以获取/some.template
  • (您的浏览器没有刷新页面,因此仍在运行版本M
  1. 服务器返回 /some.template 的内容 - 您想要它返回模板的版本 M 还是版本 N

如果版本 M 和版本 N 之间的格式发生了变化(或文件被重命名等),您可能不希望将版本 N 的模板发送到运行旧版本 M 解析器的浏览器中。

当 Web 应用程序满足两个条件时,就会遇到这个问题:

  • 在初始页面加载后一段时间内异步请求资源
  • 应用程序逻辑假设资源内容的某些方面(可能在未来版本中更改)

一旦您的应用程序需要同时提供多个版本,解决缓存和“重新加载”问题就变得简单:

  1. 将所有站点文件安装到版本化目录中:/v<release_tag_1>/…files…/v<release_tag_2>/…files…
  2. 设置 HTTP 标头以让浏览器永久缓存文件
  • (或者更好的方法是将所有内容放在CDN上)
  1. 将所有的 <script><link> 标签等更新到指向版本目录中的文件

最后一步听起来很棘手,因为它可能需要针对服务器端或客户端代码中的每个URL调用一个URL构建器。或者你可以巧妙地使用 <base> 标签,并在一个地方更改当前版本。

† 解决此问题的一种方法是在发布新版本时强制浏览器重新加载所有内容。但为了让任何正在进行的操作完成,最好仍然支持至少两个平行版本:v-current 和 v-previous。


Michael - 你的评论非常相关。我来到这里就是为了寻找我的SPA的解决方案。我得到了一些指针,但最终还是不得不自己想出一个解决方案。最后,我对我所想出的东西感到非常满意,所以我写了一篇博客文章和对这个问题的答案(包括代码)。感谢你的指引。 - statler
很好的评论。我不明白为什么人们在谈论缓存破坏和HTTP缓存作为解决网站缓存问题的真正方法时,没有评论单页应用程序的新问题,好像这只是个边角案例。 - David Casillas
2
出色的回应和绝对理想的策略!提到base标签还有附加分!至于支持旧代码:这并不总是可能的,也不总是一个好主意。新版本的代码可能支持其他应用程序部分的重大变更,或涉及紧急修复、漏洞修补等。我自己尚未实施此策略,但一直认为整体架构应允许将旧版本标记为“过时”,并在下次进行异步调用时强制重新加载(或通过WebSockets强制取消所有会话的身份验证)。 - Jonny Asmar
很高兴看到关于单页应用程序的深思熟虑的回答。 - Nate I
如果你想搜索更多信息,那就是“蓝绿部署”。 - Fil

18

Laravel (PHP)中,我们可以使用以下简明优美的方式(使用文件修改时间戳):

<script src="{{ asset('/js/your.js?v='.filemtime('js/your.js')) }}"></script>

同样适用于CSS

<link rel="stylesheet" href="{{asset('css/your.css?v='.filemtime('css/your.css'))}}">

示例HTML输出(filemtime返回时间作为Unix时间戳)

<link rel="stylesheet" href="assets/css/your.css?v=1577772366">

这个命令在HTML中的输出是什么?如果我只想更新像?v=3、?v=4等版本,该怎么办?- 不会强制浏览器在用户进入网站时每次加载CSS。 - Gediminas Šukys
filemtime:"此函数返回文件数据块被写入时的时间,即文件内容被更改的时间。"来源:http://php.net/manual/en/function.filemtime.php - Kamil Kiełczewski

16

1
首先,浏览器不缓存,这是HTTP的功能。为什么HTTP会关心URI的结构呢?有没有官方参考规范表明HTTP缓存应理解URI的语义,以便不缓存带有查询字符串的项目? - AnthonyWJones
14
一个包含缓存对象功能的网络浏览器(可检查您的浏览器缓存目录)。HTTP是一种协议,其中服务器向客户端(代理、浏览器、网络爬虫等)发出指令以建议缓存控制。 - tzot
thinkvitamin.com的链接已经失效了(域名似乎存在,但没有任何响应)。 - Peter Mortensen
文章的存档副本:https://web.archive.org/web/20060523204906/http://www.thinkvitamin.com/features/webapps/serving-javascript-fast,发布于2006年5月。但是根据这里的答案 https://dev59.com/vHVD5IYBdhLWcg3wHnsX#85386,有关Opera和Safari不缓存的说法是错误的。但我们更感兴趣的是,浏览器是否在看到不同的查询参数时破坏其缓存(大多数浏览器)(2021年)。 - Harry Wood
@AnthonyWJones,那显然是错误的。即使在各种想象的缓存关闭情况下,Brave仍会缓存文件。你不知道我为了打破Brave的缓存而费了多大的劲。如果你在JavaScript文件中使用import,缓存是根本无法关闭的。唯一的解决方案是更改文件名,这样在开发工具中每次都会销毁所有调试断点。 - SO_fix_the_vote_sorting_bug

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