我注意到一些浏览器(特别是Firefox和Opera)非常热衷于使用.css和.js文件的缓存副本,甚至在浏览器会话之间。当您更新其中一个文件时,这会导致一个问题,但用户的浏览器会继续使用缓存的副本。

当文件发生更改时,强迫用户浏览器重新加载文件的最优雅的方法是什么?

理想情况下,该解决方案不会强制浏览器在每次访问页面时重新加载文件。


我发现John Millikin和da5id的建议很有用。这有一个专门的术语:自动版本控制。

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

SCdF建议的另一个想法是将伪查询字符串附加到文件中。(一些自动使用时间戳作为伪查询字符串的Python代码是由pi..提交的)

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


当前回答

我在为我的SPA寻找解决方案时遇到了这个问题,它只有一个列出所有必要文件的index.html文件。虽然我得到了一些帮助我的线索,但我找不到一个快速而简单的解决方案。

最后,我编写了一个快速页面(包括所有代码)来自动版本HTML/JavaScript index.html文件,作为发布过程的一部分。它工作完美,只更新新文件根据日期最后修改。

你可以在Autoversion your SPA index.html上看到我的文章。还有一个独立的Windows应用程序。

代码的核心是:

private void ParseIndex(string inFile, string addPath, string outFile)
{
    string path = Path.GetDirectoryName(inFile);
    HtmlAgilityPack.HtmlDocument document = new HtmlAgilityPack.HtmlDocument();
    document.Load(inFile);

    foreach (HtmlNode link in document.DocumentNode.Descendants("script"))
    {
        if (link.Attributes["src"]!=null)
        {
            resetQueryString(path, addPath, link, "src");
        }
    }

    foreach (HtmlNode link in document.DocumentNode.Descendants("link"))
    {
        if (link.Attributes["href"] != null && link.Attributes["type"] != null)
        {
            if (link.Attributes["type"].Value == "text/css" || link.Attributes["type"].Value == "text/html")
            {
                resetQueryString(path, addPath, link, "href");
            }
        }
    }

    document.Save(outFile);
    MessageBox.Show("Your file has been processed.", "Autoversion complete");
}

private void resetQueryString(string path, string addPath, HtmlNode link, string attrType)
{
    string currFileName = link.Attributes[attrType].Value;

    string uripath = currFileName;
    if (currFileName.Contains('?'))
        uripath = currFileName.Substring(0, currFileName.IndexOf('?'));
    string baseFile = Path.Combine(path, uripath);
    if (!File.Exists(baseFile))
        baseFile = Path.Combine(addPath, uripath);
    if (!File.Exists(baseFile))
        return;
    DateTime lastModified = System.IO.File.GetLastWriteTime(baseFile);
    link.Attributes[attrType].Value = uripath + "?v=" + lastModified.ToString("yyyyMMddhhmm");
}

其他回答

我在为我的SPA寻找解决方案时遇到了这个问题,它只有一个列出所有必要文件的index.html文件。虽然我得到了一些帮助我的线索,但我找不到一个快速而简单的解决方案。

最后,我编写了一个快速页面(包括所有代码)来自动版本HTML/JavaScript index.html文件,作为发布过程的一部分。它工作完美,只更新新文件根据日期最后修改。

你可以在Autoversion your SPA index.html上看到我的文章。还有一个独立的Windows应用程序。

代码的核心是:

private void ParseIndex(string inFile, string addPath, string outFile)
{
    string path = Path.GetDirectoryName(inFile);
    HtmlAgilityPack.HtmlDocument document = new HtmlAgilityPack.HtmlDocument();
    document.Load(inFile);

    foreach (HtmlNode link in document.DocumentNode.Descendants("script"))
    {
        if (link.Attributes["src"]!=null)
        {
            resetQueryString(path, addPath, link, "src");
        }
    }

    foreach (HtmlNode link in document.DocumentNode.Descendants("link"))
    {
        if (link.Attributes["href"] != null && link.Attributes["type"] != null)
        {
            if (link.Attributes["type"].Value == "text/css" || link.Attributes["type"].Value == "text/html")
            {
                resetQueryString(path, addPath, link, "href");
            }
        }
    }

    document.Save(outFile);
    MessageBox.Show("Your file has been processed.", "Autoversion complete");
}

private void resetQueryString(string path, string addPath, HtmlNode link, string attrType)
{
    string currFileName = link.Attributes[attrType].Value;

    string uripath = currFileName;
    if (currFileName.Contains('?'))
        uripath = currFileName.Substring(0, currFileName.IndexOf('?'));
    string baseFile = Path.Combine(path, uripath);
    if (!File.Exists(baseFile))
        baseFile = Path.Combine(addPath, uripath);
    if (!File.Exists(baseFile))
        return;
    DateTime lastModified = System.IO.File.GetLastWriteTime(baseFile);
    link.Attributes[attrType].Value = uripath + "?v=" + lastModified.ToString("yyyyMMddhhmm");
}

我建议您使用实际CSS文件的MD5散列,而不是手动更改版本。

URL应该是这样的

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

您仍然可以使用重写规则来去除散列,但优点是现在您可以将缓存策略设置为“永远缓存”,因为如果URL相同,这意味着文件没有改变。

然后,您可以编写一个简单的shell脚本来计算文件的散列并更新标记(您可能希望将其移动到一个单独的文件中进行包含)。

只要在CSS每次更改时运行该脚本就可以了。浏览器只会在文件被修改时重新加载。如果你做了一个编辑,然后撤销它,为了让你的访问者不重新下载,你不需要确定需要返回到哪个版本。

有趣的文章。阅读了这里所有的答案,并结合我从未遇到过任何“伪”查询字符串问题的事实(我不确定为什么每个人都不愿意使用这个),我猜解决方案(它消除了Apache重写规则的需要,因为在接受的答案)是计算CSS文件内容的短哈希(而不是文件日期时间)作为伪查询字符串。

这将导致以下结果:

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

当然,datetime解决方案也可以在编辑CSS文件的情况下完成工作,但我认为这是关于CSS文件的内容,而不是关于文件datetime,所以为什么要把这些混为一谈呢?

这个解决方案是用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.  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" />

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

这也适用于图像、favicons和JavaScript。基本上任何不是动态生成的东西。

您可以使用SRI来破坏浏览器缓存。你只需要每次都用新的SRI哈希更新index.html文件。当浏览器加载HTML并发现HTML页面上的SRI散列与资源的缓存版本不匹配时,它将从服务器重新加载资源。它还带来了绕过跨起点读阻塞的良好副作用。

<script src="https://jessietessie.github.io/google-translate-token-generator/google_translate_token_generator.js" integrity="sha384-muTMBCWlaLhgTXLmflAEQVaaGwxYe1DYIf2fGdRkaAQeb4Usma/kqRWFWErr2BSi" crossorigin="anonymous"></script>