.net - after - how to force browser to reload cached css js files
¿Cómo puedo forzar una actualización(ctrl+F5)? (7)
Estamos desarrollando activamente un sitio web usando .Net y MVC, y nuestros evaluadores están teniendo problemas tratando de obtener lo último en pruebas. Cada vez que modificamos la hoja de estilos o los archivos javascript externos, los evaluadores deben realizar una actualización (ctrl + F5 en IE) para ver las últimas novedades.
¿Es posible para mí obligar a sus navegadores a obtener la última versión de estos archivos en lugar de confiar en sus versiones en caché? No estamos haciendo ningún tipo de almacenamiento en caché especial de IIS ni nada.
Una vez que esto entre en producción, será difícil decirle a los clientes que necesitan una actualización estricta para ver los últimos cambios.
¡Gracias!
Como usted solo menciona a los probadores que se quejan, ¿Ha considerado que apaguen su caché de navegador local para que compruebe siempre el contenido nuevo? Se ralentizará un poco su navegador ... pero a menos que esté haciendo pruebas de usabilidad todo el tiempo, esto es probablemente mucho más fácil que la postfijación del nombre de archivo, la adición de un parámetro de querystring o la modificación de los encabezados.
Esto funciona en el 90% de los casos en nuestros entornos de prueba.
Debe modificar los nombres de los archivos externos a los que hace referencia. Por ejemplo, agregue el número de compilación al final de cada archivo, como style-1423.css y haga que la numeración sea parte de su automatización de compilación para que los archivos y las referencias se implementen con un nombre único cada vez.
En lugar de un número de compilación o un número aleatorio, agregue la fecha de última modificación del archivo a la URL como cadena de consulta mediante programación. Esto evitará cualquier accidente en el que te olvides de modificar la cadena de consulta manualmente, y permitirá que el navegador guarde en caché el archivo cuando no haya cambiado.
El resultado del ejemplo podría verse así:
<script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>
En sus referencias a archivos CSS y Javascript, anexe una cadena de consulta de versión. Abdominalo cada vez que actualice el archivo. Esto será ignorado por el sitio web, pero los navegadores web lo tratarán como un nuevo recurso y lo volverán a cargar.
Por ejemplo:
<link href="../../Themes/Plain/style.css?v=1" rel="stylesheet" type="text/css" />
<script src="../../Scripts/site.js?v=1" type="text/javascript"></script>
Lo que podría hacer es llamar a su archivo JS con una cadena aleatoria cada vez que actualice la página. De esta manera estás seguro de que siempre está fresco.
Solo tiene que llamarlo de este modo "/ruta/a/tu/archivo.js? <
Número aleatorio >
"
Ejemplo: jquery-min-1.2.6.js? 234266
También me enfrenté a esto y encontré lo que considero una solución muy satisfactoria.
Tenga en cuenta que el uso de los parámetros de consulta .../foo.js?v=1
supuestamente significa que el archivo aparentemente no se almacenará en la .../foo.js?v=1
caché de algunos servidores proxy. Es mejor modificar la ruta directamente.
Necesitamos que el navegador fuerce una recarga cuando el contenido cambie. Entonces, en el código que escribí, la ruta incluye un hash MD5 del archivo al que se hace referencia. Si el archivo se vuelve a publicar en el servidor web pero tiene el mismo contenido, su URL es idéntica. Además, es seguro usar un vencimiento infinito para el almacenamiento en caché también, ya que el contenido de esa URL nunca cambiará.
Este hash se calcula en tiempo de ejecución (y se almacena en la memoria caché para el rendimiento), por lo que no es necesario modificar el proceso de compilación. De hecho, desde que agregué este código a mi sitio, no tuve que pensarlo mucho.
Puede verlo en acción en este sitio: Dive Seven - Online Dive Logging para Scuba Divers
En archivos CSHTML / ASPX
<head>
@Html.CssImportContent("~/Content/Styles/site.css");
@Html.ScriptImportContent("~/Content/Styles/site.js");
</head>
<img src="@Url.ImageContent("~/Content/Images/site.png")" />
Esto genera un marcado que se asemeja a:
<head>
<link rel="stylesheet" type="text/css"
href="/c/e2b2c827e84b676fa90a8ae88702aa5c" />
<script src="/c/240858026520292265e0834e5484b703"></script>
</head>
<img src="/c/4342b8790623f4bfeece676b8fe867a9" />
En Global.asax.cs
Necesitamos crear una ruta para servir el contenido en esta ruta:
routes.MapRoute(
"ContentHash",
"c/{hash}",
new { controller = "Content", action = "Get" },
new { hash = @"^[0-9a-zA-Z]+$" } // constraint
);
ContentController
Esta clase es bastante larga. El quid de la cuestión es simple, pero resulta que debe vigilar los cambios en el sistema de archivos para forzar el recálculo de hashes de archivos en caché. Publico mi sitio a través de FTP y, por ejemplo, la carpeta bin
se reemplaza antes de la carpeta de Content
. Cualquier persona (humana o araña) que solicite el sitio durante ese período hará que se actualice el antiguo hash.
El código parece mucho más complejo de lo que es debido al bloqueo de lectura / escritura.
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étodos de ayuda
Puede eliminar los atributos si no usa 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);
}
}
Comentarios apreciados!
podría editar los encabezados http de los archivos para obligar a los navegadores a revalidar en cada solicitud