Как я могу принудительно обновить (Ctrl + F5)? - PullRequest
26 голосов
/ 02 июня 2009

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

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

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

Спасибо!

Ответы [ 7 ]

23 голосов
/ 22 июня 2011

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

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

Обратная связь приветствуется!

16 голосов
/ 02 июня 2009

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

12 голосов
/ 02 июня 2009

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

Пример вывода может выглядеть следующим образом:

<script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>
4 голосов
/ 02 июня 2009

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

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

2 голосов
/ 02 июня 2009

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

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

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

1 голос
/ 02 июня 2009

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

1 голос
/ 02 июня 2009

В ваших ссылках на файлы 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>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...