在ASP.NET MVC中实现CSS/JS的自动版本控制?

16

我正在阅读有关ASP.NET MVC中CSS/JS文件的“自动版本控制”的这个stackoverflow帖子,并想知道实现这一点的“最佳”策略是什么。

所提供的解决方案插入了一个程序集号码——这意味着每次发布时,它都会更改每个文件,这不是理想的,因为如果您只修改一个*.css或*.js文件,则会更改每个文件。

1)如何仅针对“单个文件”进行操作,而不是使用IIS7上的站点范围程序集,可以使用修改日期或其他东西吗?

2)另外,如果我有某种“静态”资源,例如http://static.domain.com/js/123.js,我该如何使用重写来为请求发送最新文件,如果有人将此静态链接集成到他们的网站中呢?

也就是说,http://static.domain.com/js/123.js是链接,当请求该链接时,检查并发送最新文件?


说实话,如果你进行捆绑,那么每个文件都会变成2-5个压缩和捆绑后的文件。我认为这是一个可行的解决方案。 - Worthy7
6个回答

22
ASP.NET 4.5+自带内置的捆绑和缩小框架,旨在解决此问题。
如果您绝对需要一个简单的自制解决方案,可以使用下面的答案,但我总是建议使用捆绑和缩小框架的正确方法。
您可以像这样修改 AssemblyInfo.cs 文件:
Change
[assembly: AssemblyVersion("1.0.0.0")]
to    
[assembly: AssemblyVersion("1.0.*")]

这意味着每次构建项目时,它都会有一个新的程序集版本,该版本高于先前的版本。现在你有了独特的版本号。
创建一个UrlHelperExtension类,以帮助在视图中需要时获取此信息:
public static class UrlHelperExtensions
{
    public static string ContentVersioned(this UrlHelper self, string contentPath)
    {
        string versionedContentPath = contentPath + "?v=" + Assembly.GetAssembly(typeof(UrlHelperExtensions)).GetName().Version.ToString();
        return self.Content(versionedContentPath);
    }
}

现在,您可以通过以下方式轻松地向视图添加版本号:
<link href="@Url.ContentVersioned("style.css")" rel="stylesheet" type="text/css" />

在查看您的页面源代码时,您现在会看到类似于以下内容:
<link href="style.css?v=1.0.4809.30029" rel="stylesheet" type="text/css" />

我遇到了一个错误 No overload for method 'ContentVersioned' takes 1 arguments,我的代码如下:<script type="text/javascript" src='~/Scripts/demoproject/@FileVersioning.ContentVersioned("custom.js")'></script> - ANJYR
1
太棒了! - mrpotocnik
@Anjyr 基于上述建议并假设您拥有像上面那样的ContentVersioned扩展,将src ='〜/ Scripts / demoproject / @ FileVersioning.ContentVersioned(“custom.js”)'更改为src ='@ Url.ContentVersioned(“〜/ full / path / to / custom.js”)' - mrpotocnik
1
@ANJYR请确保您正在使用System.Web.Mvc.UrlHelper(而不是System.Web.Http.Routing.UrlHelper) - willkendall

9

