Reputation: 8337
Does MVC 4 bundle, resolve the issue with stale .js files? These .js files are cached on client's computer as such they are sometimes not updated with new deployment.
Will bundling and letting the framework figure out if the etag matches solve the issue in MVC 4?
Similarly what are the alternatives when using MVC 3?
Upvotes: 0
Views: 1754
Reputation: 8337
MVC 4 bundling emits a hash of the bundled resource.
Eg.
<link href="@System.Web.Optimization.BundleTable.
Bundles.ResolveBundleUrl("~/Content/css")"
rel="stylesheet"
type="text/css" />
results in the following:
<link href="/Content/css?v=ji3nO1pdg6VLv3CVUWntxgZNf1z"
rel="stylesheet" type="text/css" />
Should the file change the v
parameter will change, forcing the client to re-download the resource.
Source: http://bit.ly/xT8ZM5
Upvotes: 4
Reputation: 16150
I faced problem with script caching recently. New browsers (especially Chrome) are caching scripts, and sometimes they event don't send request to server to check if there is a new version.
In MVC3 app I decided to use custom route handler to deal with it. In html I append revision to each script link. Then in my handler I strip revision number from url, and then search for actual file on the server (eg. Path/Script.rev1000.js points to Path/Script.js).
Here is my code:
public class ContentRouteHandler : IRouteHandler
{
private OzirRouteProvider _routeProvider;
public ContentRouteHandler(OzirRouteProvider routeProvider)
{
this._routeProvider = routeProvider;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new ContentHttpHandler(this._routeProvider, this, requestContext);
}
}
internal class ContentHttpHandler : IHttpHandler, IRequiresSessionState
{
private OzirRouteProvider _routeProvider;
private ContentRouteHandler _routeHandler;
private RequestContext _requestContext;
public bool IsReusable { get { return false; } }
public ContentHttpHandler(OzirRouteProvider routeProvider, ContentRouteHandler routeHandler, RequestContext requestContext)
{
this._routeProvider = routeProvider;
this._routeHandler = routeHandler;
this._requestContext = requestContext;
}
public void ProcessRequest(HttpContext context)
{
string contentPath = context.Request.PhysicalPath;
string fileName = Path.GetFileNameWithoutExtension(contentPath);
string extension = Path.GetExtension(contentPath);
string path = Path.GetDirectoryName(contentPath);
bool minify = false;
// Here i get fileName like Script.rev1000.min.js
// I strip revision and .min from it so I'll have Script.js
var match = Regex.Match(fileName, "(\\.rev\\d+)?(\\.min)?$");
if (match.Groups[2].Success)
{
minify = true;
fileName = fileName.Remove(match.Groups[2].Index, match.Groups[2].Length);
contentPath = Path.Combine(path, fileName + extension);
}
if (match.Groups[1].Success)
{
fileName = fileName.Remove(match.Groups[1].Index, match.Groups[1].Length);
contentPath = Path.Combine(path, fileName + extension);
}
if (!File.Exists(contentPath)) // 404
{
throw new HttpException(404, "Not found");
}
DateTime lastModified = this.GetModificationDate(contentPath);
string eTag = this.GetETag(context.Request.RawUrl, contentPath, lastModified);
// Check for modification
string requestETag = context.Request.Headers["If-None-Match"];
string requestLastModified = context.Request.Headers["If-Modified-Since"];
DateTime? requestLastModifiedDate = requestLastModified == null ? null : (DateTime?)DateTime.Parse(requestLastModified).ToUniversalTime().TruncMiliseconds();
// Compare e-tag and modification date
if ((requestLastModified != null || requestETag != null) &&
(requestLastModified == null || requestLastModifiedDate == lastModified) &&
(requestETag == null || requestETag == eTag))
{
context.Response.StatusCode = 304;
context.Response.SuppressContent = true;
context.Response.Flush();
return;
}
switch (extension)
{
case ".js":
context.Response.ContentType = "application/x-javascript";
if (minify) // minify file?
{
string minContentPath = Path.Combine(path, fileName + ".min" + extension);
this.MinifyJs(contentPath, minContentPath);
contentPath = minContentPath;
}
break;
default:
throw new NotSupportedException(string.Format("Extension {0} is not supported yet", extension));
}
// g-zip and deflate support
string acceptEncoding = context.Request.Headers["Accept-Encoding"];
if (!string.IsNullOrEmpty(acceptEncoding) && acceptEncoding.Contains("gzip"))
{
context.Response.Filter = new System.IO.Compression.GZipStream(context.Response.Filter, System.IO.Compression.CompressionMode.Compress);
context.Response.AppendHeader("Content-Encoding", "gzip");
}
else if (!string.IsNullOrEmpty(acceptEncoding) && acceptEncoding.Contains("deflate"))
{
context.Response.Filter = new System.IO.Compression.DeflateStream(context.Response.Filter, System.IO.Compression.CompressionMode.Compress);
context.Response.AppendHeader("Content-Encoding", "deflate");
}
context.Response.AddCacheDependency(new CacheDependency(contentPath));
context.Response.AddFileDependency(contentPath);
context.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);
context.Response.Cache.SetETag(eTag);
context.Response.Cache.SetExpires(DateTime.Now.AddDays(7));
context.Response.Cache.SetLastModified(lastModified);
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(7));
context.Response.TransmitFile(contentPath);
context.Response.Flush();
}
private void MinifyJs(string contentPath, string minContentPath)
{
this._log.DebugFormat("Minifying JS {0} into {1}", contentPath, minContentPath);
if (!File.Exists(minContentPath) || File.GetLastWriteTime(contentPath) > File.GetLastWriteTime(minContentPath))
{
string content = File.ReadAllText(contentPath, Encoding.UTF8);
JavaScriptCompressor compressor = new JavaScriptCompressor();
compressor.Encoding = Encoding.UTF8;
compressor.ErrorReporter = new CustomErrorReporter(LoggingType.Debug);
content = compressor.Compress(content);
File.WriteAllText(minContentPath, content, Encoding.UTF8);
}
}
private DateTime GetModificationDate(string contentPath)
{
DateTime lastModified = File.GetLastWriteTimeUtc(contentPath).TruncMiliseconds();
return lastModified;
}
private string GetETag(string url, string contentPath, DateTime lastModified)
{
string eTag = string.Format("url={0},path={1},lm={2},rev={3}", url, contentPath, lastModified, AppInfo.Revision);
return Quote(GetHash(eTag));
}
private static string GetHash(string value)
{
byte[] data = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(value));
StringBuilder hex = new StringBuilder(data.Length * 2);
foreach (byte b in data)
{
hex.AppendFormat("{0:x2}", b);
}
return hex.ToString();
}
private static string Quote(string value)
{
return string.Format("\"{0}\"", value);
}
}
To use it you must turn RouteExistingFiles
on and register routes, for example:
routes.Add(new Route("Content/{*resource}", new RouteValueDictionary(), new RouteValueDictionary { { "resource", @".*(\.css)$" } }, contentHandler));
routes.Add(new Route("Scripts/{*resource}", new RouteValueDictionary(), new RouteValueDictionary { { "resource", @".*(\.js)$" } }, contentHandler));
Upvotes: 1
Reputation: 15866
You can use it with MVC 3. Its under System.Web.Optimization.dll
. You can download it and use .
For more information : http://nuget.org/packages/microsoft.web.optimization
For example in your global.asax, add this:
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
"~/Scripts/jquery-ui-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.unobtrusive*",
"~/Scripts/jquery.validate*"));
bundles.Add(new ScriptBundle("~/bundles/customjs").Include(
"~/Scripts/jquery.custom.js"));
// or what you want to add different js files.
Upvotes: 1