Как я могу принудительно обновить (ctrl + F5)?

Мы активно разрабатываем веб-сайт с использованием .Net и MVC, и наши тестеры прикладывают усилия, пытаясь получить последние материалы для тестирования. Каждый раз, когда мы изменяем таблицу стилей или внешние файлы javascript, тестерам необходимо сделать жесткое обновление (ctrl + F5 в IE), чтобы увидеть последние материалы.

Возможно ли, чтобы я заставил свои браузеры получать последнюю версию этих файлов, а не полагаться на их кешированные версии? Мы не делаем никакого специального кэширования из IIS или чего-то еще.

Как только это произойдет, будет сложно сказать клиентам, что им нужно жестко обновить, чтобы увидеть последние изменения.

Спасибо!

Ответ 1

Вам нужно изменить имена внешних файлов, на которые вы ссылаетесь. Напр. добавьте номер сборки в конце каждого файла, например style-1423.css, и сделайте нумерацию частью вашей автоматизации сборки, чтобы файлы и ссылки были развернуты с уникальным именем каждый раз.

Ответ 2

Я тоже столкнулся с этим и нашел то, что считаю очень удовлетворительным решением.

Обратите внимание, что использование параметров запроса .../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);
    }
}

Обратная связь оценена!

Ответ 3

Вместо номера сборки или случайного числа добавьте дату последнего изменения файла в URL-адрес как программный запрос. Это предотвратит любые несчастные случаи, когда вы забудете изменить запрос вручную и разрешите браузеру кэшировать файл, если он не изменился.

Пример вывода может выглядеть так:

<script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>

Ответ 4

Поскольку вы упоминаете только ваши тестеры, жалующиеся, считали ли вы, что они отключили свой локальный кеш браузера, чтобы он каждый раз проверял новый контент? Это замедлит работу браузеров... но если вы каждый раз не выполняете юзабилити-тестирование, это, вероятно, намного проще, чем постфинирование имени файла, добавление параметра querystring или изменение заголовков.

Это работает в 90% случаев в наших тестовых средах.

Ответ 5

Что вы можете сделать - это вызвать ваш JS файл со случайной строкой каждый раз при обновлении страницы. Таким образом, вы уверены, что он всегда свежий.

Вам просто нужно называть это таким образом "/path/to/your/file.js? <random-number>"

Пример: jquery-min-1.2.6.js? 234266

Ответ 6

В ваших ссылках на файлы 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>

Ответ 7

вы можете отредактировать заголовки http файлов, чтобы заставить браузеры повторить проверку по каждому запросу