65.9K
CodeProject 正在变化。 阅读更多。
Home

Combres 2.0 - 适用于 ASP.NET 网站优化的库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (28投票s)

2010年3月30日

Apache

18分钟阅读

viewsIcon

310315

downloadIcon

2822

.NET 库,能够为 ASP.NET Web Forms 和 ASP.NET MVC 网站应用程序实现 JavaScript 和 CSS 资源的组合、最小化、压缩和缓存。

引言

几个月前,我发布了 Combres 1.0 的 Beta 版本,这是一个 .NET 库,可以自动应用许多针对 ASP.NET 应用程序的 Web 性能优化技术。我还写了一篇文章来介绍该版本的特性。自从那篇文章发布以来,又发布了几个小版本,直到上周,一个主要版本 2.0 发布了。2.0 版本有许多改动,因此更新旧文章意义不大,所以我决定写这篇文章来向读者介绍 Combres 2.0。这篇文章应该是独立的,因此您无需参考旧文章即可理解 Combres。 

Combres 概览

Combres 的开发灵感来源于 Steve Sounders 一本中描述的简单但非常有效的网站优化技术,以及 FireFox 插件YSlow文档。具体来说,Combres 可以在您的 ASP.NET MVC 或 ASP.NET Web Forms 应用程序中自动应用以下网站优化技术,而您只需付出很少的工作。

  • 减少 HTTP 请求次数。使用 Combres,您可以在 XML 配置文件中描述网站的资源,包括 JavaScript 和 CSS 文件,并将它们分组到不同的资源集中。Combres 将同一资源集中的资源进行合并,并通过 1 个 HTTP 请求提供合并后的内容。
  • 添加Expires或Cache-Control标头。Combres 根据您在 XML 配置文件中指定的缓存信息,在响应每个资源集的 HTTP 请求时自动发出 ExpiresCache-Control 响应标头。此外,Combres 会在服务器上缓存组合后的内容,以便组合过程(以及下文描述的其他步骤)不会为每个新用户(或在现有用户浏览器缓存失效时)执行。
  • Gzip组件。Combres 将检测用户的浏览器是否支持 Gzip 和/或 Deflate,并对每个资源集的组合内容应用相应的压缩算法,然后再将其发送到浏览器。如果浏览器不支持压缩,Combres 将返回原始输出。
  • 最小化 JavaScript。Combres 可以最小化 JavaScript 和 CSS 资源的内容。对于 JavaScript 资源,您可以配置 Combres 在以下最小化引擎之间进行选择:YUI Compressor for .NETMicrosoft Ajax MinifierGoogle Closure Compiler。对于这些引擎中的每一个,Combres 都允许您配置所有特定的属性,以便最大限度地提高其效率。每个资源集通常会分配一个特定的最小化引擎,但如果您需要,也可以让同一资源集内的资源使用单独的最小化引擎进行最小化。
  • 配置ETags。Combres 为每个资源集的组合内容发出 ETags。当浏览器发送回 ETag 时,Combres 会检查该 ETag 是否标识了资源集的最新版本,如果不是,它将把新内容推送到浏览器;否则,它将返回 Not Modified (304) 响应状态。

简而言之,Combres 帮助您的应用程序组合、最小化、压缩、添加适当的标头并缓存 JavaScript 和 CSS 资源。您需要做的就是创建一个 XML 配置文件来描述 Combres 要执行的操作,并在应用程序中添加几行代码来注册和使用 Combres。在本文中,我们将探讨 Combres 的这些核心功能以及更高级的功能。

通过 NuGet 部署 Combres (2011 年 1 月 25 日添加)

