对外部生成的静态内容进行指纹识别(ASP.NET + browserify)

Posted

技术标签:

【中文标题】对外部生成的静态内容进行指纹识别(ASP.NET + browserify)【英文标题】:Fingerprinting externally generated static content (ASP.NET + browserify) 【发布时间】:2015-06-25 20:56:44 【问题描述】:

Nodejs browserify 在构建模块化 js 应用程序时非常棒。如果gulp 也是设置的一部分,is further enhanced 管理和解决依赖关系的工作流,正确 捆绑,使用 sourcemaps 进行 uglify,auto-polyfill,jshint,测试......这很漂亮对于 css 以及预处理、自动前缀、linting、嵌入资源和生成文档都很方便。

TL;DR:通过 npm/bower,您可以访问广泛的前端库生态系统,使 nodejs 非常适合构建(不一定是服务!)客户端代码。事实上,将它用于客户端代码非常棒,以至于在 VS 2015 中将立即支持 npmbowergrunt/gulp。同时,我们已经建立了一个 gulp运行预构建并写入 dist js/css(捆绑输出)的任务。

用指纹网址引用外部静态内容的好方法是什么?从长远来看,我们最好能够完全分离客户端内容,这样它就可以独立构建并部署到 CDN,而无需还必须构建应用程序的其余部分。

【问题讨论】:

什么是“指纹网址”? @dandavis:这是一种在aggressively caching 资产时使陈旧内容无效的技术。这种行为(缓存破坏)通常是通过引用 URL 中的文件版本或哈希“指纹”来完成的,即app.js?v=123;每当发布更新时,都会从不同的 URL 提供文件。 从客户端看,所有的 url 是否都被硬编码到 html 中作为属性,如 src 和 href? @dandavis:不确定我是否关注;如果这是您所要求的,指纹 url 将呈现到页面中 好吧,该网址出现的每个地方都是重建时需要更新的地方。如果您使用某种客户端资源加载器,您可以保留一个未永久烘焙的文件,它包含您资源的所有 url。这样,您可以更新该单个文件并立即更新所有用户或某些组,等等。就目前而言,您基本上必须在更新资源时更新您的链接,如果您想在许多地方指向无法过期的 url,则没有简单的方法。基于 JS 的加载器可以提供帮助,但对于 CSS 来说仍然不是很好...... 【参考方案1】:

CSS 问题

由于 CSS 引用的图像的相对 url 也可能发生变化,并且您需要在启动应用程序之前计算大量哈希计算,这会减慢签名 url 的生成速度。事实证明,编写代码来跟踪上次修改日期不适用于 CSS 图像 url。因此,如果 css 中引用的任何图像发生更改,则 css 也必须更改。

单个文件版本控制(如 jquery-1.11.1.js)的问题

首先它破坏了源代码版本控制,Git 或任何版本控制会将 app-script-1.11.js 和 app-script-1.12.js 识别为两个不同的文件,很难维护历史记录。

对于 jquery,它会在他们构建库时工作,并且通常您不会在页面上包含资源时更改它,但是在构建应用程序时,我们将有许多 javascript 文件并且更改版本将需要更改每个页面,但是, 单个包含文件可能会执行此操作,但请考虑大量 css 和大量图像。

将上次更新日期缓存为 URL 前缀

所以我们必须想出像/cached/lastupdate/ 这样的静态内容的版本控制,这只不过是静态资产的 url 前缀。 lastupdate 只不过是所请求文件的最后更新日期时间。如果文件在应用范围内被修改,还有一个观察者会刷新缓存键。

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

在应用设置中定义版本如下

 <appSettings>
     <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/2015-12-12-10-10-10-1111/scripts/jquery-1.11.1.js"></script>

CDN 为

 <script 
      src="//cdn111.cloudfront.net/cached/2015-12-12-10-10-10-1111/scripts/jquery-1.11.1.js">
 </script>

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

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 override bool IsReusable
    
        get
        
            return true;
        
    

    public class CachedFileInfo
    

        public string Version  get; set; 

        public string FilePath  get; set; 

        public CachedFileInfo(string path)
        
            path = HttpContext.Current.Server.MapPath(path);

            FilePath = path;

            //Watch();

            Update(null, null);
        

        private void Watch()
        
            System.IO.FileSystemWatcher fs = new FileSystemWatcher(FilePath);
            fs.Changed += Update;
            fs.Deleted += Update;
            fs.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName;
        

        private void Update(object sender, FileSystemEventArgs e)
        
            FileInfo f = new FileInfo(FilePath);
            if (f.Exists)
            
                Version = f.LastWriteTimeUtc.ToString("yyyy-MM-dd-hh-mm-ss-FFFF");
            
            else
            
                Version = "null";
            
        


    

    private static ConcurrentDictionary<string, CachedFileInfo> CacheItems = new ConcurrentDictionary<string, CachedFileInfo>();

    public static HtmlString CachedUrl(string p)
    
        //if (!Enabled)
        //    return new HtmlString(p);
        if (!p.StartsWith("/"))
            throw new InvalidOperationException("Please provide full path starting with /");

        string v = Version;

        var cv = CacheItems.GetOrAdd(p, k => new CachedFileInfo(k));
        v = cv.Version;

        if (CDNHost != null)
        
            return new HtmlString("//" + CDNHost + "/cached/" + v + p);
        
        return new HtmlString("/cached/" + v + 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;
    

使用文件修改时间而不是版本

    public static HtmlString CachedUrl(string p)
    
        if (!p.StartsWith("/"))
            throw new InvalidOperationException("Please provide full path starting with /");
        var ft = (new System.IO.FileInfo(Server.MapPath(p)).LastModified;
        return new HtmlString(cdnPrefix + "/cached/" + ft.Ticks + p);
    

这会保留基于上次修改的版本,但这会增加每次请求时对 System.IO.FileInfo 的调用,但是您可以创建另一个字典来缓存此信息并监视更改,但工作量很大。

【讨论】:

详细实现,谢谢。最好对单个静态资产进行版本化,而不是对整个集合进行版本化,例如一个版本改变了 a.js 但没有改变 b.js 或 c.js 但所有三个都使用新的 src 呈现他们的脚本标签。 单个版本在进行许多更改时通常很困难,尤其是在敏捷方法中,但是 jquery-1.11.1.min.js 等常见资源可以直接从谷歌 CDN 加载,并且此 CachedRoute 只能用于仅限我们自己的资源文件。我同意每个版本的开销都很小,但它只是在单个版本中隔离了所有依赖资源。 另一种选择是使用文件修改时间并使用它而不是版本,这将强制 URL 嵌入文件修改时间,但这也会强制检查文件修改时间,(这是系统文件调用),每次向 IIS 发出请求时。事实证明,这个调用对于每个请求都是不必要的调用,最好离开全局版本。【参考方案2】:

在每次发布后对其进行版本化并更新 url:

https://cdn.contoso.com/libs/module/2.2/module.js

【讨论】:

每次发布​​后的版本控制会使每个资源的缓存失效,即使它没有被修改。

以上是关于对外部生成的静态内容进行指纹识别(ASP.NET + browserify)的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET MVC (C#) 上提供静态文件

ASP.NET MVC 解析模板生成静态页一(RazorEngine)

使用razor/asp.net mvc3生成静态html页面?

ASP.NET Core - 从 WebApi 提供静态内容 [重复]

为啥 FireFox 3.6.8 不缓存来自 asp.net 开发者服务器的静态内容?

Asp.net - 用于存储字典的缓存与静态变量