如何强制浏览器在IIS中缓存文件?

时间:2015-07-05 08:52:50

标签: asp.net caching iis

目前,我将应用程序版本附加到所有JavaScript& StyleSheet文件,以防止旧浏览器中的缓存。它工作正常。但是,我想缓存所有JavaScript&没有任何Web服务器请求的StyleSheet。

enter image description here

使用当前设置,Web服务器响应如下图所示。我不希望浏览器花时间检查所有JavaScript和ET的ETag。 StyleSheet文件。

enter image description here

以下是web.config中的当前设置

<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" />

2 个答案:

答案 0 :(得分:0)

当IIS提供文件和监视更改时,IIS将始终发送重新验证缓存标头,强制浏览器检查更改。为了解决这个问题,我们设计了CachedRoute,如下所示,但是这在ASP.NET MVC中运行良好,但是在ASP.NET WebForms中也可以实现相同的更改。

此代码还为您提供了将静态资源移至CDN的好处。

缓存版本网址前缀

我们必须提出像/cached/version/这样的静态内容的版本控制,这只是静态资产的网址前缀。 version可以是完全无用的任何随机字母数字字符串,但标识不同的版本。

最简单的方法之一是在URL中使用版本密钥。

首先,在AssemblyInfo.cs中创建构建版本

 [assembly: AssemblyVersion("1.5.*.*")]

保留,*作为内部版本号替换,.net编译器将随每次构建自动递增。

或者在应用设置中定义版本,如下所示

 <appSettings>
     <add key="Static-Content-Version" value="1.5.445.55565"/>
     <add key="CDNHost" value="cdn1111.cloudfront.net"/>
 </appSettings>

 // Route configuration

 // set CDN if you have
 string cdnHost = WebConfigrationManager.AppSettings["CDNHost"];
 if(!string.IsEmpty(cdnHost)){
     CachedRoute.CDNHost = cdnHost;
 }

 // get assembly build information
 string version = typeof(RouteConfig).Assembly.GetName().Version.ToString();

 CachedRoute.CORSOrigins = "*";
 CachedRoute.Register(routes, TimeSpam.FromDays(30), version);

现在,在每个页面上,将您的静态内容引用为

 <script src="@CachedRoute.CachedUrl("/scripts/jquery-1.11.1.js")"></script>

渲染时,您的页面将呈现为(无CDN)

 <script src="/cached/1.5.445.55565/scripts/jquery-1.11.1.js"></script>

将CDN作为

 <script 
      src="//cdn111.cloudfront.net/cached/1.5.445.55565/scripts/jquery-1.11.1.js">
 </script>

将版本放在URL路径而不是查询字符串中会使CDN执行得更好,因为在CDN配置中可以忽略查询字符串(这通常是默认情况)。

<强>优点 如果将版本设置为与Assembly版本相同,则可以轻松地放置新版本。否则,每次版本更改时都必须手动更改web.config。

来自的CachedRoute类 https://github.com/neurospeech/atoms-mvc.net/blob/master/src/Mvc/CachedRoute.cs

public class CachedRoute : HttpTaskAsyncHandler, IRouteHandler
{

    private CachedRoute()
    {
        // only one per app..

    }

    private string Prefix { get; set; }

    public static string Version { get; private set; }

    private TimeSpan MaxAge { get; set; }

    public static string CORSOrigins { get; set; }
    //private static CachedRoute Instance;

    public static void Register(
        RouteCollection routes,
        TimeSpan? maxAge = null,
        string version = null)
    {
        CachedRoute sc = new CachedRoute();
        sc.MaxAge = maxAge == null ? TimeSpan.FromDays(30) : maxAge.Value;

        if (string.IsNullOrWhiteSpace(version))
        {
            version = WebConfigurationManager.AppSettings["Static-Content-Version"];
            if (string.IsNullOrWhiteSpace(version))
            {
                version = Assembly.GetCallingAssembly().GetName().Version.ToString();
            }
        }

        Version = version;

        var route = new Route("cached/{version}/{*name}", sc);
        route.Defaults = new RouteValueDictionary();
        route.Defaults["version"] = "1";
        routes.Add(route);
    }

    public override bool IsReusable
    {
        get
        {
            return true;
        }
    }

    public static string CDNHost { get; set; }

    public static HtmlString CachedUrl(string p)
    {
        if (!p.StartsWith("/"))
            throw new InvalidOperationException("Please provide full path starting with /");
        string cdnPrefix = string.IsNullOrWhiteSpace(CDNHost) ? "" : ("//" + CDNHost);
        return new HtmlString(cdnPrefix + "/cached/" + Version + p);
    }