Combres 现在可以通过 NuGet (http://nuget.codeplex.com/) 进行部署。有关更多信息,请参阅此页面 (http://combres.codeplex.com/wikipage?title=5-Minute%20Quick%20Start%20for%20the%20Impatient)。“使用 Combres”部分的内容对于想要超越基础的用户仍然非常相关。

使用 Combres

示例 Web 应用程序

我将使用一个非常简单的 ASP.NET Web Forms 应用程序来演示 Combres 的功能。假设我们有一个应用程序,其结构如下:

页面 Default.aspx 按如下方式引用 JavaScript 和 CSS 文件:

<head runat="server">
    <title>Using Combres</title>
    <link type="text/css" rel="stylesheet" href="Styles/Site.css" />
    <link type="text/css" rel="stylesheet" href="Styles/jquery-ui-1.7.2.custom.css" />
    <script type="text/javascript" src="Scripts/jquery-1.3.2.js"></script>
    <script type="text/javascript" src="Scripts/jquery-ui-1.7.2.custom.min.js"></script>
</head>

如果您运行此页面并检查浏览器发出的 HTTP 请求,您会发现除了页面本身的 HTTP 请求之外,还有 4 个其他 HTTP 请求对应于这 4 个 JavaScript 和 CSS 文件。下图显示了通过 FireBugs 插件跟踪的 HTTP 请求和相应的响应。

Fig2_Requests.png

当我们仔细查看任何一个 HTTP 请求和响应时,有几件有趣的事情。

首先,缺少 Content-Encoding 响应标头(值为 GzipDeflate)意味着服务器在发送内容到浏览器之前没有压缩每个资源的内容,尽管浏览器在 Accept-Encoding 请求标头中表明它可以接受 GzipDeflate 流。

此外,Web 服务器根本没有生成 ExpiresCache-ControlETag 标头。

最后,如果您查看某个特定 JavaScript 或 CSS 文件(例如下图所示)的实际响应,您会看到它的内容是以原始形式发送的,包含完整的注释、制表符和空格等。所有这些的结果是消耗了更多的带宽,并且浏览器没有得到适当的指示如何最好地缓存资源内容。

Fig3_Response.png

现在,让我们使用 Combres 来解决这些问题。

步骤 1 - 创建 Combres 配置文件

首先,在您的应用程序中引用 Combres.dll。(这是一个包含在二进制下载中的合并程序集;Combres 实际上依赖于一些第三方程序集,因此如果您不使用合并程序集,还需要添加对这些第三方库的引用。) 之后,添加一个名为 Combres.xml 的 XML 文件,并将其放在 App_Data 文件夹中 (如果您愿意,可以使用其他文件名和文件夹)。现在,为了方便编辑 XML 文件,在 Visual Studio .NET 的工具栏中,选择 XML > 架构。单击添加,然后选择 combres.xsd 架构文件,该文件随 Combres 的下载一起提供。这样做有助于在编辑 Combres 的配置文件时获得 Intellisense 支持。

Fig4_Schema.png

将以下内容添加到 Combres.xml

<?xml version="1.0" encoding="utf-8" ?>
<combres xmlns='urn:combres'>
  <resourceSets url="~/combres.axd" defaultDuration="30" 
                                defaultVersion="auto" 
                                defaultDebugEnabled="auto" >
    <resourceSet name="siteCss" type="css">
      <resource path="~/styles/site.css" />
      <resource path="~/styles/jquery-ui-1.7.2.custom.css" />
    </resourceSet>
    <resourceSet name="siteJs" type="js">
      <resource path="~/scripts/jquery-1.3.2.js" />
      <resource path="~/scripts/jquery-ui-1.7.2.custom.min.js" />
    </resourceSet>
  </resourceSets>
</combres>	

上面的 XML 文件定义了 2 个资源集,每个资源集包含 2 个资源。您可以在应用程序中添加任意数量的资源集和资源。resourceSets 元素的属性需要解释。

url 属性指定了 Combres 内容生成器的起始 URL。defaultDuration 属性定义了 Combres 用来指示浏览器和 Combres 本身缓存内容的持续天数。为了使客户端和服务器端的缓存失效,Combres 需要更改资源集的请求 URL。在这里,我们通过为 defaultVersion 属性指定 auto 来指示 Combres 检测资源集的变化(例如,资源内容、配置设置等)并自动生成新的 URL。最后,我们希望 Combres 重用 web.config 中指定的调试模式,因此我们将 defaultDebugEnabled 的值设置为 auto(关于调试稍后详述)。这些默认值(包括本文中提到的所有以“default”开头的属性值)将用于所有资源集,尽管任何资源集都可以根据需要覆盖这些值。

高级用法

外部和动态生成的资源

如果 JavaScript 文件或 CSS 文件是应用程序外部的,您需要按如下方式引用它:

<resource path="http://sub.domain.com/jquery-1.3.2.js" mode="dynamic" />

请注意,您必须指定完整 URL 并将 mode 更改为 dynamic。这告诉 Combres 打开一个 HTTP 请求来下载所需的资源,而不是尝试从文件系统读取它。同样,如果您有一个在运行时动态生成的本地资源,例如通过某个 HTTP 处理程序,那么您也需要使用动态模式。例如:

<resource path="~/generator.aspx?file=jquery-1.3.2.js" mode="dynamic" /> 
共享资源集中的资源

如果您的网站很大,其中一些资源集共享一些公共资源,那么您可以定义一个公共资源集并让其他资源集引用它,而不是在多个资源集中重复这些资源声明。例如:

<resourceSet name="commonJs" type="js">
    <!-- some resources -->
</resourceSet>
<resourceSet name="anotherSet" type="js">
    <!-- some resources -->
    <resource reference="commonJs"/>
    <!-- some resources -->
</resourceSet>
<resourceSet name="yetAnotherSet" type="js">
    <!-- some resources -->
    <resource reference="commonJs"/>
    <!-- some resources -->
</resourceSet>
禁用压缩

如果您不希望 Combres 为您压缩内容(通常是因为您依赖于其他压缩机制,如硬件压缩),那么您可以将 defaultCompressionEnabled 设置为 false

步骤 2 - 修改 web.config

下一步是修改 web.config 来执行 2 个操作:

  1. 为 Combres 添加一个配置节,并在该节中指定 Combres 配置文件的位置。
     <configSections>
          <section name="combres" type="Combres.ConfigSectionSetting, Combres, 
    	Version=2.0.0.0, Culture=neutral, PublicKeyToken=49212d24adfbe4b4"/>
     </configSections>
     <combres definitionUrl="~/App_Data/combres.xml"/>
  2. 注册 UrlRoutingModule,这是一个 Combres 依赖的用于注册其路由的 HTTP 模块。
    <httpModules>
            <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, 
    	System.Web.Extensions, Version=3.5.0.0, Culture=neutral, 
    	PublicKeyToken=31BF3856AD364E35"/>
            <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, 
    	System.Web.Routing, Version=3.5.0.0, Culture=neutral, 
    	PublicKeyToken=31BF3856AD364E35"/>
    </httpModules>

步骤 3 - 注册 Combres 路由

添加全局应用程序文件 (global.ascx),并确保其代码隐藏文件包含以下内容。(请注意,AddCombresRoute 是在 Combres 命名空间中定义的扩展方法。)

using System;
using System.Web.Routing;
using Combres;
	
namespace UsingCombres
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.AddCombresRoute("Combres Route");
        }
    }
}

