如何强制浏览器重新加载缓存的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个回答

15

这里是一个纯JavaScript的解决方案。

(function(){

    // Match this timestamp with the release of your code
    var lastVersioning = Date.UTC(2014, 11, 20, 2, 15, 10);
 
    var lastCacheDateTime = localStorage.getItem('lastCacheDatetime');

    if(lastCacheDateTime){
        if(lastVersioning > lastCacheDateTime){
            var reload = true;
        }
    }

    localStorage.setItem('lastCacheDatetime', Date.now());

    if(reload){
        location.reload(true);
    }

})();

上述代码将查找用户上次访问您的站点的时间。如果上次访问是在您发布新代码之前,它将使用location.reload(true)来从服务器强制刷新页面。

通常我将这个代码放在<head>标签的最前面,以便在加载任何其他内容之前先执行此代码。如果需要重新加载页面,用户几乎不会察觉到。

我正在使用本地存储在浏览器中存储上次访问的时间戳,但如果您想支持旧版本的IE,可以将cookie添加到混合中。


1
我尝试过类似这样的方法,这只能在重新加载页面时起作用,但如果网站有多个页面共享相同的CSS /图像,则其他页面仍将使用旧资源。 - DeepBlue
愚蠢的问题:我能否将这段代码直接注入到我的非常基本的静态HTML页面中? - David.P

12

对于以点记号版本结尾的JavaScript或CSS文件,RewriteRule需要进行小型更新。例如json-1.3.js

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

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

2
感谢您的输入!自从我写了这篇文章以来,我也遇到了这个问题。我的解决方案是只有在文件名的最后一部分恰好包含十个数字时才进行重写。(10个数字涵盖了从2001年9月9日到2286年11月20日的所有时间戳。)我已经更新了我的答案,包括这个正则表达式:^(.*)\.[\d]{10}\.(css|js)$ $1.$2 - Kip
我理解正则表达式,但我不明白你在这里用 [^.] 解决了什么问题。此外,在字符类中写入 \d 没有任何好处 -- \d+ 可以做同样的事情。按照发布的方式,您的模式将匹配任意数量的字符(贪婪地),然后是一个字面点、一个非点、一个或多个数字、一个点、cssjs,然后是文件名的结尾。无法匹配您的示例输入:https://regex101.com/r/RPGC62/1 - mickmackusa

11

对于 ASP.NET 4.5 及更高版本,您可以使用 脚本捆绑

请求 http://localhost/MvcBM_time/bundles/AllMyScripts?v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81 是针对 AllMyScripts 捆绑包的,并包含一个查询字符串对 v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81。查询字符串 v 具有一个值标记,该标记是用于缓存的唯一标识符。只要包不改变,ASP.NET 应用程序就会使用此标记请求 AllMyScripts 捆绑包。如果捆绑包中的任何文件发生更改,则 ASP.NET 优化框架将生成新的标记,确保浏览器请求获取最新的捆绑包。

捆绑还具有其他好处,包括在首次页面加载时进行缩小,从而提高性能。


11

很有趣的帖子。阅读了这里所有的回答,再加上我从来没有遇到过任何关于“虚假”查询字符串的问题(我不确定为什么每个人都如此不愿使用它),我猜测解决方案(可以避免使用Apache重写规则,就像被接受的答案中所述)是计算CSS文件内容的短哈希值(而不是文件日期时间)作为虚假的查询字符串。

这将导致以下结果:

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

当然,对于编辑CSS文件,日期时间解决方案也能够胜任,但我认为这与CSS文件内容有关,而不是文件的日期时间,所以为什么要混淆这些呢?


9

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

https://superuser.com/a/512833

打开开发者工具,只需长按刷新按钮,当您悬停在“清空缓存并硬刷新”上时松手即可。

这是我的好朋友,也是轻量级获取您想要的东西的超级方法!


如果您将Chrome作为开发环境的话,另一个非侵入式的解决方案是禁用缓存:在设置齿轮下,选择“禁用缓存”可以使磁盘缓存失效(注意:DevTools必须可见/打开才能起作用)。 - Velojet
1
什么是“长按”? - Peter Mortensen
该链接 (实际上) 已经失效。它重定向到通用页面 "Chrome DevTools" - https://developers.google.com/web/tools/chrome-devtools/。 - Peter Mortensen
@PeterMortensen 当您单击并按住鼠标按钮时。 - Frank Bryce
为什么不直接按Ctrl+F5呢? - Alex78191

8

我没有找到在客户端DOM中动态创建脚本节点(或CSS)元素的方法:

<script>
    var node = document.createElement("script");
    node.type = "text/javascript";
    node.src = 'test.js?' + Math.floor(Math.random()*999999999);
    document.getElementsByTagName("head")[0].appendChild(node);
</script>

那么你找到了什么?你能不能更清楚地表达一下?最好通过编辑你的回答来实现(但不要在这里添加“编辑:”,“更新:”或类似的内容)。 - Peter Mortensen

8

感谢 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 https://dev59.com/tnVD5IYBdhLWcg3wAWoO
 *
 * 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;
            }
        }
    }
}

7
你可以在CSS和JavaScript的URL后面添加一些随机数字,例如:
example.css?randomNo = Math.random()

7

谷歌浏览器有“硬刷新”和“清空缓存并硬刷新”选项。您可以在“检查模式”下单击并长按刷新按钮来选择其中一个。


1
澄清一下,“检查模式”是指“开发工具”,也称为F12,ctrl+shift+i,ant菜单>更多工具>开发者工具”,以及右键单击>检查元素”。在开发工具中还有一个设置(我忘记了位置),可以在每次重新加载时进行硬重载。 - Jonny Asmar

7

最近我用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 %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文件即可自动触发缓存失效。它运作得非常好,而且开销几乎不可感知。

os.stat()会成为瓶颈吗? - hoju
@Richard状态可能会成为瓶颈,如果磁盘非常慢且请求非常多。在这种情况下,您可以将时间戳缓存到内存中的某个位置,并在每次新部署时清除此缓存。然而,在大多数用例中,这种复杂性是不必要的。 - pi.
我知道这已经很古老了,但是对于任何人来说,时间戳太过激进了。这意味着你根本没有任何缓存,如果你想要缓存,可以通过静态文件的自定义头来管理。 - LarryBud
2
@LarryBud:这是文件的时间戳,而不是当前时间戳。你肯定会有缓存。 - pi.

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