代码之家  ›  专栏  ›  技术社区  ›  Chris Conway

如何强制硬刷新(ctrl+F5)?

  •  26
  • Chris Conway  · 技术社区  · 17 年前

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

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

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

    谢谢

    7 回复  |  直到 13 年前
        1
  •  24
  •   Bigwave    14 年前

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

    请注意,使用查询参数 .../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();
            }
        }
    }
    

    帮助方法

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

    感谢您的反馈!

        2
  •  17
  •   Serhat Ozgel    17 年前

        3
  •  12
  •   D'Arcy Rittich    17 年前

    与生成号或随机数不同,可以通过编程方式将文件的上次修改日期作为querystring附加到URL。这将防止您忘记手动修改查询字符串的任何事故,并允许浏览器在文件未更改时缓存该文件。

    示例输出可能如下所示:

    <script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>
    
        4
  •  4
  •   Chad Ruppert    17 年前

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

    在我们的测试环境中,这在90%的情况下都有效。

        5
  •  2
  •   Erick    17 年前

    你只需要这样称呼它“/path/to/your/file.js?”? < 随机数 > "

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

        6
  •  1
  •   dso    17 年前

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

    例如:

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

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