步骤 4 - 生成 Combres 链接

在最后一步,我们需要修改 Default.aspx 来使用 Combres API 生成资源集的链接。请注意,我们传递给 CombresLink() 方法的参数是在 Combres.xml 中定义的资源集的名称。

<%@ Import Namespace="Combres" %>
<head runat="server">
    <title>Using Combres</title>
    <%= WebExtensions.CombresLink("siteCss") %>
    <%= WebExtensions.CombresLink("siteJs") %>
</head>

完成了。运行您的应用程序,并在 FireBugs 下查看 HTTP 请求。您将看到以下捕获:

Fig5_Combres.png

让我们讨论一下应用 Combres 的效果。首先,与之前对应的 4 个不同资源的 4 个 HTTP 请求相比,我们现在只有 2 个 HTTP 请求对应于我们定义的 2 个资源集。其次,每个资源集的内容在发送到浏览器之前都经过 gzip 压缩,这通过 Content-Encoding 响应标头的值来表示。此外,还会根据您在 Combres.xml 文件中的 defaultDuration 指定的值生成适当的 Cache-ControlExpires 标头。Combres 还生成了一个 ETag,以便浏览器可以用来检查是否有更新的内容。

如果您查看实际响应,您会发现每个资源集的内容都经过了最小化处理,即删除了注释和不必要的空格。事实上,Combres 使用的 JavaScript 最小化器甚至会缩短变量名、内联函数调用和移除未使用的代码。快速计算表明,新页面只需要下载 80.4 KB 的资源,不到原始 344.2 KB 的四分之一。

