Как я могу принудительно обновить (Ctrl+F5)?
Мы активно развиваем веб-сайт с использованием.Net и MVC, и наши тестировщики стараются изо всех сил тестировать новейшие материалы. Каждый раз, когда мы изменяем таблицу стилей или внешние файлы javascript, тестеры должны выполнять полное обновление (ctrl+F5 в IE), чтобы увидеть последние новости.
Могу ли я заставить их браузеры получать последнюю версию этих файлов, а не полагаться на их кэшированные версии? Мы не занимаемся каким-либо специальным кэшированием из IIS или чем-то еще.
Как только это будет запущено в производство, клиентам будет сложно сказать, что им необходимо обновить систему, чтобы увидеть последние изменения.
Спасибо!
7 ответов
Вам нужно изменить имена внешних файлов, на которые вы ссылаетесь. Например, добавьте номер сборки в конце каждого файла, например style-1423.css, и сделайте нумерацию частью вашей автоматизации сборки, чтобы файлы и ссылки каждый раз развертывались с уникальным именем.
Я тоже столкнулся с этим и нашел то, что считаю очень удачным решением.
Обратите внимание, что с помощью параметров запроса .../foo.js?v=1
предположительно означает, что файл не будет кэшироваться некоторыми прокси-серверами. Лучше изменить путь напрямую.
Нам нужен браузер для принудительной перезагрузки при изменении содержимого. Итак, в коде, который я написал, путь включает в себя MD5-хеш файла, на который ссылаются. Если файл повторно опубликован на веб-сервере, но имеет тот же контент, то его 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
);
ContentController
Этот класс довольно длинный. Суть этого проста, но оказывается, что вам нужно следить за изменениями в файловой системе, чтобы форсировать пересчеты кэшированных файловых хэшей. Я публикую свой сайт по 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-адресу как строку запроса программным способом. Это предотвратит любые аварии, когда вы забудете изменить строку запроса вручную, и позволит браузеру кэшировать файл, если он не изменился.
Пример вывода может выглядеть так:
<script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>
Поскольку вы упоминаете только жалобы ваших тестировщиков, задумывались ли вы, чтобы они отключали локальный кеш браузера, чтобы он каждый раз проверял наличие нового контента? Это замедлит работу их браузеров... но если вы не проводите юзабилити-тестирование каждый раз, это, вероятно, будет намного проще, чем постфиксировать имя файла, добавить параметр строки запроса или изменить заголовки.
Это работает в 90% случаев в наших тестовых средах.
Что вы можете сделать, это вызывать ваш файл JS со случайной строкой каждый раз, когда страница обновляется. Таким образом, вы уверены, что он всегда свежий.
Вам просто нужно назвать это так "/path/to/your/file.js?<
случайный номер>
"
Пример: jquery-min-1.2.6.js?234266
Вы можете отредактировать http-заголовки файлов, чтобы браузеры выполняли повторную проверку при каждом запросе.
В ссылках на файлы 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>