即时或在构建时连接和压缩JavaScript - ASP.NET MVC

47
作为这个问题 Linking JavaScript Libraries in User Controls 的补充,我想要一些人们如何在运行时或构建时连接和压缩JavaScript的例子。我还想看看它如何与您的主页面一起工作。
我不介意将特定于页面的文件像现在一样(请参见下文)进行压缩和链接,但是我希望将所有主要主页面上的JavaScript文件(我大约有5或6个)连接并压缩。
如果有人还能将CSS连接和压缩结合起来,则得到奖励分! :-)
包含我想要连接和压缩的公共JavaScript文件的当前主页面:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<head runat="server">
    ... BLAH ...
    <asp:ContentPlaceHolder ID="AdditionalHead" runat="server" />
    ... BLAH ...
    <%= Html.CSSBlock("/styles/site.css") %>
    <%= Html.CSSBlock("/styles/jquery-ui-1.7.1.css") %>
    <%= Html.CSSBlock("/styles/jquery.lightbox-0.5.css") %>
    <%= Html.CSSBlock("/styles/ie6.css", 6) %>
    <%= Html.CSSBlock("/styles/ie7.css", 7) %>
    <asp:ContentPlaceHolder ID="AdditionalCSS" runat="server" />
</head>
<body>
    ... BLAH ...
    <%= Html.JSBlock("/scripts/jquery-1.3.2.js", "/scripts/jquery-1.3.2.min.js") %>
    <%= Html.JSBlock("/scripts/jquery-ui-1.7.1.js", "/scripts/jquery-ui-1.7.1.min.js") %>
    <%= Html.JSBlock("/scripts/jquery.validate.js", "/scripts/jquery.validate.min.js") %>
    <%= Html.JSBlock("/scripts/jquery.lightbox-0.5.js", "/scripts/jquery.lightbox-0.5.min.js") %>
    <%= Html.JSBlock("/scripts/global.js", "/scripts/global.min.js") %>
    <asp:ContentPlaceHolder ID="AdditionalJS" runat="server" />
</body>

在这样的页面中使用(我对此感到满意):

<asp:Content ID="signUpContent" ContentPlaceHolderID="AdditionalJS" runat="server">
    <%= Html.JSBlock("/scripts/pages/account.signup.js", "/scripts/pages/account.signup.min.js") %>
</asp:Content>

更新:现在(2013年底)的建议:

我会看一下微软ASP.NET的内置Bundling和Minification


1
有人使用YUI有什么解决方案吗? - Charlino
“Duck's nuts” 是指某物好还是不好? - Mark
我必须问一下,“鸭子的坚果”是否可接受... <g> - Jonesome Reinstate Monica
@Charlino 现在有更好的方法吗?我四处搜索了一下,找到的所有 SO 帖子和博客都是基于旧的 3.5 方法。 - Jonesome Reinstate Monica
@Jonesome - 我认为现在最好的方法是使用ASP.NET捆绑和缩小。请参见此处:http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification - Charlino
显示剩余2条评论
8个回答

41

尝试这个:

我最近在工作中完成了大量的研究和开发,以显著提高我们网页应用程序前端的性能。我想在这里分享基本的解决方案。

首先明显的是,使用Yahoo的YSlow和Google的PageSpeed对您的网站进行基准测试。这些工具将突出显示需要进行的“低成本”性能改进。除非您已经这样做,否则得到的建议几乎肯定包括合并,缩小和压缩静态内容。

我们要执行的步骤为:

编写自定义HTTPHandler以组合和缩小CSS。 编写自定义HTTPHandler以组合和缩小JS。 包括一种机制,以确保上述仅在应用程序不处于调试模式时才发挥其魔力。 编写自定义服务器端Web控件以便于维护css/js文件包含。 启用IIS 6上某些内容类型的GZIP。 好的,让我们从实现.NET IHttpHandler接口的CSSHandler.asax开始吧:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;

namespace WebApplication1
{
    public class CssHandler : IHttpHandler
    {
        public bool IsReusable { get { return true; } }

        public void ProcessRequest(HttpContext context)
        {
            string[] cssFiles = context.Request.QueryString["cssfiles"].Split(',');

            List<string> files = new List<string>();
            StringBuilder response = new StringBuilder();
            foreach (string cssFile in cssFiles)
            {
                if (!cssFile.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
                {
                    //log custom exception
                    context.Response.StatusCode = 403;
                    return;
                }

                try
                {
                    string filePath = context.Server.MapPath(cssFile);
                    string css = File.ReadAllText(filePath);
                    string compressedCss = Yahoo.Yui.Compressor.CssCompressor.Compress(css);
                    response.Append(compressedCss);
                }
                catch (Exception ex)
                {
                    //log exception
                    context.Response.StatusCode = 500;
                    return;
                }
            }

            context.Response.Write(response.ToString());

            string version = "1.0"; //your dynamic version number 

            context.Response.ContentType = "text/css";
            context.Response.AddFileDependencies(files.ToArray());
            HttpCachePolicy cache = context.Response.Cache;
            cache.SetCacheability(HttpCacheability.Public);
            cache.VaryByParams["cssfiles"] = true;
            cache.SetETag(version);
            cache.SetLastModifiedFromFileDependencies();
            cache.SetMaxAge(TimeSpan.FromDays(14));
            cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
        }
    }
}

好的,现在有些解释:

IsReUsable属性:

我们没有处理任何特定于实例的内容,这意味着我们可以安全地重复使用处理程序的同一实例来处理多个请求,因为我们的ProcessRequest是线程安全的。更多信息请参见此处。

ProcessRequest方法:

这里没有太过复杂的操作。我们正在循环遍历传递给我们的CSS文件(请参见下面的CSSControl以了解它们是如何传入的),并使用Yahoo的YUICompressor的.NET移植版压缩每个文件,然后将内容添加到输出响应流中。

该方法的其余部分涉及设置一些HTTP缓存属性,以进一步优化浏览器客户端下载(或不下载)内容的方式。

我们在代码中设置ETag,以便它们可以在我们的服务器群中的所有计算机上相同。 我们在实际文件上设置了Response和Cache依赖项,以便更换它们时会使缓存失效。 我们设置Cacheability以使代理可以缓存。 我们使用cssfiles属性进行VaryByParams,以便我们可以根据通过处理程序提交的CSS文件组进行缓存。 这就是CSSControl,一个自定义的服务器端控件,继承自.NET LiteralControl。

前端:

<customcontrols:csscontrol id="cssControl" runat="server">
  <CustomControls:Stylesheet File="main.css" />
  <CustomControls:Stylesheet File="layout.css" />
  <CustomControls:Stylesheet File="formatting.css" />
</customcontrols:csscontrol>

返回:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Linq;
using TTC.iTropics.Utilities;

namespace WebApplication1
{
    [DefaultProperty("Stylesheets")]
    [ParseChildren(true, "Stylesheets")]
    public class CssControl : LiteralControl
    {
        [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
        public List<Stylesheet> Stylesheets { get; set; }

        public CssControl()
        {
            Stylesheets = new List<Stylesheet>();
        }

        protected override void Render(HtmlTextWriter output)
        {
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                const string format = "<link rel=\"Stylesheet\" href=\"stylesheets/{0}\"></link>";

                foreach (Stylesheet sheet in Stylesheets)
                    output.Write(format, sheet.File);
            }
            else
            {
                const string format = "<link type=\"text/css\" rel=\"Stylesheet\" href=\"stylesheets/CssHandler.ashx?cssfiles={0}&version={1}\"/>";
                IEnumerable<string> stylesheetsArray = Stylesheets.Select(s => s.File);
                string stylesheets = String.Join(",", stylesheetsArray.ToArray());
                string version = "1.00" //your version number

                output.Write(format, stylesheets, version);
            }

        }
    }