Fig6_CombresResponse.png

高级用法

ASP.NET MVC 应用程序可以按照上述步骤与 Combres 集成。但是,如果您添加了 Combres.Mvc.dll 程序集的引用,那么您可以从 UrlHelperHtmlHelper 调用 CombresLink() 扩展方法(及其重载),而不是直接调用 WebExtensions 类。如果您这样做,生成 Combres 链接的代码将如下所示:

<%= Url.CombresLink("siteCss") %>
<%= Url.CombresLink("siteJs") %>

如果您必须在不允许内联代码的环境中部署 ASPX 页面(例如,CompilationMode 设置为 true,CMS 等),您可以使用 Include 自定义服务器控件来生成 Combres 链接。例如:

<%@ Register Assembly="Combres" Namespace="Combres" TagPrefix="cb" %>
<cb:Include ID="siteCssCtrl" runat="server" SetName="siteCss"/>
<cb:Include ID="siteJsCtrl" runat="server" SetName="siteJs"/>

高级主题 (Advanced Topics)

版本生成器

如果您查看 Combres 为每个资源集生成的 URL,您会发现 URL 的末尾附加了一串随机数字。这是一个 32 位整数的字符串,Combres 通过对资源集的各种元素(如资源内容、应用的过滤器和最小化设置等)进行哈希自动生成。(稍后将详细介绍过滤器和最小化设置。) 这是为了确保对任何资源文件或任何相关设置的任何更改都会强制客户端和服务器端的缓存失效。如果您担心哈希冲突的可能性,可以使用一个更“强大”的内置版本生成器 Sha512VersionGenerator,它生成 512 位版本字符串,如下所示:

 <resourceSets url="~/combres.axd" defaultDuration="30" 
                                    defaultVersion="auto" 
                                    defaultDebugEnabled="auto"
                                    defaultVersionGenerator=
				"Combres.VersionGenerators.Sha512VersionGenerator" >

如果您对这两个内置版本生成器不满意,您可以随时通过创建一个实现 Combres.IVersionGenerator 接口的类来插入您自己的实现,然后将 defaultVersionGenerator 属性的值替换为您类的完全限定名。如果您完全不希望 Combres 自动管理版本,您可以通过将 defaultVersion 更改为 auto 以外的任何值来执行手动版本控制。这样做,Combres 将不再尝试检测更改并调整版本,您需要自己控制这个过程。

Combres 中的更改检测机制通过使用 FileSystemWatcher 来监控 Combres 配置文件、JavaScript 和 CSS 文件的变化。这是一个相当轻量级的更改检测机制。然而,对于外部和动态生成的资源,此机制将不起作用。相反,Combres 需要持续检索资源的內容并与 Combres 内部内容缓存中的现有内容进行比较。这是一个昂贵的操作,应针对每个应用程序进行优化。因此,Combres 暴露了两个属性:localChangeMonitorIntervalremoteChangeMonitorInterval,它们分别指示 Combres 在对动态生成的资源和外部资源执行查找和比较操作之前等待的秒数。

最小化器

默认情况下,Combres 对 CSS 和 JavaScript 最小化使用 YUI .NET Compressor,并带有一些默认设置。您可以通过在 Combres.xml 中声明它们来创建使用不同最小化引擎和/或设置的最小化器实例。声明这些之后,您可以将这些最小化器实例应用于所有资源集、单个资源集,甚至集中的单个资源。

回到我们的示例,假设我们想为 jquery-ui-1.7.2.custom.min.js 关闭最小化,并使用 Microsoft Ajax Minifier 来最小化 jquery-1.3.2.js。此外,我们希望为 YUI CSS Compressor 使用一些自定义设置。我们可以将 Combres.xml 更改为如下:

<?xml version="1.0" encoding="utf-8" ?>
<combres xmlns='urn:combres'>
  <cssMinifiers>
    <minifier name="yui" type="Combres.Minifiers.YuiCssMinifier, Combres">
      <param name="CssCompressionType" type="string" value="StockYuiCompressor" />
      <param name="ColumnWidth" type="int" value="-1" />
    </minifier>
  </cssMinifiers>
  <jsMinifiers>
    <minifier name="msajax" type="Combres.Minifiers.MSAjaxJSMinifier, Combres" 
	binderType="Combres.Binders.SimpleObjectBinder, Combres">
      <param name="CollapseToLiteral" type="bool" value="true" />
      <param name="EvalsAreSafe" type="bool" value="true" />
      <param name="MacSafariQuirks" type="bool" value="true" />
      <param name="CatchAsLocal" type="bool" value="true" />
      <param name="LocalRenaming" type="string" value="CrunchAll" />
      <param name="OutputMode" type="string" value="SingleLine" />
      <param name="RemoveUnneededCode" type="bool" value="true" />
      <param name="StripDebugStatements" type="bool" value="true" />
    </minifier>
  </jsMinifiers>
  <resourceSets url="~/combres.axd" defaultDuration="30" 
                                defaultVersion="auto" 
                                defaultDebugEnabled="auto" >
    <resourceSet name="siteCss" type="css" minifierRef="yui">
      <resource path="~/styles/site.css" />
      <resource path="~/styles/jquery-ui-1.7.2.custom.css" />
    </resourceSet>
    <resourceSet name="siteJs" type="js">
      <resource path="~/scripts/jquery-1.3.2.js" minifierRef="msajax"  />
      <resource path="~/scripts/jquery-ui-1.7.2.custom.min.js" minifierRef="off" />
    </resourceSet>
  </resourceSets>
</combres>

要准确了解最小化引擎的每个属性的作用,请参阅每个特定最小化器类型的文档(例如 MSAjaxJSMinifier)。您也可以通过创建一个实现 Combres.IResourceMinifier 接口的类来添加您自己的自定义最小化器。

调试支持

虽然 Combres 执行的处理工作流对于生产环境中的 Web 应用程序很有用,但它在开发环境中可能会带来很多不便,因为几乎不可能理解(或调试)组合和最小化的 JavaScript 和 CSS 资源集。为了便于调试,您可以为所有资源集打开 defaultDebugEnabled 设置,或为单个资源集打开 debugEnabled 设置。如前所述,如果设置为 auto,Combres 将使用 web.config 中的当前调试设置。对于 debugEnabledtrue 的每个资源集,资源会被合并,但完全不会被最小化或缓存。Combres 还在每个资源内容的组合内容前添加一个注释,以帮助识别该资源的位置。

虽然以上使得调试更加容易,但仍然不如在没有使用 Combres 的情况下调试 JavaScript 代码或 CSS 内容那样容易。幸运的是,在 Combres 2.0 中,您可以将新属性 defaultIgnorePipelineWhenDebug 的值设置为 true,这将强制 Combres 生成指向资源的直接链接,从而绕过整个 Combres 处理管道。生成的 HTML 将与您根本不使用 Combres 时生成的 HTML 完全相同。(这意味着这些资源不会执行任何 Combres 过滤器。)

日志支持

一个用于监控 Combres 内部处理的有用工具是 Combres 日志。Combres 中的日志机制的实现方式是,可以轻松插入自定义日志提供程序。目前只有一个内置日志提供程序,Log4Net。要启用此日志提供程序,您需要修改 web.config 中的相关部分,使其看起来像这样:

    <configSections>
      <section name="combres" type="Combres.ConfigSectionSetting, 
	Combres, Version=2.0.0.0, 
	Culture=neutral, PublicKeyToken=49212d24adfbe4b4"/>
      <section name="log4net" 
	type="log4net.Config.Log4NetConfigurationSectionHandler, Combres, 
	Version=2.0.0.0, Culture=neutral, PublicKeyToken=49212d24adfbe4b4"/>
    </configSections>
    <combres definitionUrl="~/App_Data/combres.xml" 
	logProvider="Combres.Loggers.Log4NetLogger" />
    <log4net>
      <root>
        <level value="ALL"/>
        <appender-ref ref="RollingFile"/>
      </root>
      <logger name="Combres">
        <level value="DEBUG"/>
      </logger>
      <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
        <file value="log.txt"/>
        <appendToFile value="true"/>
        <maximumFileSize value="100KB"/>
        <maxSizeRollBackups value="2"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%d [%t] %-5p %c - %m%n"/>
        </layout>
      </appender>
    </log4net>

