指纹外部生成静态内容(ASP.NET + browserify)

在构build模块化js应用程序时,Nodejs browserify非常棒。 如果gulp也是安装程序的一部分,工作stream将进一步增强,以pipe理和解决依赖关系, 正确捆绑,uglify与源代码,自动填充,jshint,testing…这对于css以及预处理,自动前缀,linting,embedded资源和生成文档。

TL; DR:使用npm / bower可以访问前端库的广泛生态系统,使nodej成为客户端代码的完美构build(不一定是服务!)。 事实上,将它用于客户端代码是非常棒的,以便在VS 2015中支持npmbowergrunt / gulp 。与此同时,我们设置了一个运行预编译和写入的吞吐任务dist js / css(打包输出)。

指纹url引用外部静态内容的好方法是什么? 从长远来看,我们最好能够将客户端内容完全分开,以便可以独立构build和部署到CDN,而无需构build其他应用程序。

与CSS的问题

由于CSS引用相对URL的图像可能会改变,你将需要计算大量的哈希计算之前启动您的应用程序,这将减缓签名url的生成。 事实certificate,编写代码来跟踪上次修改date不适用于CSS图片url。 所以如果在CSS内部引用的任何图像改变,CSS也必须改变。

单个文件版本问题像jquery-1.11.1.js

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

对于jquery来说,它会像构build库一样工作,在包含页面上的资源时,通常不会改变它,但是在构build应用程序时,我们将有很多JavaScript文件,更改版本需要更改每个页面,但是单个包含文件可能会这样做,但考虑大量的CSS和大量的图像。

caching最后更新date作为URL前缀

所以我们不得不提出像/cached/lastupdate/这样的静态内容版本,这只不过是静态资源的一个URL前缀。 lastupdate只是最后更新的请求文件的date时间。 如果在应用程序的范围内修改文件,还有一个刷新caching键的观察器。

那么最简单的方法之一是在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放在URLpath而不是查询string中使得CDN执行得更好,因为查询string可以在CDNconfiguration(通常是默认情况下)中被忽略。

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

 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调用,但是您可以创build另一个字典来caching这些信息并监视更改,但这是很多工作。

只需在每次发布后更新版本并更新url:

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