    public override async Task ProcessRequestAsync(HttpContext context)
    {
        var Response = context.Response;
        Response.Cache.SetCacheability(HttpCacheability.Public);
        Response.Cache.SetMaxAge(MaxAge);
        Response.Cache.SetExpires(DateTime.Now.Add(MaxAge));

        if (CORSOrigins != null)
        {
            Response.Headers.Add("Access-Control-Allow-Origin", CORSOrigins);
        }


        string FilePath = context.Items["FilePath"] as string;

        var file = new FileInfo(context.Server.MapPath("/" + FilePath));
        if (!file.Exists)
        {
            throw new FileNotFoundException(file.FullName);
        }

        Response.ContentType = MimeMapping.GetMimeMapping(file.FullName);

        using (var fs = file.OpenRead())
        {
            await fs.CopyToAsync(Response.OutputStream);
        }
    }

    IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
    {
        //FilePath = requestContext.RouteData.GetRequiredString("name");
        requestContext.HttpContext.Items["FilePath"] = requestContext.RouteData.GetRequiredString("name");
        return (IHttpHandler)this;
    }
}

第一次请求的样本响应标头

Access-Control-Allow-Origin:*
Cache-Control:public
Content-Length:453
Content-Type:image/png
Date:Sat, 04 Jul 2015 08:04:55 GMT
Expires:Mon, 03 Aug 2015 00:46:43 GMT
Server:Microsoft-IIS/8.5
Via:1.1 ********************************
X-Amz-Cf-Id: ******************************
X-AspNet-Version:4.0.30319
X-AspNetMvc-Version:5.2
X-Cache:Miss from cloudfront
X-Powered-By:ASP.NET

请参阅,没有ETag,Vary by,Last Modified或验证标头,并且还看到显式的Expires标头,当您发送显式Expires标头时,浏览器将永远不会尝试验证缓存。

答案 1 :(得分:0)

这是一个简单的解决方案,其中包含一个适用于ASP实现的HttpModule。我们在SPA应用程序中使用它。它会要求浏览器缓存一年的某些资源。主页/登录页面是一个例外,将始终使用ETag进行检查。

第1步: 您已经完成的第一步是在每个资源的url中添加版本号。我们这样做是构建过程中的一个自动步骤。

第2步:接下来,在您的应用中添加一个CacheModule类:

public class CacheModule : IHttpModule
    {
        // extensions to cache, e.g. ".css",".html",".js",".png",".jpg",".gif",".ico",".woff",".eot",".svg",".ttf"
        private readonly string[] _extensions = ConfigurationManager.AppSettings["CacheModule_Extensions"].Split(",");        


        private readonly string[] _exceptions = ConfigurationManager.AppSettings["CacheModule_Exceptions"].Split(",");  


        public void Dispose() {}

        public void Init(HttpApplication context)
        {
            context.EndRequest += (sender, args) =>
            {
                var ctx = HttpContext.Current;
                var path = ctx.Request.Url.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);

                var isExcept = _exceptions.Any(path.Contains);

                ctx.Response.AddHeader("Cache-Control", "private");

                if (_extensions.Any(path.Contains) && ! isExcept )
                {
                    ctx.Response.AddHeader("Expires", (DateTime.Now.ToUniversalTime().AddYears(1)).ToString("r"));
                }
                else if (isExcept)
                {
                    ctx.Response.AddHeader("Expires", (DateTime.Now.ToUniversalTime().AddHours(-1)).ToString("r"));
                    ctx.Response.AddHeader("Max-Age", "0");
                }

            };
        }


    }

第3步:最后你输入了你的配置:

<?xml version="1.0"?>
    <configuration>
        <appSettings>
            <!-- resource extensions to cache -->
            <add key="CacheModule_Extensions" value=".css,.html,.js,.png,.jpg,.gif,.ico,.woff,.eot,.svg,.ttf" />
            <!-- exceptions to caching such as home/landing page e.g. "index.html" or any page/resource with known url that users may enter directly or may be redirected to -->
            <add key="CacheModule_Exceptions" value="index.html,buildinfo.html,unsupportedmobilebrowser.html, unsupportedbrowser.html" />
        </appSettings>
    <system.webServer>
        <modules>
            <add name="CacheModule" type="MyApp.Caching.CacheModule, MyApp"/>
        </modules>
    </system.webServer>
</configuration>