现在,如果您运行应用程序,您应该会看到 log.txt 文件被创建,并被 Combres 填充了许多日志语句。

过滤器。

在 Combres 提供的所有钩子中,没有什么比过滤器更强大的了。Combres 提供 4 种过滤器类型,每种过滤器类型在 Combres 处理管道的特定时间被调用。过滤器类必须实现以下一个或多个接口(除了基本接口 IContentFilter):

Fig7_Filters.png

CanApplyTo(),定义在 IContentFilter 中,接收一个 ResourceType enum(只有 2 个值:JSCSS),如果过滤器实现支持该资源类型则返回 true,否则返回 false。每个过滤器接口的 TransformContent() 方法具有唯一的签名,但本质上做同样的事情:接受一些内容,执行一些处理,并返回一个转换后的内容。

要理解这些过滤器类型的工作原理,我们需要了解 Combres 的处理工作流。对于每个非调试模式的资源集,Combres 首先将其资源划分为多个最小化组,每个组与相同的最小化器实例相关联。(如果为集中的所有资源只配置了一个最小化器实例,那么就只有一个最小化组。) 对于每个最小化组,Combres 将执行以下步骤。(每个步骤的输出将用作下一步的输入。)

  • 读取集中每个资源的內容,对每次读取调用实现 ISingleContentFilter 的过滤器,将资源內容传递进去,并将返回的內容添加到数组中。
  • 组合上一步中的字符串数组,将组合內容传递给实现 ICombinedContentFilter 的过滤器,并获取转换后的组合內容。
  • 最小化组合內容,将最小化后的內容传递给实现 IMinifiedContentFilter 的过滤器,并获取转换后的最小化內容。

最后,一旦有了所有最小化组的最小化內容,Combres 将它们合并在一起,压缩合并后的內容,并将压缩后的內容传递给实现 ICompressedContentFilter 的过滤器,并使用转换后的压缩內容发送到浏览器。

对于处于调试模式的资源集,工作流略有不同。要为每个资源集执行的步骤包括:

  • 读取集中每个资源的內容,对每次读取调用实现 ISingleContentFilter 的过滤器,将资源內容传递进去,并将返回的內容添加到数组中
  • 组合上一步中的字符串数组,将组合內容传递给实现 ICombinedContentFilter 的过滤器,并获取转换后的组合內容
  • 压缩组合內容,将压缩后的內容传递给实现 ICompressedContentFilter 的过滤器,并使用转换后的压缩內容发送到浏览器。

Combres 内置了 3 个过滤器:FixUrlsInCssFilterHandleCssVariablesFilterDotLessCssFilter。让我们分别看一下。

FixUrlsInCssFilter

此过滤器实现 ISingleContentFilter,解决了当 Combres 使用时,CSS 文件中的相对 URL 不再有效的问題。这是因为这些 URL 现在相对于 Combres 生成器,而不是 CSS URL,并且除非这些 CSS 文件恰好位于与 Combres 生成器相同的路径下,否则浏览器将无法解析引用的 URL。为了解决这个问题,该过滤器会修改 CSS 资源中所有引用的相对 URL,使其成为相对于应用程序根路径的绝对路径。基本上是一个显而易见的选择,只需应用该过滤器,您的 CSS 路径问題就会消失。更重要的是,该过滤器允许您使用标准的 ASP.NET 部分路径语法 ~/,这样 URL 就可以与虚拟应用程序无缝配合。

此过滤器的代码如下:

public sealed class FixUrlsInCssFilter : ISingleContentFilter
{
    private static readonly ILogger Log = LoggerFactory.CreateLogger(
                   System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    /// <inheritdoc cref="IContentFilter.CanApplyTo" />
    public bool CanApplyTo(ResourceType resourceType)
    {
        return resourceType == ResourceType.CSS;
    }

    /// <inheritdoc cref="ISingleContentFilter.TransformContent" />
    public string TransformContent(ResourceSet resourceSet, 
	Resource resource, string content)
    {
        return Regex.Replace(content, @"url\((?<url>.*?)\)",
            match => FixUrl(resource, match),
            RegexOptions.IgnoreCase | RegexOptions.Singleline | 
			RegexOptions.ExplicitCapture);
    }

    private static string FixUrl(Resource resource, Match match)
    {
        try
        {
            const string template = "url(\"{0}\")";
            bool isInSameApp = resource.IsInSameApplication;
            var url = match.Groups["url"].Value.Trim('\"', '\'');

            // Absolute URL, return as-is
            if (url.StartsWith("http", true, CultureInfo.InvariantCulture))
                return string.Format(CultureInfo.InvariantCulture, template, url);

            if (url.StartsWith("~", StringComparison.Ordinal))
            {
                // The CSS is in the same application 
                // resolve partial URLs found in the CSS to full relative paths
                if (isInSameApp)
                    return string.Format(CultureInfo.InvariantCulture, 
			template, url.ResolveUrl());
                
                /* Otherwise, attempt to treat ~ as /
                 * 
                 * @NOTE: This won't work if the remote app is in a virtual directory
                 * See my comment dated 11:00PM Monday Nov 23 in this discussion
                 * http://combres.codeplex.com/Thread/View.aspx?ThreadId=64366
                 */
                url = "/" + url.Substring(2); // 2 for the "~/"
            }

            var cssPath = resource.Path;
            if (url.StartsWith("/", StringComparison.Ordinal))
            {
                // The CSS is in the same application, keep root-based URLs as-is
                if (isInSameApp)
                    return string.Format(CultureInfo.InvariantCulture, template, url);

                // Otherwise, append root URL of the remote server/app to this url object
                var uri = new Uri(cssPath);
                return string.Format(CultureInfo.InvariantCulture, 
			template, uri.GetBase() + url);
            }

            // Relative URL in CSS mean relative to the CSS location
            // Because CSS location must either be ~/ or absolute, the ResolveUrl() 
            // at the end of this code block will do 
            var cssFolder = cssPath.Substring(0, cssPath.LastIndexOf
		("/", StringComparison.Ordinal)); // e.g. ~/content/css
            while (url.StartsWith("../", StringComparison.Ordinal))
            {
                url = url.Substring(3); // skip one '../'
                cssFolder = cssFolder.Substring(0, cssFolder.LastIndexOf
		("/", StringComparison.Ordinal)); // move back one folder
            }

            return string.Format(CultureInfo.InvariantCulture, 
		template, (cssFolder + "/" + url).ResolveUrl());
        }
        catch (Exception ex)
        {
            // Be lenient here, only log.  After all, this is 
            // just an image in the CSS file
            // and it should't be the reason to stop loading that CSS file.
            if (Log.IsWarnEnabled) 
                Log.Warn("Cannot fix url " + match.Value, ex);
            return match.Value;
        }
    }
}

HandleCssVariablesFilter

这是一个 ISingleContentFilter 过滤器,它允许您在特定的 CSS 文件中定义变量,并在文件中重复使用它们。例如,您可以有一个 CSS 文件,其中包含以下 @define 块:

@define
{
    boxColor: #345131;
    boxWidth: 150px;
}
p
{
    color: @boxColor;
    width: @boxWidth;
}

过滤器将以上内容转换为以下内容:

p
{
    color: #345131;
    width: 150px;
}

这种简单的技术是重构 CSS 文件的绝佳方式。此过滤器的实现如下:

public sealed class HandleCssVariablesFilter : ISingleContentFilter
{
    /// <inheritdoc cref="IContentFilter.CanApplyTo" />
    public bool CanApplyTo(ResourceType resourceType)
    {
        return resourceType == ResourceType.CSS;
    }