更新:之前的版本在Azure上无法运行,下面简化并更正了。(注意,在使用IIS Express的开发模式中使其正常工作,你需要从Microsoft安装URL Rewrite 2.0http://www.iis.net/downloads/microsoft/url-rewrite - 它使用WebPi安装程序,请确保先关闭Visual Studio)

如果您想要更改实际文件的名称,而不是附加查询字符串(某些代理/浏览器会忽略静态文件的查询字符串),则可以按照以下步骤操作:(我知道这是一个老帖子,但我在开发解决方案时遇到了它):

如何做:每次构建项目时自动递增程序集版本,并将该数字用于要保持刷新的特定资源的路由静态文件(因此,something.js将被包括为something.v1234.js,并且1234会在每次构建项目时自动更改)- 我还添加了一些额外的功能,以确保在生产中使用.min.js文件,在调试时使用regular.js文件(我正在使用WebGrease来自动执行缩小过程)这个解决方案的一个好处是它既适用于本地/ 开发模式,也适用于生产环境。(我使用的是Visual Studio 2015 / Net 4.6,但我相信这也适用于早期版本)。

第一步:在构建时启用程序集自动递增 在AssemblyInfo.cs文件(在项目的“属性”部分中找到)中更改以下行:

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

to

[assembly: AssemblyVersion("1.0.*")]
//[assembly: AssemblyFileVersion("1.0.0.0")]

步骤2:为包含嵌入版本标识的文件在web.config中设置url重写(请参见步骤3)

在web.config文件(项目的主要配置文件)中,在<system.webServer>部分添加以下规则。我直接将其放在了</httpProtocol>结束标签之后。

<rewrite>
  <rules>
    <rule name="static-autoversion">
      <match url="^(.*)([.]v[0-9]+)([.](js|css))$" />
      <action type="Rewrite" url="{R:1}{R:3}" />
    </rule>
    <rule name="static-autoversion-min">
      <match url="^(.*)([.]v[0-9]+)([.]min[.](js|css))$" />
      <action type="Rewrite" url="{R:1}{R:3}" />
    </rule>
  </rules>
</rewrite>

步骤3:设置应用变量来读取当前程序集版本并创建js和css文件中的版本slug。

在Global.asax.cs文件(位于项目根目录中)中,将以下代码添加到protected void Application_Start()方法中(在注册行之后):

            // setup application variables to write versions in razor (including .min extension when not debugging)
            string addMin = ".min";
            if (System.Diagnostics.Debugger.IsAttached) { addMin = ""; }  // don't use minified files when executing locally
            Application["JSVer"] = "v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString().Replace('.','0') + addMin + ".js";
            Application["CSSVer"] = "v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString().Replace('.', '0') + addMin + ".css";

第四步:使用我们在Global.asax.cs中设置的应用程序变量更改Razor视图中的src链接。

@HttpContext.Current.Application["CSSVer"]
@HttpContext.Current.Application["JSVer"]

例如,在我的_Layout.cshtml文件中,头部区域有以下代码块用于样式表:
<!-- Load all stylesheets -->
<link rel='stylesheet' href='https://fontastic.s3.amazonaws.com/8NNKTYdfdJLQS3D4kHqhLT/icons.css' />
<link rel='stylesheet' href='/Content/css/main-small.@HttpContext.Current.Application["CSSVer"]' />
<link rel='stylesheet' media='(min-width: 700px)' href='/Content/css/medium.@HttpContext.Current.Application["CSSVer"]' />
<link rel='stylesheet' media='(min-width: 700px)' href='/Content/css/large.@HttpContext.Current.Application["CSSVer"]' />
@RenderSection("PageCSS", required: false)

需要注意的有两点:1)文件没有扩展名。2)也没有.min。这两个问题都由Global.asax.cs中的代码处理。

同样,在我的javascript部分中(也在_Layout.cs中),我有以下代码:

<script src="~/Scripts/all3bnd100.min.js" type="text/javascript"></script>
<script src="~/Scripts/ui.@HttpContext.Current.Application["JSVer"]" type="text/javascript"></script>
@RenderSection("scripts", required: false)

第一个文件是我手动使用WebGrease创建的所有第三方库的捆绑包。如果我添加或更改了捆绑包中的任何文件(这很少见),那么我会手动将文件重命名为all3bnd101.min.js、all3bnd102.min.js等等。此文件与重写处理程序不匹配,因此将在客户端浏览器上保留缓存,直到您手动重新捆绑/更改名称。
第二个文件是ui.js(根据您是否运行调试模式,将编写为ui.v12345123.js或ui.v12345123.min.js)。这将被处理/重写。(您可以在Global.asax.cs的Application_OnBeginRequest中设置断点以观察其工作)
有关详细讨论,请参见:ASP.NET MVC 5中简化的自动版本控制以解决缓存问题(适用于Azure和本地环境),可使用或不使用URL重写(包括一种无需URL重写的方法)。

6

1) 使用文件修改时间。以下是一个示例:

public static string GeneratePathWithTime(string cssFileName)
{
  var serverFilePath = server.MapPath("~/static/" + cssFileName);
  var version = File.GetLastWriteTime(serverFilePath).ToString("yyyyMMddhhmmss");
  return string.Format("/static/{0}/{1}", version, cssFileName);
}

这将生成一个路径,例如"/static/201109231100/style.css",用于"style.css"(假设您的style.css位于static目录中)。 然后,在IIS中添加一个重写规则,将"/ static / 201109231100 / style.css"重写为" /static/style.css"。版本号仅在CSS文件被修改时才会更改,并且仅适用于已修改的文件。

