简体   繁体   中英

Stale javascript cache files and MVC4

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?

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

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM