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

Combres - WebForm & MVC 客户端资源组合库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (8投票s)

2009年11月1日

Apache

13分钟阅读

viewsIcon

53677

downloadIcon

3129

一个 .NET 库,用于为 ASP.NET 和 ASP.NET MVC Web 应用程序实现 JavaScript 和 CSS 资源的缩小、压缩、合并和缓存。简单地说,它可以帮助您的应用程序在 YSlow 和 PageSpeed 中获得更好的排名。

引言

许多 Web 开发人员可能都知道 YSlow 文档Steve Souders 的书中描述的各种网站优化技术。这些技术大多非常简单,但对大多数网页的下载时间产生了巨大的影响。尽管它们很简单,但在所有 .NET Web 应用程序中一遍又一遍地应用其中一些规则很容易变成一项繁琐的任务。由于我无法从互联网上找到满足我现有需求的解决方案,我开始实现 Combres(以前称为客户端资源合并库),这是一个非常易于使用的库,可用于自动化在 MVC 和 Web Form ASP.NET 应用程序中应用某些优化技术时必须手动完成的许多步骤。

Combres 的主要功能

  • 将资源文件(包括 JavaScript 和 CSS)组织成独立的资源集;每个资源集可以共享相同的或使用不同的配置设置。
    • 配置设置在 XML 文件中指定,该文件由 Combres 监控,以便立即注意到并应用更改。
    • 资源文件可以是 Web 服务器中的静态文件、动态生成的文件,或来自外部服务器或 Web 应用程序的远程文件。
  • 允许将资源集中的文件进行合并、缩小和 Gzip 压缩,然后发送到浏览器。所有这些都通过每个资源集的单个 HTTP 请求完成。(请参阅 Yslow 的性能规则 #1、#4 和 #10,了解为什么这很有用。)
  • 为每个响应生成正确的 ETag 和 Expires/Cache-Control 标头,并支持服务器端缓存。(请参阅 Yslow 的性能规则 #3 和 #13,了解为什么这很有用。)
  • 与 ASP.NET 路由引擎集成,因此在 ASP.NET MVC 和 ASP.NET WebForm 应用程序中同样适用。
  • 支持调试模式,该模式根本不会缓存或缩小内容以方便调试。
  • 通过过滤架构实现可扩展性。任何人都可以通过编写自定义过滤器轻松地向 Combres 引擎添加更多功能。1.0 Beta 版本中有两个内置过滤器,我将在本文中进行描述。

使用 Combres 的步骤

步骤 1. 修改 web.config 文件以注册 Combres 配置文件的路径

将以下元素添加到您的 web.config 文件中

您可以对 definitionUrl 使用任何路径,只要它是部分 ASP.NET 路径(即以“~/”开头)。如果您不想,不必将其放在 App_Data 文件夹中,但将应用程序数据文件放在那里是一个很好的约定。

在此示例中,App_Data 文件夹中的文件 combres.xml 将成为 Combres 库的配置文件。您可能想知道为什么我选择将配置设置放在另一个文件中,而不是利用现有的 web.config 文件。原因是对 web.config 的任何更改都会强制应用程序重新启动,我认为没有人会希望仅仅因为对 CSS 和 JavaScript 文件进行简单的更改(例如重命名文件或将其移动到另一个文件夹)就经历如此耗时的过程。

步骤 2. 在上一步指定的目录中创建 Combres 配置文件

让我们看一个 Combres 配置文件示例,我将解释每个元素和属性的含义。

上述文件(可选)定义了用于扩展 Combres 标准行为的过滤器列表。我稍后会解释什么是过滤器;现在,先暂时忘记它们。

Combres 配置文件中最重要的是资源集定义。正如我前面提到的,您可以将 JavaScript 和 CSS 文件组织成独立的资源集。这个示例配置文件定义了三个资源集:一个 CSS 集和两个 JavaScript 集(注意 resourceSet 元素的 type 属性值)。当然,您可以定义的资源集数量没有限制。

每个资源集包含一个或多个相同类型(CSS 或 JavaScript)的资源。所有这些文件将一起合并、缩小、Gzip 压缩和缓存,并以单个 HTTP 请求的形式请求。

我们来看一下 resourceSet 元素的属性

  • name:资源集的名称。资源集不能同名,如果存在两个同名的资源集,Combres 将抛出验证异常。资源集的名称用于构成请求集中所有资源合并内容的 URL 路径。
  • type:'css' 或 'js'。
  • duration:浏览器和服务器缓存资源集计算结果的持续时间(天)。这可以节省大量时间,因为资源集中所有资源进行合并、缩小和 Gzip 压缩的过程成本很高。为什么要同时支持客户端缓存和服务器端缓存?因为用户可以通过各种方式轻松地使他们的浏览器缓存失效(例如按下 Ctrl-F5 或删除浏览器数据等),在这种情况下,浏览器将发送一个新的 HTTP 请求来获取数据,您不会希望仅仅因为一个特定用户的操作而重新进行整个计算过程。
    • 如果未为资源集指定持续时间,则它将继承自 resourceSets 元素的 defaultDuration 属性。两者中至少必须指定一个。
  • version:每个资源集都必须有一个版本,可以是任何数字或字符串。如果版本更改,将使浏览器缓存和服务器缓存失效。因此,如果资源集中的一个或多个文件已更新并准备好进行测试或投入生产,您将需要更改资源集的版本,以便可以使用新内容。
    • 如果未为资源集指定版本,则它将继承自 resourceSets 元素的 defaultVersion 属性。两者中至少必须指定一个。
  • debugEnabled:在开发期间,您可能不希望内容从缓存中提供,并强制您不断增加资源集的版本。此外,如果您要进行任何客户端调试,您也不希望看到 CSS 和 JavaScript 的缩小版本。默认情况下,debugEnabledfalse;如果设置为 true,Combres 将禁用所有缓存机制,包括不向浏览器发出缓存头,以及忽略缩小步骤。
    • 如果未为资源集指定 debugEnabled,则它将继承自 resourceSets 元素的 defaultDebugEnabled 属性。两者都可以省略,在这种情况下,假定值为 false

现在,让我们看一下 resource 元素的属性。

  • path:JavaScript 或 CSS 资源的 URL。如果资源与您的 Web 应用程序一起部署,它们的 URL 必须以 ~/ 开头,以便 Combres 可以将它们解析为正确的 URL,无论您的应用程序是否部署在虚拟目录中。如果资源是远程的(例如,来自 CDN),它们的 URL 可以是任何内容。
  • modeRemoteLocalDynamicLocalStatic(默认)。
    • Remote:Combres 使用 HTTP 从远程服务器(或 Web 应用程序)请求和检索资源。
    • LocalDynamic:与 Remote 相同,尽管 Combres 解析 URL 的方式不同。
    • LocalStatic:Combres 直接从文件系统读取资源。

步骤 3. 注册路由

请注意 resourceSets 元素中的 URL 属性。您在此处指定的任何值都将由 ASP.NET 路由引擎用于映射 Combres 处理管道。在此示例中,我使用 combres.axd,尽管您可以使用任何名称和扩展名(只要您将 IIS 配置为将该扩展名路由到 ASP.NET;axd 扩展名已开箱即用)。您接下来需要做的是将该路由注册到 ASP.NET 路由引擎。根据您是在 ASP.NET MVC 应用程序还是 ASP.NET WebForms 应用程序上工作,步骤略有不同。

ASP.NET MVC

路由模块默认已为 ASP.NET MVC 应用程序注册,因此您只需执行几个步骤。在 global.asax 中的路由注册例程中,调用 routes.AddCombresRoute("Combres")。(AddCombresRoute()Combres 命名空间中类型中定义的一个扩展方法,因此您需要将 Combres 命名空间导入到您的 global.asax 中,即“using Combes”。)请注意,由于我使用 axd 扩展名,我需要将调用放在 routes.IgnoreRoute("{resource}.axd/{*pathInfo}") 之前,该调用由 ASP.NET MVC 项目模板默认添加;否则,路由引擎将忽略我们的 Combres 路由。

mvc.asax.png

Combres 现在可以通过“combres.axd”路径提供合并内容。接下来,在我们需要使用 JS 和 CSS 文件的视图中,添加指向资源集的链接。以下示例展示了一个使用我们在配置文件中定义的两个资源集的 MVC 视图

mvc.view.png

CombresUrlCombresLinkCombres 命名空间中定义的扩展方法,因此请先将此命名空间导入您的页面。CombresUrl 仅生成 URL,而 CombresLink 根据资源集的类型生成完整的脚本或链接标签。这是生成的代码

mvc.view.generated.png

请注意生成的漂亮 URL:combres.axd 后跟资源集名称及其当前版本。在 URL 中包含版本是 Combres 为控制客户端浏览器缓存行为而做的事情之一。应该就是这样了。如果您运行应用程序并使用 FireBugs 等工具反汇编 HTTP 请求和响应,例如对 siteCss 资源集的请求,您将看到以下内容

browser.headers.png

browser.response.png

以上截图中有几点值得注意

  • 内容已进行 Gzip 压缩。(如果您的浏览器不支持,则不会进行 Gzip 压缩。Combres 会自动检测。)
  • 根据您的配置发送了适当的 Cache-Control 和 Expires 标头。ETag 也已计算并发送到浏览器。
  • CSS 资源已合并到一个请求中并进行了缩小。

ASP.NET WebForm

截至 ASP.NET 3.5 SP1,路由模块默认未在 ASP.NET 项目模板中注册。因此,您需要执行多个步骤来注册该模块。有一个很棒的分步教程向您展示如何执行此操作,因此我在此处不再赘述。一旦路由模块集成到 ASP.NET 管道中,请按如下方式注册 Combres 路由

form.asax.png

要在 ASPX 网页中使用 Combres,请按如下操作

form.aspx.png

WebExtensionsCombres 命名空间中定义的一个类型,因此请先将此命名空间导入您的页面。事情将与上面的 ASP.MVC 示例完全相同。

了解过滤器

过滤器是 Combes 启用的一种机制,通过拦截关键转换阶段,帮助开发人员向 Combres 的标准处理管道添加自定义逻辑。为了实现过滤器,您需要通过覆盖其三个方法来实现 ICombresFilter 接口

filter.png

下图展示了 Combres 在其处理管道中如何与过滤器交互,图中仅显示了相关部分。在每个拦截点,所有注册到 Combres 的过滤器都将被实例化并调用。每个过滤器都有机会修改上一阶段的输出,修改后的内容将进入下一阶段。

filter.pipeline.png

在每个拦截点,都会动态实例化一个过滤器类型的实例,并根据当前阶段调用其方法之一。这种调用方式是为了简化自定义过滤器的开发:如果您愿意,可以拥有实例变量,并且在实现过滤器时不必考虑线程安全。您也不必担心性能(因为这里使用了大量的反射),因为 Combres 内部使用了我开发的另一个库 Fasterflect,它将反射调用转换为接近本机调用。

开发过滤器后,您需要通过配置文件将其注册到 Combres。(现在,您可以回过头查看示例配置文件中的 filters 元素。)每个过滤器都需要以其类型在 filter 元素中出现。

Combres 附带了两个内置过滤器:HandleCssVariablesFilterFixUrlsInCssFilter

让我们看看它们的作用以及它们是如何实现的。

HandleCssVariablesFilter

这个过滤器的想法来自于 Rory Neopoleon 的一篇博客文章。基本上,它允许您在 CSS 文件中定义变量。例如,您可以有一个带有以下 @define 块的 CSS

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

过滤器会将内容转换为以下内容

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

这种简单的技术是重构 CSS 文件的好方法。让我们看看这个过滤器的实现。

public class HandleCssVariablesFilter : ICombresFilter
{
    public string TransformSingleContent(Settings settings, 
                  Resource resource, string content)
    {
        if (resource.ParentSet.Type != ResourceType.CSS)
            return content;

        // Remove comments because it may mess up the result
        content = Regex.Replace(content, @"/\*.+?\*/", "", 
                                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(var =>
                      {
                         if (string.Empty == var.Trim())
                             return;
                         var pair = var.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();
    }

    public string TransformCombinedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }

    public string TransformMinifiedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }
}

请注意,TransformSingleContent() 被重写,因为我们希望此过滤器按每个 CSS 文件工作。其他方法只是简单地返回原始输入。有些人可能会被大量的文本操作及其对性能的影响所吓倒。实际上,这几乎不是问题,这得益于 Combres 支持的缓存机制。处理很可能只进行一次,然后内容将从浏览器或服务器的缓存中提供数天甚至数月,具体取决于您的配置设置以及您更新内容的频率。

FixUrlsInCssFilter

这是一个很棒的过滤器,它解决了许多 Combres 用户报告的一个问题。问题是 CSS 文件中引用的 URL 被浏览器解释为相对于 CSS 文件位置的路径。因此,当使用 Combres 提供 CSS 内容时,除非 Combres 注册的路由与 CSS 文件在同一文件夹中开始(不太可能,因为您的应用程序中可能有许多不同文件夹中的 CSS 文件),否则浏览器可能无法解析引用 URL 的正确路径(这会导致图像无法显示等问题)。FixUrlsInCssFilter 就是为了解决这个问题而构建的。更重要的是,它允许您使用标准的 ASP.NET 部分路径,例如以“~/”开头的 URL。

例如,假设 CSS 文件的路径是 ~/content/site.css,那么过滤器会对 CSS 文件中的每个 URL 引用执行以下转换

  • 绝对路径
    • /path/to/image.gif 变为 /path/to/image.gif
  • 相对于 CSS 位置 -> 相对于 CSS 位置
    • path/to/image.gif 变为 /content/path/to/imag2.gif
    • ../path/to/style1.css 变为 /path/to/style1.css
  • 从应用程序根目录开始
    • ~/path/to/style2.css 变为 /content/path/to/style2.css

让我们看看这个过滤器是如何实现的

public class FixUrlsInCssFilter : ICombresFilterreadonly {
    ILog Log = LogManager.GetLogger(
                   System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    public string TransformSingleContent(Settings settings, 
                  Resource resource, string content)
    {
        if (resource.ParentSet.Type == ResourceType.CSS)
            return Regex.Replace(content, @"url\((?<url>.*?)\)", 
                match => FixUrl(resource.Path, match),
                RegexOptions.IgnoreCase | RegexOptions.Singleline | 
                RegexOptions.ExplicitCapture);
        return content;
    }

    private static string FixUrl(string cssPath, Match match)
    {
        try
        {
            const string template = "url(\"{0}\")";
            var url = match.Groups["url"].Value.Trim('\"', '\'');
            if (url.StartsWith("/"))
                return string.Format(template, url);
            if (url.StartsWith("~"))
                return string.Format(template, url.ResolveUrl());

            var cssFolder = cssPath.Substring(0, cssPath.LastIndexOf("/"));
            var backFolderCount = Regex.Matches(url, @"\.\./").Count;
            for (int i = 0; i < backFolderCount; i++)
            {
                url = url.Substring(3); // skip a '../'
                cssFolder = cssFolder.Substring(0, cssFolder.LastIndexOf("/"));
                // move back 1 folder
            }
            return string.Format(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;
        }
    }

    public string TransformCombinedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }

    public string TransformMinifiedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }
}

结论

在本文中,我展示了如何在 ASP.NET 和 ASP.NET MVC 应用程序中轻松使用 Combres 来应用许多网站性能优化技术。我还演示了 Combres 通过过滤机制的可扩展性。有了这个机制,您可以轻松扩展 Combres 以满足您自己的需求。

Combres 的源代码附带两个示例应用程序,一个 ASP.NET MVC 和一个 ASP.NET WebForm。您可以使用这些示例中的配置和代码来快速启动和运行您的 Web 应用程序与 Combres。

我很乐意听到您的意见。请在此处或在 CodePlex 上的项目讨论板中发表您的评论。

© . All rights reserved.