2) 您可以通过HttpModule处理对123.js的请求并发送最新内容,但我认为您无法保证请求获取最新版本。这取决于浏览器如何处理其缓存。您可以在响应头中设置较早的过期时间(例如,一分钟前),以告诉浏览器始终重新下载文件,但这完全取决于浏览器本身是否决定重新下载文件。这就是为什么我们需要为每次更新文件时生成不同的路径来处理修改后的文件,就像在问题1中一样,如果URL从未被访问过,则浏览器将始终尝试下载该文件。


3
你不需要在IIS中配置重写规则 - MVC本身支持路由(Routing) - Marius Schulz
当然,不仅限于MVC。您还可以编写请求处理程序并在web.config中注册它,或者使用Global.asax。它们也是本地支持的。IIS重写只是众多解决方案之一。我推荐使用IIS重写,因为它可以在.NET运行时之前处理请求,并且可以轻松配置而无需更改代码。 - Miller
你说得对,从ASP.NET 4开始,在ASP.NET Core中(不在MVC之内)也可以使用路由。然而,我提到的是MVC,因为问题标记了它。 - Marius Schulz

4
这个问题现在非常老了,但如果有人遇到它,根据我所知,以下是当前技术的最新进展:
  1. In ASP.NET Core you can use TagHelpers and simply add the asp-append-version attribute to any <link> or <script> tag:

    <script src="~/js/my.js" asp-append-version="true"></script>
    
  2. For both ASP.NET Core and Framework there is a NuGet Package called WebOptimizer (https://github.com/ligershark/WebOptimizer). It allows for both bundling and minification, and will also append a content-based version string to your file.

  3. If you want to do it yourself, there is the handy IFileVersionProvider interface, which you can get from your IServiceProvider in .NET Core:

    // this example assumes, you at least have a HttpContext
    var fileVersionProvider = httpContext.RequestServices.GetRequiredService<IFileVersionProvider>();
    string path = httpContext.Content("/css/site.css");
    string pathWithVersionString = fileVersionProvider.AddFileVersionToPath(httpContext.Request.PathBase, path);
    

    For .NET Framework, you can get the FileVersionProvider source from here: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor/src/Infrastructure/DefaultFileVersionProvider.cs You will have to do some work, like replacing the Cache with MemoryCache.Default or a ConcurrentDictionary or something, but the 'meat' is there.


4
我写了一个Url助手,它可以为我执行CacheBusting。
public static string CacheBustedContent(this UrlHelper helper, string contentPath)
{
    var path = string.Empty;

    if (helper.RequestContext.HttpContext.Cache["static-resource-" + contentPath] == null)
    {
        var fullpath = helper.RequestContext.HttpContext.Server.MapPath(contentPath);
        var md5 = GetMD5HashFromFile(fullpath);
        path = helper.Content(contentPath) + "?v=" + md5;

        helper.RequestContext.HttpContext.Cache.Add("static-resource-" + contentPath, path, null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(24, 0, 0), System.Web.Caching.CacheItemPriority.Default, null);
    }
    else
    {
        path = helper.RequestContext.HttpContext.Cache["static-resource-" + contentPath].ToString();
    }

    return path;
}

您可以将GetMD5HashFromFile()替换为CRC或任何其他基于文件内容或最后修改日期生成唯一字符串的方法。

缺点是每当缓存失效时,都会调用此方法。如果您在实时更改文件但不重置应用程序池,则可能需要触摸web.config以使其正确重新加载。


1
话虽如此,我真的很喜欢SquishIt用于压缩、合并和缓存JavaScript和CSS。http://www.codethinked.com/squishit-the-friendly-aspnet-javascript-and-css-squisher - davewasthere

3

您可能想看一下Dean Hume的博客文章《MVC和HTML5应用程序缓存》。在这篇文章中,他指出了一种优雅的方式来自动处理每个请求的版本控制,使用了@ShirtlessKirk的一个类库:

@Url.Content("~/Content/Site.css").AppendHash(Request)

4
对于任何偶然看到这篇答案的人,我想提醒一下——这个技巧依赖于应用缓存,但似乎已经弃用:https://developer.mozilla.org/en/docs/Web/HTML/Using_the_application_cache - Oggy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接