    public class Stylesheet
    {
        public string File { get; set; }
    }
}

HttpContext.Current.IsDebuggingEnabled被连接到您的web.config文件中的以下设置:

<system.web>
  <compilation debug="false">
</system.web>

基本上,如果您的网站处于调试模式,您将获得以下HTML标记:

<link rel="Stylesheet" href="stylesheets/formatting.css"></link>
<link rel="Stylesheet" href="stylesheets/layout.css"></link
<link rel="Stylesheet" href="stylesheets/main.css"></link>

但是如果你处于生产模式(debug=false),你将会得到这样的标记:

<link type="text/css" rel="Stylesheet" href="CssHandler.ashx?cssfiles=main.css,layout.css,formatting.css&version=1.0"/>

接下来显然会调用CSSHandler,它将负责合并、压缩和缓存准备您的静态CSS内容。

以上所有内容也可以为您的静态JavaScript内容复制:

`JSHandler.ashx:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;

namespace WebApplication1
{
    public class JSHandler : IHttpHandler
    {
        public bool IsReusable { get { return true; } }

        public void ProcessRequest(HttpContext context)
        {
            string[] jsFiles = context.Request.QueryString["jsfiles"].Split(',');

            List<string> files = new List<string>();
            StringBuilder response = new StringBuilder();

            foreach (string jsFile in jsFiles)
            {
                if (!jsFile.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
                {
                    //log custom exception
                    context.Response.StatusCode = 403;
                    return;
                }

                try
                {
                    string filePath = context.Server.MapPath(jsFile);
                    files.Add(filePath);
                    string js = File.ReadAllText(filePath);
                    string compressedJS = Yahoo.Yui.Compressor.JavaScriptCompressor.Compress(js);
                    response.Append(compressedJS);
                }
                catch (Exception ex)
                {
                    //log exception
                    context.Response.StatusCode = 500;
                    return;
                }
            }

            context.Response.Write(response.ToString());

            string version = "1.0"; //your dynamic version number here

            context.Response.ContentType = "application/javascript";
            context.Response.AddFileDependencies(files.ToArray());
            HttpCachePolicy cache = context.Response.Cache;
            cache.SetCacheability(HttpCacheability.Public);
            cache.VaryByParams["jsfiles"] = true;
            cache.VaryByParams["version"] = true;
            cache.SetETag(version);
            cache.SetLastModifiedFromFileDependencies();
            cache.SetMaxAge(TimeSpan.FromDays(14));
            cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
        }
    }
}

并且它相应的JS控制器:

前端:

<customcontrols:JSControl ID="jsControl" runat="server">
  <customcontrols:Script File="jquery/jquery-1.3.2.js" />
  <customcontrols:Script File="main.js" />
  <customcontrols:Script File="creditcardpayments.js" />
</customcontrols:JSControl>

返回:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Linq;

namespace WebApplication1
{
    [DefaultProperty("Scripts")]
    [ParseChildren(true, "Scripts")]
    public class JSControl : LiteralControl
    {
        [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
        public List<Script> Scripts { get; set; }

        public JSControl()
        {
            Scripts = new List<Script>();
        }

        protected override void Render(HtmlTextWriter writer)
        {
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                const string format = "<script src=\"scripts\\{0}\"></script>";

                foreach (Script script in Scripts)
                    writer.Write(format, script.File);
            }
            else
            {
                IEnumerable<string> scriptsArray = Scripts.Select(s => s.File);
                string scripts = String.Join(",", scriptsArray.ToArray());
                string version = "1.0" //your dynamic version number
                const string format = "<script src=\"scripts/JsHandler.ashx?jsfiles={0}&version={1}\"></script>";

                writer.Write(format, scripts, version);
            }
        }
    }

    public class Script
    {
        public string File { get; set; }
    }
}

启用Gzip:

正如Jeff Atwood所说,启用Web服务器上的Gzip压缩是一件轻而易举的事情。在跟踪后,我决定对以下文件类型启用Gzip压缩:

.css .js .axd(Microsoft Javascript文件) .aspx(通常的ASP.NET Web Forms内容) .ashx(我们的处理程序) 要在IIS 6.0 Web服务器上启用HTTP压缩,请执行以下操作:

打开IIS,右键单击Web Sites,选择服务选项卡,启用“压缩应用程序文件”和“压缩静态文件” 停止IIS 在记事本中打开IIS元数据库(C:\WINDOWS\system32\inetsrv\MetaBase.xml)-如果您对这些东西感到紧张,请备份 找到并覆盖两个IIsCompressionScheme和一个IIsCompressionSchemes元素,如下所示:

就这样!这为我们节省了大量带宽,并使Web应用程序更加响应。

享受吧!


3
哇,这是非常详细的回复,绝对值得在博客上发表!如果适合您的网站,那绝对是一个好的解决方案。对于我的网站来说,所有需要合并的js和css都已经合并了,所以我不需要如此复杂的解决方案。同时,我已经启用了gzip压缩。此外,我还为我的js和css文件设置了远期过期头,并在一个无cookie域上自动版本控制 - 但这又是另一个问题了! - Charlino
1
几年过去了,世界已经发生了变化,尽管我需要在我的新雇主处重新解决这个问题。毫无疑问,我现在建议使用Cassette:http://getcassette.net/ - Mark Gibaud

14

为什么不使用ScriptManager? 这里有一个MVCScriptManager 可以合并和压缩脚本文件。


2
那看起来是一个很好的即时拼接和压缩选项。但我肯定更倾向于构建时间解决方案。没有额外负担,更加清洁,而且我可以在那里处理CSS :-) - Charlino

7

看起来很不错,我得试一下。我听说过'Packer'的坏话,但我发现它也支持'JSMin'。 - Charlino
虽然看起来很不错,但 YUI Compress 似乎有一个优点,就是它可以进行 CSS 压缩和合并。 - Bayard Randel
Packer for .NET 也可以进行 CSS 的合并和压缩 - 可以查看链接 :-) 不过,确实有听说 YUI Compress 在压缩 JS 和 CSS 方面做得比其他任何工具都要好。 - Charlino

6
请使用YUI压缩器或Dojo压缩器。它们都使用Rhino JS解析引擎对您的代码进行标记化,因此仅在代码有效时才能正常工作。如果出现错误,它们会通知您(在我看来这是一个很好的奖励!)。另一方面,Packer会压缩您的代码,即使其中包含错误。
我通过构建脚本在所有项目中使用YUI。不要在运行时执行,这需要太长时间来进行压缩。YUI和Dojo都基于Java(ala Rhino),如果您在运行时执行,则将生成后台进程以生成输出-这对性能不利。请始终在构建时执行。

4
Rejuicer是一个非常受欢迎的ASP.NET缩小工具,正在引起很多关注:http://rejuice.me 它被配置为HTTP模块,并在运行时(一次)执行缩小并缓存输出。
它具有以下特点:
- 具有流畅的配置接口 - 允许您使用通配符规则指定要缩小的文件 - 在Windows Azure上运行 - 在开发环境中以某种神奇的方式自动关闭,因此您可以调试原始JavaScript代码(未缩小)。
配置(在global.asax.cs的ApplicationStart上完成)就像这样简单:
OnRequest.ForJs("~/Combined.js")
            .Compact
            .FilesIn("~/Scripts/")
              .Matching("*.js")
            .Cache
            .Configure();

2
以下是我用于连接、压缩和缓存CSS和JS文件的方法:http://gist.github.com/130913。只需要在bin目录中添加Yahoo.Yui.Compressor.dll即可。它不会在编译时进行压缩,但是这些文件会被缓存,并且有一个文件依赖项,因此只会加载一次,直到它们被更改。
然后我只需将以下代码添加到标签中:
<link rel="stylesheet" type="text/css" href="/YuiCompressor.ashx?css=reset,style,etc" />

并且这段代码需要放在 </body> 标签之前:

<script type="text/javascript" src="/YuiCompressor.ashx?js=main,other,etc"></script>

它被设计用于处理在同一路径下的多个文件,但很容易升级以支持不同的路径。


2
我使用基于MSBuild和Microsoft Ajax Minifier的定制解决方案。许多现有的博客文章并没有正确处理某些情况,例如与TFS构建的集成。
对于每个Web项目,我们创建一个"wpp.targets"文件来扩展Web发布管道。例如,如果项目是"Website.csproj",则在项目中创建名为"Website.wpp.targets"的文件。
将以下代码放入目标文件中:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath32)\PATH TO YOUR MSBUILD MINIFY TARGETS" />

  <!-- Hook up minification task to WPP build process -->
  <PropertyGroup>
    <OnAfterPipelineTransformPhase>
      $(OnAfterPipelineTransformPhase);
      MinifyResourceFiles;
    </OnAfterPipelineTransformPhase>
  </PropertyGroup>

  <!-- Define temporary location to store minified resources -->
  <PropertyGroup>
    <MinifyResourceIntermediateOutput Condition="'$(MinifyResourceIntermediateOutput)'==''">MinifyResourceFiles</MinifyResourceIntermediateOutput>
    <MinifyResourceIntermediateLocation Condition="'$(MinifyResourceIntermediateLocation)'==''">$(_WPPDefaultIntermediateOutputPath)$(MinifyResourceIntermediateOutput)</MinifyResourceIntermediateLocation>
  </PropertyGroup>

  <Target Name="MinifyResourceFiles" DependsOnTargets="PipelineCollectFilesPhase" Condition="'$(Configuration)' == 'Release'">
    <!-- Create lists of the resources to minify -->
    <!-- These extract all Javascript and CSS files from the publishing pipeline "FilesForPackagingFromProject" and create two new lists.
     The "MinifiedFile" metadata on each item contains the temporary location where the minified file will be stored -->
    <ItemGroup>
      <JavaScriptToMinify Include="@(FilesForPackagingFromProject)" 
                          Condition="'%(FilesForPackagingFromProject.Extension)' == '.js'">
        <MinifiedFile>$(MinifyResourceIntermediateLocation)\minified\%(DestinationRelativePath)</MinifiedFile>
      </JavaScriptToMinify>
      <StylesheetToMinify Include="@(FilesForPackagingFromProject)"
                          Condition="'%(FilesForPackagingFromProject.Extension)' == '.css'">
        <MinifiedFile>$(MinifyResourceIntermediateLocation)\minified\%(DestinationRelativePath)</MinifiedFile>
      </StylesheetToMinify>    
    </ItemGroup>

    <!-- Minify resources -->
    <!-- These commands should be replaced with the MSBuild Tasks used to perform your minification
         I use my own custom tasks based on the Microsoft Ajax Minifier DLL 
         The input of the minifier takes a source file directly from the project and outputs to a temporary location -->
    <MinifyJavaScript SourceFiles="@(JavaScriptToMinify)" DestinationFiles="@(JavaScriptToMinify->'%(MinifiedFile)')"
                      Comments="None" />
    <MinifyStylesheet SourceFiles="@(StylesheetToMinify)" DestinationFiles="@(StylesheetToMinify->'%(MinifiedFile)')"
                      Comments="None" />

    <!-- Remove the original source files from the packaging system and include the new minfied resources from the temporary location -->
    <ItemGroup>
      <!--Remove unminified resources from the pipeline -->
      <FilesForPackagingFromProject Remove="@(JavaScriptToMinify)" Condition="'@(JavaScriptToMinify)' != ''" />
      <FilesForPackagingFromProject Remove="@(StylesheetToMinify)" Condition="'@(StylesheetToMinify)' != ''" />
      <!--Add the minified resources at the new loction to the pipeline -->
      <FilesForPackagingFromProject Include="@(JavaScriptToMinify->'%(MinifiedFile)')" Condition="'@(JavaScriptToMinify)' != ''"/>
      <FilesForPackagingFromProject Include="@(StylesheetToMinify->'%(MinifiedFile)')" Condition="'@(StylesheetToMinify)' != ''"/>
    </ItemGroup>
  </Target>
</Project>

“$(Configuration') == 'Release'”条件可以根据您的需求进行修改,该条件会在发布、打包和服务器构建时自动缩小(并验证)项目中的所有CSS和JS文件。

您可能需要为服务器构建启用WPP“CopyWebApplication”目标。要执行此操作,请将MSBuild属性UseWP_CopyWebApplication设置为True,并将PipelineDependsOnBuild设置为False。我们在包含Web应用程序目标文件之前在项目文件中设置这些属性。


2
我推荐使用http://www.RequestReduce.com来最小化和合并CSS和JavaScript,以及整合CSS背景图像和优化它们的PNG压缩。它可以在运行时完成所有这些,并缓存输出结果。除了添加HttpModule之外,它不需要任何代码或配置。它使用经过优化的远期标头和ETags来提供所有缓存的内容,以确保浏览器尽可能长时间地缓存CSS/JavaScript/sprites。虽然它不需要配置,但它是高度可配置的,可以设置为与CDN一起运行,并在Web Farm上同步缓存文件。
所有JavaScript、图像和CSS都通过HTTP获取,因此它可以包括第三方的CSS和JS,并且也是压缩/合并.axd资源(如WebResource.axd和ScriptResource.axd)的好方法。它通过内容类型来确定JS和CSS的存在,因此目标资源可以具有任何(或没有)扩展名。它可以运行在任何基于IIS的技术上,包括所有版本和视图引擎的MVC、Web Forms和“Web Pages”。
你可以从http://www.RequestReduce.com、Nuget 或者从https://github.com/mwrock/RequestReduce进行下载或分支。

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