Comment puis-je forcer un rafraîchissement dur (ctrl+F5)?

nous développons activement un site Web en utilisant .Net et MVC et nos testeurs ont des crises en essayant d'obtenir les dernières choses à tester. Chaque fois que nous modifions la feuille de style ou les fichiers javascript externes, les testeurs ont besoin de faire un rafraîchissement dur (ctrl+F5 dans IE) afin de voir les derniers trucs.

est-il possible pour moi de forcer leurs navigateurs à obtenir la dernière version de ces fichiers au lieu d'eux en se basant sur leurs versions cachées? Nous ne faisons pas tout type de spécial cachant à partir de IIS ou autre chose.

une fois que cela sera en production, il sera difficile de dire aux clients qu'ils ont besoin de rafraîchir dur afin de voir les derniers changements.

Merci!

26
demandé sur Drew Noakes 2009-06-02 00:32:24

7 réponses

Vous devez modifier les noms des fichiers externes auquel vous vous référez. Pour ajouter par exemple le numéro de build à la fin de chaque fichier, comme style-1423.css et faire de la numérotation une partie de votre build automation afin que les fichiers et les références soient déployés avec un nom unique à chaque fois.

15
répondu Serhat Ozgel 2009-06-01 20:36:18

je me suis également heurté à cela et j'ai trouvé ce que je considère être une solution très satisfaisante.

notez que l'utilisation des paramètres de requête .../foo.js?v=1 signifie que le fichier ne sera apparemment pas mis en cache par certains serveurs mandataires. Il est préférable de modifier le chemin directement.

nous avons besoin du navigateur pour forcer un rechargement lorsque le contenu change. Donc, dans le code que j'ai écrit, le chemin inclut un hachage MD5 du fichier référencé. Si le fichier est republié au serveur web mais a le même contenu, alors son URL est identique. De plus, il est sûr d'utiliser une expiration infinie pour la mise en cache aussi, car le contenu de cette URL ne changera jamais.

ce hachage est calculé à l'exécution (et caché en mémoire pour les performances), donc il n'y a pas besoin de modifier votre processus de construction. En fait, depuis l'ajout de ce code à mon site, je n'ai pas eu à y réfléchir beaucoup.

Vous pouvez le voir en action sur ce site: Plongée Sept - en Ligne de Plongée de la Journalisation pour les Plongeurs

In CSHTML / ASPX files

<head>
  @Html.CssImportContent("~/Content/Styles/site.css");
  @Html.ScriptImportContent("~/Content/Styles/site.js");
</head>
<img src="@Url.ImageContent("~/Content/Images/site.png")" />

cela génère un markup ressemblant:

<head>
  <link rel="stylesheet" type="text/css"
        href="/c/e2b2c827e84b676fa90a8ae88702aa5c" />
  <script src="/c/240858026520292265e0834e5484b703"></script>
</head>
<img src="/c/4342b8790623f4bfeece676b8fe867a9" />

En Mondiale.asax.cs

nous devons créer une route pour servir le contenu à cette voie:

routes.MapRoute(
    "ContentHash",
    "c/{hash}",
    new { controller = "Content", action = "Get" },
    new { hash = @"^[0-9a-zA-Z]+$" } // constraint
    );

ContentController

Cette classe est assez longue. Le cœur du problème est simple, mais il s'avère que vous devez surveiller les changements apportés au système de fichiers afin de forcer le recalcul des hachages de fichiers en cache. Je publie mon site par FTP et, par exemple, le dossier bin est remplacé avant le dossier Content . Toute personne (humaine ou araignée) qui demande le site pendant cette période fera en sorte que le vieux hachage soit mis à jour.

le code semble beaucoup plus complexe qu'il n'est dû à la lecture/écriture de verrouillage.

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();
        }
    }
}

Méthodes D'Aide

, Vous pouvez supprimer les attributs si vous n'utilisez pas 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);
    }
}

Feedback appreciated!

22
répondu Drew Noakes 2012-05-05 05:40:48

plutôt qu'un nombre de compilation ou un nombre aléatoire, ajoutez la date de dernière modification du fichier à L'URL en tant que querystring programmatically. Cela évitera tout accident où vous oubliez de modifier manuellement le querystring, et permettra au navigateur de mettre en cache le fichier quand il n'a pas changé.

exemple de sortie pourrait ressembler à ceci:

<script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>
12
répondu RedFilter 2009-06-01 22:14:02

puisque vous ne mentionnez que vos testeurs se plaignent, avez-vous envisagé de Les Faire désactiver leur cache de navigateur local, de sorte qu'il vérifie à chaque fois pour de nouveaux contenus? Il ralentira leurs navigateurs une touche... mais à moins que vous ne fassiez des tests d'utilisabilité à chaque fois, c'est probablement beaucoup plus facile que de postfixer le nom du fichier, d'ajouter un paramètre querystring ou de modifier les en-têtes.

Cela fonctionne dans 90% des cas dans nos environnements de test.

4
répondu Chad Ruppert 2009-06-01 21:19:02

ce que vous pouvez faire est d'appeler votre fichier JS avec une chaîne de caractères aléatoire à chaque fois que la page se rafraîchit. De cette façon, vous êtes sûr qu'il est toujours frais.

il suffit de l'appeler ainsi "/path/to/your/file.js? < nombres aléatoires > "

exemple: jquery-min-1.2.6.js?234266

2
répondu Erick 2009-06-01 20:37:15

dans vos références aux fichiers CSS et Javascript, ajoutez une chaîne de requête de version. Elle bosse à chaque fois que vous mettez à jour le fichier. Ce sera ignoré par le site web, mais des navigateurs web les traiter comme une nouvelle ressource et re-charger.

par exemple:

<link href="../../Themes/Plain/style.css?v=1" rel="stylesheet" type="text/css" />
<script src="../../Scripts/site.js?v=1" type="text/javascript"></script>
1
répondu DSO 2009-06-01 20:39:10

vous pouvez éditer les en-têtes http des fichiers pour forcer les navigateurs à revalider sur chaque requête

1
répondu Ozzy 2009-10-10 04:46:26