    /// <inheritdoc cref="ISingleContentFilter.TransformContent" />
    public string TransformContent(ResourceSet resourceSet, 
	Resource resource, string content)
    {
        // Remove comments because they may mess up the result
        content = Regex.Replace(content, @"/\*.*?\*/", 
		string.Empty, RegexOptions.Singleline);
        var regex = new Regex(@"@define\s*{(?<define>.*?)}", RegexOptions.Singleline);
        var match = regex.Match(content);
        if (!match.Success)
            return content;

        var value = match.Groups["define"].Value;
        var variables = value.Split(';');
        var sb = new StringBuilder(content);
        variables.ToList().ForEach(variable =>
                      {
                         if (string.IsNullOrEmpty(variable.Trim()))
                             return;
                         var pair = variable.Split(':');
                         sb.Replace("@" + pair[0].Trim(), pair[1].Trim());
                      });

        // Remove the variables declaration, it's not needed in the final output
        sb.Replace(match.ToString(), string.Empty);
        return sb.ToString();
    }
}

DotLessCssFilter

ISingleContentFilter 过滤器已在 2.0 版本中添加。它使 CSS 资源通过 dotLessCSS 处理器,该处理器提供了许多有用的功能,包括变量声明、表达式和 mixins 等。有关这些功能的更多信息,请参阅 dotLessCSS 的文档。(尽管这些功能涵盖了 HandleCssVariablesFilter 提供的功能,但 HandleCssVariablesFilterDotLessCssFilter 更轻量级,因此前者有时仍然有用。)

DotLessCssFilter 的代码如下:

public sealed class DotLessCssFilter : ISingleContentFilter
{
    private static readonly DotlessConfiguration Config = new DotlessConfiguration
                                                            {
                                                                CacheEnabled = false,
                                                                MinifyOutput = false
                                                            };

    private static readonly EngineFactory EngineFactory = new EngineFactory();

    /// <inheritdoc cref="IContentFilter.CanApplyTo" />
    public bool CanApplyTo(ResourceType resourceType)
    {
        return resourceType == ResourceType.CSS;
    }

    /// <inheritdoc cref="ISingleContentFilter.TransformContent" />
    public string TransformContent(ResourceSet resourceSet, 
	Resource resource, string content)
    {
        var tmpFile = Path.GetTempFileName();
        File.WriteAllText(tmpFile, content);
        try
        {
            return EngineFactory.GetEngine(Config).TransformToCss(tmpFile);
        } 
        finally
        {
            File.Delete(tmpFile);
        }
    }
}

使用过滤器

为了将过滤器应用于您的资源集,您需要修改 Combres 配置文件。例如,您可以将 Combres.xml 更改为如下:

<combres xmlns='urn:combres'>
  <filters>
    <filter type="Combres.Filters.FixUrlsInCssFilter, Combres" />
    <filter type="Combres.Filters.DotLessCssFilter, Combres" 
	acceptedResourceSets="dotLessCss" />
  </filters>
	...

默认情况下,Combres 将调用每个过滤器实现的 CanApplyTo() 方法来确定是否应处理某个资源集。但是,您可以通过在过滤器的 acceptedResourceSets 属性中指定资源集名称列表来更精细地控制该过程,如上所示,Combres 将确保只有这些资源集会通过该过滤器。

想了解更多?

本文未讨论 Combres 的一些其他小功能。如果您想了解这些功能,可以参考随此下载捆绑的带注释的 Combres 配置文件 (combres_full_with_annotation.xml)。此文件描述了您可以添加到 Combres 配置文件中的所有元素和属性及其作用。另一方面,对于想要探索 Combres 公共 API 和/或用新的最小化器、过滤器和日志记录器进行扩展的用户来说,CHM API 文档应该很有帮助。

结论

我希望本文提供了足够的信息,帮助您开始使用 Combres。如果您有任何反馈或建议,请随时在此处或在 项目 CodePlex 页面的讨论论坛中发表评论。谢谢,祝您旅途愉快!

© . All rights reserved.