【问题标题】:How can I force a hard refresh (ctrl+F5)?如何强制硬刷新(ctrl+F5)?
【发布时间】:2010-10-30 12:20:00
【问题描述】:

我们正在积极开发一个使用 .Net 和 MVC 的网站,我们的测试人员正在尝试获取最新的东西进行测试。每次我们修改样式表或外部 javascript 文件时,测试人员都需要进行一次硬刷新(在 IE 中为 ctrl+F5)才能看到最新的内容。

我是否可以强制他们的浏览器获取这些文件的最新版本,而不是让他们依赖缓存的版本?我们不会从 IIS 或其他任何东西中进行任何类型的特殊缓存。

一旦投入生产,就很难告诉客户他们需要硬刷新才能看到最新的更改。

谢谢!

【问题讨论】:

    标签: .net asp.net-mvc http caching


    【解决方案1】:

    我也遇到了这个问题,并找到了我认为非常令人满意的解决方案。

    请注意,使用查询参数.../foo.js?v=1 可能意味着该文件显然不会被某些代理服务器缓存。最好直接修改路径。

    我们需要浏览器在内容更改时强制重新加载。因此,在我编写的代码中,路径包含被引用文件的 MD5 哈希值。如果文件重新发布到 Web 服务器但具有相同的内容,则其 URL 是相同的。更重要的是,使用无限期缓存也是安全的,因为该 URL 的内容永远不会改变。

    此哈希在运行时计算(并缓存在内存中以提高性能),因此无需修改构建过程。事实上,自从将这段代码添加到我的网站后,我就不必考虑太多了。

    您可以在这个网站上看到它的实际效果:Dive Seven - Online Dive Logging for Scuba Divers

    在 CSHTML/ASPX 文件中

    <head>
      @Html.CssImportContent("~/Content/Styles/site.css");
      @Html.ScriptImportContent("~/Content/Styles/site.js");
    </head>
    <img src="@Url.ImageContent("~/Content/Images/site.png")" />
    

    这会生成类似的标记:

    <head>
      <link rel="stylesheet" type="text/css"
            href="/c/e2b2c827e84b676fa90a8ae88702aa5c" />
      <script src="/c/240858026520292265e0834e5484b703"></script>
    </head>
    <img src="/c/4342b8790623f4bfeece676b8fe867a9" />
    

    在 Global.asax.cs 中

    我们需要创建一个路由来在这个路径上提供内容:

    routes.MapRoute(
        "ContentHash",
        "c/{hash}",
        new { controller = "Content", action = "Get" },
        new { hash = @"^[0-9a-zA-Z]+$" } // constraint
        );
    

    内容控制器

    这节课很长。它的症结很简单,但事实证明,您需要注意文件系统的更改才能强制重新计算缓存的文件哈希。我通过 FTP 发布我的网站,例如,bin 文件夹在 Content 文件夹之前被替换。在此期间请求该站点的任何人(人类或蜘蛛)都会导致旧哈希值被更新。

    代码看起来比读/写锁定要复杂得多。

    public sealed class ContentController : Controller
    {
        #region Hash calculation, caching and invalidation on file change
    
        private static readonly Dictionary<string, string> _hashByContentUrl = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        private static readonly Dictionary<string, ContentData> _dataByHash = new Dictionary<string, ContentData>(StringComparer.Ordinal);
        private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        private static readonly object _watcherLock = new object();
        private static FileSystemWatcher _watcher;
    
        internal static string ContentHashUrl(string contentUrl, string contentType, HttpContextBase httpContext, UrlHelper urlHelper)
        {
            EnsureWatching(httpContext);
    
            _lock.EnterUpgradeableReadLock();
            try
            {
                string hash;
                if (!_hashByContentUrl.TryGetValue(contentUrl, out hash))
                {
                    var contentPath = httpContext.Server.MapPath(contentUrl);
    
                    // Calculate and combine the hash of both file content and path
                    byte[] contentHash;
                    byte[] urlHash;
                    using (var hashAlgorithm = MD5.Create())
                    {
                        using (var fileStream = System.IO.File.Open(contentPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                            contentHash = hashAlgorithm.ComputeHash(fileStream);
                        urlHash = hashAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(contentPath));
                    }
                    var sb = new StringBuilder(32);
                    for (var i = 0; i < contentHash.Length; i++)
                        sb.Append((contentHash[i] ^ urlHash[i]).ToString("x2"));
                    hash = sb.ToString();
    
                    _lock.EnterWriteLock();
                    try
                    {
                        _hashByContentUrl[contentUrl] = hash;
                        _dataByHash[hash] = new ContentData { ContentUrl = contentUrl, ContentType = contentType };
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }
                }
    
                return urlHelper.Action("Get", "Content", new { hash });
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
        }
    
        private static void EnsureWatching(HttpContextBase httpContext)
        {
            if (_watcher != null)
                return;
    
            lock (_watcherLock)
            {
                if (_watcher != null)
                    return;
    
                var contentRoot = httpContext.Server.MapPath("/");
                _watcher = new FileSystemWatcher(contentRoot) { IncludeSubdirectories = true, EnableRaisingEvents = true };
                var handler = (FileSystemEventHandler)delegate(object sender, FileSystemEventArgs e)
                {
                    // TODO would be nice to have an inverse function to MapPath.  does it exist?
                    var changedContentUrl = "~" + e.FullPath.Substring(contentRoot.Length - 1).Replace("\\", "/");
                    _lock.EnterWriteLock();
                    try
                    {
                        // if there is a stored hash for the file that changed, remove it
                        string oldHash;
                        if (_hashByContentUrl.TryGetValue(changedContentUrl, out oldHash))
                        {
                            _dataByHash.Remove(oldHash);
                            _hashByContentUrl.Remove(changedContentUrl);
                        }
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }
                };
                _watcher.Changed += handler;
                _watcher.Deleted += handler;
            }
        }
    
        private sealed class ContentData
        {
            public string ContentUrl { get; set; }
            public string ContentType { get; set; }
        }
    
        #endregion
    
        public ActionResult Get(string hash)
        {
            _lock.EnterReadLock();
            try
            {
                // set a very long expiry time
                Response.Cache.SetExpires(DateTime.Now.AddYears(1));
                Response.Cache.SetCacheability(HttpCacheability.Public);
    
                // look up the resource that this hash applies to and serve it
                ContentData data;
                if (_dataByHash.TryGetValue(hash, out data))
                    return new FilePathResult(data.ContentUrl, data.ContentType);
    
                // TODO replace this with however you handle 404 errors on your site
                throw new Exception("Resource not found.");
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }
    

    辅助方法

    如果不使用 ReSharper,可以删除属性。

    public static class ContentHelpers
    {
        [Pure]
        public static MvcHtmlString ScriptImportContent(this HtmlHelper htmlHelper, [NotNull, PathReference] string contentPath, [CanBeNull, PathReference] string minimisedContentPath = null)
        {
            if (contentPath == null)
                throw new ArgumentNullException("contentPath");
    #if DEBUG
            var path = contentPath;
    #else
            var path = minimisedContentPath ?? contentPath;
    #endif
    
            var url = ContentController.ContentHashUrl(contentPath, "text/javascript", htmlHelper.ViewContext.HttpContext, new UrlHelper(htmlHelper.ViewContext.RequestContext));
            return new MvcHtmlString(string.Format(@"<script src=""{0}""></script>", url));
        }
    
        [Pure]
        public static MvcHtmlString CssImportContent(this HtmlHelper htmlHelper, [NotNull, PathReference] string contentPath)
        {
            // TODO optional 'media' param? as enum?
            if (contentPath == null)
                throw new ArgumentNullException("contentPath");
    
            var url = ContentController.ContentHashUrl(contentPath, "text/css", htmlHelper.ViewContext.HttpContext, new UrlHelper(htmlHelper.ViewContext.RequestContext));
            return new MvcHtmlString(String.Format(@"<link rel=""stylesheet"" type=""text/css"" href=""{0}"" />", url));
        }
    
        [Pure]
        public static string ImageContent(this UrlHelper urlHelper, [NotNull, PathReference] string contentPath)
        {
            if (contentPath == null)
                throw new ArgumentNullException("contentPath");
            string mime;
            if (contentPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
                mime = "image/png";
            else if (contentPath.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || contentPath.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
                mime = "image/jpeg";
            else if (contentPath.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
                mime = "image/gif";
            else
                throw new NotSupportedException("Unexpected image extension.  Please add code to support it: " + contentPath);
            return ContentController.ContentHashUrl(contentPath, mime, urlHelper.RequestContext.HttpContext, urlHelper);
        }
    }
    

    感谢您的反馈!

    【讨论】:

    • 从这个技术一目了然,我有一个理论上的担忧是哈希冲突可能导致客户端和服务器具有相同的 URL 的不同文件内容。您的哈希很长,这很好,但我会更喜欢基于日期的方法,因为至少它在正确性而非性能方面出错。
    • 在实践中,如果你使用足够多的字符,哈希冲突是不值得担心的。使用 8 个字符,您有 4.2e9 的碰撞几率。 12 个字符给你 2.8e14。我会为最终成为最佳策略的内容提供解决方案。
    【解决方案2】:

    您需要修改您引用的外部文件的名称。例如在每个文件的末尾添加内部版本号,例如 style-1423.css,并将编号作为构建自动化的一部分,以便每次部署文件和引用时都使用唯一的名称。

    【讨论】:

    • 但是如何自动更改 ASPX 源以使用正确的外部文件名称?
    • 可以通过后期构建脚本轻松完成。只需编写一个脚本或一段简单的代码来查找 *.css 或 *.js 文件并重命名它们并在每次构建后运行它。如果您有构建自动化,您可以在每次构建结束时自动添加此脚本。
    • @Alexander & @Serhat,请参阅my answer,了解一种在运行时重写名称而无需修改构建过程的方法,以及在不引入任何缓存的情况下最大限度地利用缓存的方式OP 描述的问题。
    【解决方案3】:

    而不是构建号或随机数,以编程方式将文件的最后修改日期作为查询字符串附加到 URL。这将防止您忘记手动修改查询字符串的任何意外,并允许浏览器在文件未更改时缓存文件。

    示例输出可能如下所示:

    <script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>
    

    【讨论】:

    • 一个好主意,如果您可以设计一种方法来避免在每个请求上查询每个引用文件的文件属性的性能影响。
    • 性能影响几乎为零,操作系统会缓存该信息。如果这是一个真正的问题,您可以将信息缓存 5 秒左右。
    • 或者,您可以在构建过程中附加最后修改日期,这将完全减轻任何性能影响,并防止因忘记手动修改文件而出现人为错误的机会。
    • 这会起作用,但不是最优的。例如,如果您重新上传相同的文件而不做任何更改,即使内容没有更改,您的用户也需要重新下载它。此外,一些代理服务器不会缓存具有查询字符串的 URL,因此您可能会失去缓存优势。有关解决这些问题的技术,请参阅 my answer
    【解决方案4】:

    由于您只提到您的测试人员在抱怨,您是否考虑过让他们关闭本地浏览器缓存,以便每次都检查新内容?这会使他们的浏览器慢一点……但除非你每次都进行可用性测试,否则这可能比后缀文件名、添加查询字符串参数或修改标题要容易得多。

    这适用于我们测试环境中 90% 的情况。

    【讨论】:

    • 我支持这个。您的测试人员应该使用封闭环境,因此应该很容易将环境配置为不使用缓存或“重置”自身,例如虚拟机。
    • 这不仅是测试环境中的问题(尽管这可能是 OP 最直接关心的问题)。当你更新一个生产站点时,你必须处理用户的浏览器缓存了旧版本的 css/js 文件。
    • 目前它只影响我们的测试人员。我担心的是,一旦我们投入生产,它会影响实时用户,我们会在它上线后进行修改。
    • 这不是问题的解决方案。如果您的测试人员看到了这个问题,那么您的用户也会看到。否则为什么会有测试人员?
    【解决方案5】:

    你可以做的是在每次页面刷新时用一个随机字符串调用你的 JS 文件。这样你就可以确保它总是新鲜的。

    你只需要这样称呼它“/path/to/your/file.js?&lt;random-number&gt;

    示例:jquery-min-1.2.6.js?234266

    【讨论】:

    • 我唯一的问题是在这种情况下它永远不会被缓存。如果它没有被修改,我希望它被缓存。 OrbMan 的解决方案似乎恰到好处。
    • 是的。您还可以通过一个常量传递,您每次迭代都会更改该常量。两者都运作良好。
    • 查询字符串可能会导致代理服务器缓存出现一些问题(他们不会缓存它),因此您可能会失去好处。此外,您要求所有用户每次迭代都重新下载内容,即使它没有更改。请参阅my answer 了解基于文件本身内容工作的解决方案,因此仅在需要时进行更改并允许最大可能的缓存优势。
    【解决方案6】:

    在您对 CSS 和 Javascript 文件的引用中,附加一个版本查询字符串。每次更新文件时都会碰到它。这将被网站忽略,但网络浏览器会将其视为新资源并重新加载。

    例如:

    <link href="../../Themes/Plain/style.css?v=1" rel="stylesheet" type="text/css" />
    <script src="../../Scripts/site.js?v=1" type="text/javascript"></script>
    

    【讨论】:

    • 这是一个非常手动的解决方案。如果您像我一样懒惰,请参阅my answer :)
    【解决方案7】:

    您可以编辑文件的 http 标头以强制浏览器在每个请求上重新验证

    【讨论】:

    • 没错,这可以解决问题,但会降低您的网站速度。存在更好的解决方案。
    猜你喜欢
    • 2012-01-24
    • 2021-04-30
    • 1970-01-01
    • 2012-01-25
    • 2021-05-24
    • 1970-01-01
    • 2010-09-27
    • 2020-12-25
    • 2011-09-13
    相关资源
    最近更新 更多