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

时间:2020-03-06 14:34:42  来源:igfitidea点击:

我注意到一些浏览器(特别是Firefox和Opera)非常热衷于使用.css和.js文件的缓存副本,即使在浏览器会话之间也是如此。当我们更新这些文件之一而用户的浏览器继续使用缓存的副本时,这会导致出现问题。

问题是:强迫用户的浏览器在文件更改后重新加载文件的最优雅的方法是什么?

理想情况下,该解决方案不会强制浏览器在每次访问页面时重新加载文件。我将发布自己的解决方案作为答案,但我很好奇是否有人有更好的解决方案,我将让我们决定。

更新:

经过一段时间的讨论后,我发现John Millikin和da5id的建议很有用。事实证明有一个术语:自动版本化。

我在下面发布了一个新答案,该答案是我原来的解决方案和约翰的建议的结合。

SCdF建议的另一个想法是将伪造的查询字符串添加到文件中。 (一些由pi提交的将时间戳自动用作伪造查询字符串的Python代码。)但是,关于浏览器是否将使用查询字符串缓存文件存在一些讨论。 (请记住,我们希望浏览器缓存文件并在以后的访问中使用它。我们只希望它在更改后再次获取文件。)

由于尚不清楚假查询字符串会发生什么,因此我不接受该答案。

解决方案

更新:重写以合并John Millikin和da5id的建议。该解决方案是用PHP编写的,但应易于适应其他语言。

更新2:结合尼克·约翰逊(Nick Johnson)的评论,即原始的.htaccess正则表达式会导致诸如json-1.3.js之类的文件出现问题。解决方案是仅在末尾恰好有10位数字时才重写。 (因为10位数字涵盖了从9/9/2001到11/20/2286的所有时间戳。)

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

