我们正在积极开发使用.Net和MVC的网站,我们的测试人员正在试图获得最新的测试内容。每次我们修改样式表或外部javascript文件时,测试人员都需要进行硬刷新(IE中的ctrl + F5)才能看到最新的内容。
我是否有可能强制他们的浏览器获取这些文件的最新版本而不是依赖于他们的缓存版本?我们没有从IIS或其他任何方式进行任何特殊的缓存。
一旦投入生产,很难告诉客户他们需要进行硬刷新才能看到最新的变化。
谢谢!
答案 0 :(得分:23)
我也反对这一点,并发现我认为是一个非常令人满意的解决方案。
请注意,使用查询参数.../foo.js?v=1
可能意味着某些代理服务器显然不会缓存该文件。最好直接修改路径。
我们需要浏览器在内容更改时强制重新加载。因此,在我编写的代码中,路径包含被引用文件的MD5哈希值。如果文件重新发布到Web服务器但具有相同的内容,则其URL是相同的。更重要的是,使用无限期到缓存也是安全的,因为该URL的内容永远不会改变。
此哈希值在运行时计算(并在内存中缓存以提高性能),因此无需修改构建过程。事实上,自从将此代码添加到我的网站后,我就不必多考虑了。
您可以在此网站上看到它:Dive Seven - Online Dive Logging for Scuba Divers
<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" />
我们需要创建一条路径来提供此路径中的内容:
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);
}
}
反馈意见!
答案 1 :(得分:16)
您需要修改所引用的外部文件的名称。对于例如在每个文件的末尾添加内部版本号,例如style-1423.css,并使编号成为构建自动化的一部分,以便每次都使用唯一的名称部署文件和引用。
答案 2 :(得分:12)
而不是内部版本号或随机数,以编程方式将文件的最后修改日期作为查询字符串附加到URL。这将防止您忘记手动修改查询字符串的任何事故,并允许浏览器在文件未更改时缓存该文件。
示例输出可能如下所示:
<script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>
答案 3 :(得分:4)
由于您只提到您的测试人员抱怨,您是否考虑让他们关闭本地浏览器缓存,以便每次都检查新内容?它会减慢浏览器的速度...但除非你每次都进行可用性测试,否则这可能比后缀文件名,添加查询字符串参数或修改标题要容易得多。
这适用于我们测试环境中90%的情况。
答案 4 :(得分:2)
您可能要做的是每次页面刷新时使用随机字符串调用JS文件。这样你就可以确定它总是新鲜的。
你只需要这样调用它“/path/to/your/file.js?<
random-number >
”
示例:jquery-min-1.2.6.js?234266
答案 5 :(得分:1)
在对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>
答案 6 :(得分:1)
您可以编辑文件的http标头以强制浏览器在每个请求上重新验证