如何強制硬刷新(ctrl+F5)?
我們正在積極開發一個使用 .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); } }回饋讚賞!