我注意到一些浏览器(特别是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");
}

其他回答

TomA的回答是对的。

使用“querystring”方法将不会被缓存,Steve Souders引用如下:

...Squid,一个流行的代理,不缓存资源 变量的名称。

TomA建议使用style.TIMESTAMP.css很好,但是MD5会更好,因为只有当内容真正改变时,MD5也会改变。

"Another idea which was suggested by SCdF would be to append a bogus query string to the file. (Some Python code to automatically use the timestamp as a bogus query string was submitted by pi.) However, there is some discussion as to whether or not the browser would cache a file with a query string. (Remember, we want the browser to cache the file and use it on future visits. We only want it to fetch the file again when it has changed.) Since it is not clear what happens with a bogus query string, I am not accepting that answer."

<link rel="stylesheet" href="file.css?<?=hash_hmac('sha1', session_id(), md5_file("file.css")); ?>" />

哈希文件意味着当文件发生变化时,查询字符串也会发生变化。如果没有,它将保持不变。每个会话也强制重新加载。

此外,您还可以使用重写使浏览器认为它是一个新的URI。

对于一个2008年左右的网站来说,这30个左右的答案是很好的建议。然而,当涉及到现代的单页应用程序(SPA)时,可能是时候重新考虑一些基本假设了……特别是web服务器只提供文件的单个最新版本是理想的想法。

假设您是一个用户,浏览器中加载了SPA的M版本:

CD管道将应用程序的新版本N部署到服务器上 您在SPA中导航,它向服务器发送一个XMLHttpRequest (XHR)以获取/some.template

(您的浏览器没有刷新页面,所以您仍然在运行版本M)

服务器返回/some的内容。template -你想让它返回模板的版本M还是N ?

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

Web应用程序在满足两个条件时遇到这个问题:

在初始页面加载后的一段时间内异步请求资源 应用程序逻辑假设有关资源内容的事情(在将来的版本中可能会改变)

一旦你的应用程序需要并行提供多个版本,解决缓存和“重新加载”变得微不足道:

将所有site文件安装到versioned目录:/v<release_tag_1>/…files…,/v<release_tag_2>/…files… 设置HTTP头,让浏览器永远缓存文件

(或者更好的是,把所有东西都放在CDN中)

更新所有<script>和<link>标签等,以指向某个版本目录中的该文件

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

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

对于我的开发,我发现Chrome有一个很好的解决方案。

https://superuser.com/a/512833

随着开发人员工具的打开,只需长按刷新按钮,让放手一旦你悬停在“空缓存和硬加载”。

这是我最好的朋友,是一种超轻量级的方式来得到你想要的!

感谢Kip的完美解决方案!

我对它进行了扩展,将其用作Zend_view_Helper。因为我的客户端在虚拟主机上运行他的页面,我也为此扩展了它。

/**
 * Extend filepath with timestamp to force browser to
 * automatically refresh them if they are updated
 *
 * This is based on Kip's version, but now
 * also works on virtual hosts
 * @link http://stackoverflow.com/questions/118884/what-is-an-elegant-way-to-force-browsers-to-reload-cached-css-js-files
 *
 * Usage:
 * - extend your .htaccess file with
 * # Route for My_View_Helper_AutoRefreshRewriter
 * # which extends files with there timestamp so if these
 * # are updated a automatic refresh should occur
 * # RewriteRule ^(.*)\.[^.][\d]+\.(css|js)$ $1.$2 [L]
 * - then use it in your view script like
 * $this->headLink()->appendStylesheet( $this->autoRefreshRewriter($this->cssPath . 'default.css'));
 *
 */
class My_View_Helper_AutoRefreshRewriter extends Zend_View_Helper_Abstract {

    public function autoRefreshRewriter($filePath) {

        if (strpos($filePath, '/') !== 0) {

            // Path has no leading '/'
            return $filePath;
        } elseif (file_exists($_SERVER['DOCUMENT_ROOT'] . $filePath)) {

            // File exists under normal path
            // so build path based on this
            $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $filePath);
            return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $filePath);
        } else {

            // Fetch directory of index.php file (file from all others are included)
            // and get only the directory
            $indexFilePath = dirname(current(get_included_files()));

            // Check if file exist relativ to index file
            if (file_exists($indexFilePath . $filePath)) {

                // Get timestamp based on this relativ path
                $mtime = filemtime($indexFilePath . $filePath);

                // Write generated timestamp to path
                // but use old path not the relativ one
                return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $filePath);
            } else {
                return $filePath;
            }
        }
    }
}