RewriteEngine on
RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ . [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.  Must be an absolute path (i.e.
 *                starting with slash).
 */
function auto_version($file)
{
  if(strpos($file, '/') !== 0 || !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" />

这样,我们无需再次修改link标记,并且用户将始终看到最新的CSS。浏览器将能够缓存CSS文件,但是当我们对CSS进行任何更改时,浏览器会将其视为新的URL,因此它将不使用缓存的副本。

这也可以与图像,图标和JavaScript一起使用。基本上任何不是动态生成的。

我们可以将?foo = 1234放在css / js导入的末尾,将1234更改为我们喜欢的任何值。请看一下SO html源代码的示例。

有那个想法吗?无论如何,这些参数都会在请求中被丢弃/忽略,我们可以在推出新版本时更改该数字。

注意:关于它如何影响缓存,存在一些争论。我认为一般的要点是,无论是否带有参数的GET请求都应该是可缓存的,因此上述解决方案应该可以工作。

但是,由Web服务器来决定是否要遵守该部分规范以及用户使用的浏览器,这是因为它可以继续前进并始终要求提供新版本。

我听说这叫做"自动版本控制"。最常见的方法是将静态文件的mtime包含在URL中的某个位置,然后使用重写处理程序或者URL conf将其删除:

也可以看看:

  • Django中的自动资产版本控制
  • 自动版本化CSS和JavaScript文件

如果将session-id添加为js / css文件的适当参数,则可以强制执行"整个会话范围的缓存":

<link rel="stylesheet" src="myStyles.css?ABCDEF12345sessionID" />
<script language="javascript" src="myCode.js?ABCDEF12345sessionID"></script>

如果要使用版本范围的缓存,可以添加一些代码以打印文件日期或者类似日期。如果我们使用的是Java,则可以使用自定义标签以一种优雅的方式生成链接。

<link rel="stylesheet" src="myStyles.css?20080922_1020" />
<script language="javascript" src="myCode.js?20080922_1120"></script>

简单的客户端技术

通常,缓存是好的。.因此,有两种技术,取决于我们是在开发网站时为自己解决问题还是在生产环境中尝试控制缓存。

网站的一般访问者不会拥有与开发网站时相同的体验。由于一般的访问者访问该网站的频率较低(除非我们是Google或者hi5网络,否则每个月访问几次),因此他们将文件缓存在其中的可能性较小,这就足够了。如果要将新版本强加到浏览器中,则始终可以向请求中添加查询字符串,并在进行重大更改时提高版本号:

<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标头,但是我上面编写的小技巧可能是解决问题的一种简单得多的方法。由于缓存很好,因此我们通常希望将日期设置为较远的日期(" Far-future Expires Header"),并使用上述技术来强制进行更改。

如果我们对HTTP的更多信息或者如何发出这些请求感兴趣,那么一本好书就是Steve Souders的" High Performance Web Sites"。这是对该主题的很好的介绍。

更改文件名将起作用。但这通常不是最简单的解决方案。

我们已经注意到," no-cache"的HTTP缓存控制标头并不总是有效。 HTTP 1.1规范允许用户代理摆动空间来决定是否请求新副本。 (如果只看指令的名称,这是不直观的。请阅读有关缓存的实际HTTP 1.1规范...在上下文中它更有意义。)

简而言之,如果我们想使用严格的缓存控制

Cache-Control: no-cache, no-store, must-revalidate

在响应标题中。

不要使用foo.css?version = 1!浏览器不应该缓存带有GET变量的URL。根据http://www.thinkvitamin.com/features/webapps/serving-javascript-fast,尽管IE和Firefox忽略了这一点,Opera和Safari却没有!相反,请使用foo.v1234.css,并使用重写规则删除版本号。

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

因此,网址将类似于

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

我们仍然可以使用重写规则来去除哈希,但是好处是现在我们可以将缓存策略设置为"永远缓存",因为如果URL相同,则意味着文件未更改。

然后,我们可以编写一个简单的Shell脚本,该脚本将计算文件的哈希值并更新标签(我们可能希望将其移动到单独的文件中以包含在内)。

每次CSS更改时,只需运行该脚本即可,一切都很好。更改后,浏览器将仅重新加载文件。如果我们进行了修改然后撤消了修改,则无需费心找出要返回到哪个版本,以免访问者重新下载。

我最近使用Python解决了这个问题。这里的代码(应该很容易被其他语言采用):

def import_tag(pattern, name, **kw):
    if name[0] == "/":
        name = name[1:]
    # Additional HTML attributes
    attrs = ' '.join(['%s="%s"' % item for item in kw.items()])
    try:
        # Get the files modification time
        mtime = os.stat(os.path.join('/documentroot', name)).st_mtime
        include = "%s?%d" % (name, mtime)
        # this is the same as sprintf(pattern, attrs, include) in other
        # languages
        return pattern % (attrs, include)
    except:
        # In case of error return the include without the added query
        # parameter.
        return pattern % (attrs, name)

def script(name, **kw):
    return import_tag("""<script type="text/javascript" """ +\
        """ %s src="/%s"></script>""", name, **kw)

def stylesheet(name, **kw):
    return import_tag('<link rel="stylesheet" type="text/css" ' +\
        """%s href="/%s">', name, **kw)

此代码基本上将文件时间戳作为查询参数添加到URL。调用以下函数

script("/main.css")

将导致

<link rel="stylesheet" type="text/css"  href="/main.css?1221842734">

当然,这样做的好处是我们不必再更改html,触摸CSS文件将自动触发缓存失效。效果非常好,开销也不明显。

假设我们有一个可用的文件:

/styles/screen.css

我们可以将带有版本信息的查询参数添加到URI上,例如:

/styles/screen.css?v=1234

或者我们可以在版本信息前添加例如:

/v/1234/styles/screen.css

恕我直言,第二种方法更适合CSS文件,因为它们可以使用相对URL引用图像,这意味着如果我们指定background-image,如下所示:

body {
    background-image: url('images/happy.gif');
}

其网址将有效地是:

/v/1234/styles/images/happy.gif

这意味着,如果我们更新使用的版本号,则服务器会将其视为新资源,而不使用缓存的版本。如果版本号基于Subversion / CVS / etc。修订版,这意味着将注意到CSS文件中引用的图像更改。第一种方案无法保证这一点,即相对于/styles/screen.css?v = 1235的URLimages / happy.gif/ styles / images / happy.gif。任何版本信息。

我已经使用Java Servlet使用此技术实现了一个缓存解决方案,并仅使用委派给基础资源的Servlet处理对" / v / *"的请求(即" /styles/screen.css")。在开发模式下,我设置了缓存头,告诉客户端始终与服务器一起检查资源的新鲜度(如果将其委托给Tomcat的DefaultServlet和.css,.js等,则通常会导致304. (文件未更改)),而在部署模式下,我设置了标有"永远缓存"的标头。

我建议执行以下过程:

  • 每次部署时都要对css / js文件进行版本控制,例如:screen.1233.css(如果使用版本控制系统,则该数字可以是SVN修订版)
  • 最小化它们以优化加载时间

如果我们使用的是jquery,则有一个名为cache的选项,它将添加一个随机数
我知道这不是一个完整的答案,但它可以为我们节省一些时间

我这样做的方法只是将link元素放入服务器端的include中:

<!--#include virtual="/includes/css-element.txt"-->

css-element.txt的内容在哪里

<link rel="stylesheet" href="mycss.css"/>

因此,只要我们想链接到my-new-css.css或者其他内容,就只需更改包含。

有趣的帖子。在阅读了所有答案之后,再加上我从未遇到过"伪造"查询字符串的任何问题(我不确定为什么每个人都不太愿意使用它),我猜想解决方案(这消除了对Apache重写规则的需求)就像在接受的答案中一样)是将CSS文件内容的简短哈希值(而不是文件日期时间)计算为伪查询字符串。

这将导致以下结果:

<link rel="stylesheet" href="/css/base.css?[hash-here]" type="text/css" />

当然,在编辑CSS文件的情况下,datetime解决方案也能完成工作,但是我认为这与css文件的内容有关,与文件datetime无关,那么为什么要混在一起呢?

RewriteRule需要对js或者css文件进行小的更新,该文件的末尾包含点符号版本。例如。 json-1.3.js。

我在正则表达式中添加了点否定类[^。],因此.number。被忽略。

RewriteRule ^(.*)\.[^.][\d]+\.(css|js)$ . [L]