Dot-Net

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

  • June 1, 2009

我們正在積極開發一個使用 .Net 和 MVC 的網站,我們的測試人員正在嘗試獲取最新的東西進行測試。每次我們修改樣式表或外部 javascript 文件時,測試人員都需要進行一次硬刷新(在 IE 中為 ctrl+F5)才能看到最新的內容。

我是否可以強制他們的瀏覽器獲取這些文件的最新版本,而不是依賴他們的記憶體版本?我們沒有從 IIS 或任何東西做任何類型的特殊記憶體。

一旦投入生產,就很難告訴客戶他們需要硬刷新才能看到最新的變化。

謝謝!

您需要修改您引用的外部文件的名稱。例如,在每個文件的末尾添加內部版本號,例如 style-1423.css,並使編號成為建構自動化的一部分,以便每次部署文件和引用時都使用唯一的名稱。

我也遇到了這個問題,發現我認為是一個非常令人滿意的解決方案。

請注意,使用查詢參數.../foo.js?v=1據說意味著文件顯然不會被某些代理伺服器記憶體。最好直接修改路徑。

我們需要瀏覽器在內容更改時強制重新載入。因此,在我編寫的程式碼中,路徑包含被引用文件的 MD5 雜湊值。如果文件重新發佈到 Web 伺服器但具有相同的內容,則其 URL 是相同的。更重要的是,使用無限期記憶體也是安全的,因為該 URL 的內容永遠不會改變。

此雜湊在執行時計算(並記憶體在記憶體中以提高性能),因此無需修改建構過程。事實上,自從將這段程式碼添加到我的網站後,我就不必多想了。

你可以在這個網站上看到它的實際效果:Dive Seven - 水肺潛水員的線上潛水記錄

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

回饋讚賞!

引用自:https://stackoverflow.com/questions/936626