首页 文章

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

提问于
浏览
896

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

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

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

Update:

在这里讨论了一段时间后,我发现 John Millikinda5id 的建议很有用 . 事实证明,有一个术语: auto-versioning .

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

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

由于不清楚伪造的查询字符串会发生什么,我不接受这个答案 .

30 回答

  • 8

    我听说这叫做“自动版本控制” . 最常见的方法是在URL中的某处包含静态文件的mtime,并使用重写处理程序或URL confs将其删除:

    也可以看看:

  • 2

    Simple Client-side Technique

    一般来说,缓存很好..所以有几种技术,取决于你是在为开发网站时自己解决问题,还是在尝试控制 生产环境 环境中的缓存 .

    您网站的一般访问者将无法获得与您开发网站时相同的体验 . 由于普通访问者访问网站的频率较低(可能每月只有几次,除非你是谷歌或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的“高性能网站” . 这是对这个主题的一个非常好的介绍 .

  • 5

    你可以简单地用CSS / JS url添加一些随机数

    example.css?randomNo=Math.random()
    
  • 57

    没有找到客户端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>
    
  • 2

    我在其URL中放置了文件内容的MD5哈希值 . 这样我就可以设置一个很长的截止日期,而不必担心用户使用旧的JS或CSS .

    我还在运行时(或文件系统更改)每个文件计算一次,因此在设计时或构建过程中没有什么好笑的 .

    如果您正在使用ASP.NET MVC,那么您可以查看代码in my other answer here .

  • 5

    似乎这里的所有答案都提出了命名方案中的某种版本控制,这有其缺点 .

    浏览器应该很清楚要通过阅读缓存什么以及不缓存什么Web服务器响应,特别是http标头 - 该资源有效期多长?自从我上次检索以来,这个资源是否已更新?等等 .

    如果事情“正确”配置,只需更新应用程序的文件(在某些时候)刷新浏览器缓存 . 例如,您可以配置Web服务器以告知浏览器永远不会缓存文件(这是一个坏主意) .

    有关其工作原理的更深入解释,请点击此处https://www.mnot.net/cache_docs/#WORK

  • 5

    Update: 重写以包含来自 John Millikinda5id 的建议 . 此解决方案是用PHP编写的,但应该很容易适应其他语言 .

    Update 2: 合并来自 Nick Johnson 的注释,原始 .htaccess 正则表达式可能会导致 json-1.3.js 等文件出现问题 . 解决方案是只在末尾有10个数字时才重写 . (因为10位数字涵盖了从9/9/2001到11/20/2286的所有时间戳 . )

    首先,我们在.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,因此它不会使用缓存副本 .

    这也适用于图像,favicon和JavaScript . 基本上任何不动态生成的东西 .

  • 2

    假设您有一个文件可用于:

    /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');
    }
    

    它的URL实际上是:

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

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

    我已经使用Java servlet实现了一种使用此技术的缓存解决方案,并使用委托给底层资源(即 /styles/screen.css )的servlet简单地处理对 /v/* 的请求 . 在开发模式中,我设置了缓存标头,告诉客户端始终使用服务器检查资源的新鲜度(如果您委托给Tomcat的 DefaultServlet 并且 .css.js 等文件没有更改,这通常会导致304)在部署模式下,我设置了 Headers "cache forever" .

  • 10

    我最近用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文件会自动触发缓存失效 . 工作非常好,开销不明显 .

  • 7

    对于ASP.NET 4.5及更高版本,您可以使用script bundling .

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

    捆绑还有其他好处,包括首次加载页面时提高性能 .

  • 2

    对于Java Servlet环境,您可以查看Jawr library . 功能页面解释了它如何处理缓存:

    Jawr将尽力强迫您的客户缓存资源 . 如果浏览器询问文件是否发生更改,则会返回304(未修改)标头而不显示内容 . 另一方面,使用Jawr,您将100%确定所有客户端都下载了新版本的捆绑包 . 资源的每个URL都将包含一个自动生成的基于内容的前缀,该前缀会在更新resurce时自动更改 . 部署新版本后,捆绑包的URL也会发生变化,因此客户端将无法使用较旧的缓存版本 .

    该库也可以进行js / css缩小,但如果你不想要它,可以关闭它 .

  • 4

    我正在添加这个答案作为SilverStripe http://www.silverstripe.org我正在寻找的具体答案从来没有找到,但已经从阅读中找到了:http://api.silverstripe.org/3.0/source-class-SS_Datetime.html#98-110

    希望这将有助于某人使用SilverStripe模板并尝试强制在每个页面访问/刷新时重新加载缓存的图像 . 在我的情况下,它是一个gif动画,只播放一次,因此在缓存后没有重放 . 在我的模板中,我只是添加:

    ?$Now.Format(dmYHis)
    

    到文件路径的末尾以创建唯一的时间戳并强制浏览器将其视为新文件 .

  • 21

    对于ASP.NET,我认为下一个解决方案具有高级选项(调试/发布模式,版本):

    通过这种方式包含的Js或Css文件:

    <script type="text/javascript" src="Scripts/exampleScript<%=Global.JsPostfix%>" />
    <link rel="stylesheet" type="text/css" href="Css/exampleCss<%=Global.CssPostfix%>" />
    

    Global.JsPostfix和Global.CssPostfix在Global.asax中通过以下方式计算:

    protected void Application_Start(object sender, EventArgs e)
    {
        ...
        string jsVersion = ConfigurationManager.AppSettings["JsVersion"];
        bool updateEveryAppStart = Convert.ToBoolean(ConfigurationManager.AppSettings["UpdateJsEveryAppStart"]);
        int buildNumber = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Revision;
        JsPostfix = "";
    #if !DEBUG
        JsPostfix += ".min";
    #endif      
        JsPostfix += ".js?" + jsVersion + "_" + buildNumber;
        if (updateEveryAppStart)
        {
            Random rand = new Random();
            JsPosfix += "_" + rand.Next();
        }
        ...
    }
    
  • 5

    在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'))}}">
    
  • 2

    感谢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;
                }
            }
        }
    
    }
    

    干杯谢谢 .

  • 6

    如果您使用的是现代浏览器,则可以使用清单文件通知浏览器需要更新哪些文件 . 这不需要 Headers ,网址中没有版本......

    有关更多详细信息,请参阅:请参阅:https://developer.mozilla.org/nl/docs/Web/HTML/Applicatie_cache_gebruiken#Introduction

  • 4

    您可以将 ?foo=1234 放在css / js导入的末尾,将1234更改为您喜欢的任何内容 . 看一下SO html源代码示例 .

    那个想法是什么?无论如何,请求都会丢弃/忽略参数,您可以在推出新版本时更改该数字 .


    Note: 关于这究竟如何影响缓存存在一些争论 . 我相信它的一般要点是有或没有参数的GET请求应该是可缓存的,因此上述解决方案应该可行 .

    但是,Web服务器决定是否要遵守规范的那一部分以及用户使用的浏览器,因为它可以直接进入并要求提供新版本 .

  • 111

    Google的mod_pagespeed apache插件将为您自动进行版本控制 . 这真的很光滑 .

    它解析HTML从Web服务器出来(使用PHP,rails,python,静态HTML - 任何东西)并重写CSS,JS,图像文件的链接,因此它们包含id代码 . 它在修改后的URL上提供文件,并对它们进行非常长的缓存控制 . 当文件发生变化时,它会自动更改URL,以便浏览器重新获取它们 . 它基本上只是工作,没有任何代码更改 . 它甚至会在出路时缩小你的代码 .

  • 2

    这是一个纯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 .

  • 8

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

  • 14

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

    <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>
    
  • 4

    不知道为什么你们为实施这个解决方案付出了太多的痛苦 .

    如果获取文件的修改时间戳并将其作为查询字符串附加到文件,则需要执行的操作

    在PHP中我会这样做:

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

    filemtime是一个返回文件修改时间戳的PHP函数 .

  • 51

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

    https://developer.chrome.com/devtools/docs/tips-and-tricks#hard-reload

    打开开发人员工具后,只需长按一下刷新按钮,然后将鼠标悬停在“清空缓存和硬重新加载”上即可释放 .

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

  • 9

    RewriteRule需要对js或css文件进行小的更新,这些文件最后包含点符号版本控制 . 例如 . JSON-1.3.js .

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

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

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

    所以你的网址会是这样的

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

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

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

    每次CSS更改时,只需运行该脚本即可 . 浏览器只会在更改文件时重新加载 . 如果您进行编辑然后撤消它,那么您无需确定需要返回哪个版本才能让访问者不重新下载 .

  • 38

    对于大约2008年的网站,30个左右的现有答案是很好的建议 . 然而,当谈到现代的时候,可能是重新思考一些基本假设的时候了......特别是Web服务器只需要提供单个最新版本的文件的想法 .

    想象一下,您是一个在浏览器中加载了SPA版本M的用户:

    • 您的CD管道将应用程序的新版本N部署到服务器上

    • 您在SPA中导航,它将XHR发送到服务器以获取 /some.template

    • (您的浏览器还没有't refreshed the page, so you'仍在运行版本M)

    • 服务器以 /some.template 的内容响应 - 您是否希望它返回模板的版本M或N?

    如果 /some.template 的格式在版本M和N之间发生了更改(或者文件已重命名或其他) you probably don't want version N of the template sent to the browser that's running the old version M of the parser . †

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

    • 在初始页面加载后的某个时间异步请求资源

    • 应用程序逻辑假设有关资源内容的事情(可能在将来的版本中有所改变)

    一旦您的应用需要并行提供多个版本, solving caching and "reloading" becomes trivial:

    • 将所有站点文件安装到版本化的目录中: /v<release_tag_1>/…files…/v<release_tag_2>/…files…

    • 设置HTTP标头,让浏览器永久缓存文件

    • (或者更好的是,将所有内容都放在CDN中)

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

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

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

  • 90

    有趣的帖子 . 阅读完这里的所有答案并结合我从未遇到过“伪造”查询字符串的任何问题(我不确定为什么每个人都不愿意使用这个)我猜这个解决方案(这消除了对apache重写规则的需求)如在接受的答案中)是计算CSS文件内容的短HASH(而不是文件datetime)作为伪造的查询字符串 .

    这将导致以下结果:

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

    当然,日期时间解决方案也可以在编辑CSS文件的情况下完成工作,但我认为这是关于css文件内容而不是文件日期时间,那么为什么要将这些混合起来?

  • 173

    谷歌浏览器有 Hard Reload 以及 Empty Cache and Hard Reload 选项 . 您可以单击并按住重新加载按钮(在检查模式下)以选择一个 .

  • 425

    我建议实施以下过程:

    在部署时

    • 版本你的css / js文件,例如:screen.1233.css(如果你使用版本控制系统,这个数字可以是你的SVN版本)

    • 缩小它们以优化加载时间

  • 5

    很抱歉带回了一个死线程 .

    @ TomA是对的 .

    使用"querystring"方法不会被下面的Steve Souders引用缓存:

    ... Squid是一种流行的代理,它不会使用查询字符串来缓存资源 .

    @ TomA使用style.TIMESTAMP.css的建议很好,但MD5会好得多,因为只有当内容真正改变时,MD5才会改变 .

相关问题