[翻译]ASP.NET MVC4新特性之脚本压缩和合并
目前主流浏览器限制客户端对同一域名只能同时发起6(PS:原文如此)个HTTP连接。 这意味着,打开一个网页只能同时加载6个HTTP请求,在同一个域名下其他的请求将被浏览器加入到请求队列中。 在IE浏览器中按F12调出开发人员工具,切换到网络标签,如下图所示,显示的是一个实例网站关于HTTP资源请求的情况。
灰色的进度条显示的是当前请求等待的时间,浏览器通过队列来实现其他资源的依次加载。 黄色的进度条显示的是客户端与服务器建立请求所花费的时间。 蓝色条显示的是当前资源从服务器下载完毕所花费的时间。 你可以双击当前请求查询详细情况。 例如,下图显示的是加载/ Scripts/MyScripts/JavaScript6.js文件整个请求过程的详细情况。
上面的图片中显示了当前资源在开始事件中被浏览器加入了请求队列。因为浏览器同时请求限制的影响,当前资源必须等待46毫秒完成上一个HTTP请求才能执行当前的请求。
合并
合并是ASP.NET 4.5中的新功能,使开发者很容易实现把多个文件合并成一个文件。 你可以实现CSS、javascript脚本以及其他文件的合并功能。合并多个文件意味着减少了HTTP请求的个数,同时提高了页面的加载速度。
下图显示了开启了脚本合并功能后打开该网站的HTTP请求情况。
压缩
压缩功能实现了对javascript脚本和CSS进行压缩的功能,它能够去除脚本或样式中不必要的空白和注释,同时能够优化脚本变量名的长度。 我们来看下面这段JavaScript函数。
AddAltToImg = function (imageTagAndImageID, imageContext) { ///<signature> ///<summary> Adds an alt tab to the image // </summary> //<param name="imgElement" type="String">The image selector.</param> //<param name="ContextForImage" type="String">The image context.</param> ///</signature> var imageElement = $(imageTagAndImageID, imageContext); imageElement.attr('alt', imageElement.attr('id').replace(/ID/, '')); }
压缩后,该函数被合并到了一列:
AddAltToImg = function (n, t) { var i = $(n, t); i.attr("alt", i.attr("id").replace(/ID/, "")) }
除了删除注释和多余的空格外,其变量名也被缩短为如下这样:
原变量 | 缩短后 |
imageTagAndImageID | n |
imageContext | t |
imageElement | i |
合并和压缩脚本对网站性能的影响
下面表格显示了脚本被合并压缩后对网站性能提升的影响。
| 合并和压缩后 | 未压缩和合并 | 性能提升 |
HTTP请求数 | 9 | 34 | 256% |
发送字节(KB) | 3.26 | 11.92 | 266% |
响应字节(KB) | 388.51 | 530 | 36% |
耗时(ms) | 510 MS | 780 MS | 53% |
与未优化的网站相比,优化后不但减少了HTTP请求头的大小,同时请求文件的大小也有着明显的减少。 不同文件的压缩大小是不一样的,该网站的最大的脚本文件已经是压缩过的(Scripts\jquery-ui-1.8.11.min.js and Scripts\jquery-1.7.1.min.js) 。
注:网站的耗时实例是通过Fiddler工具来实现的 。 (从Fiddler的Rules 菜单中选择Performance 然后选择 Simulate Modem Speeds )。
调试以及压缩Javascript
在开发环境下因为JavaScript脚本不会被压缩和合并,所以调试JavaScript是件很容易的事情(在 Web.config文件中compilation节点设置debug="true" )。你可以调试一个发布版本的JavaScript用于生产环境。 使用IE F12开发人员工具,调试JavaScript脚本的方法如下:
1 选择“脚本 ”选项卡,然后选择“开始调试”按钮。
2 选择你要调试的脚本文件。
3. 选择“配置” 按钮 ,格式化压缩后的JavaScript,然后选择“格式后的JavaScript”按钮。
4. 你还可以通过搜索方法来检索你需要调试的函数。 在下面的图片,在搜索输入框中输入AddAltToImg,搜索结果会以高亮方式显示。
关于开发人员工具调试的更多信息,请参阅MSDN文章使用开发人员工具调试JavaScript错误 。
配置脚本压缩和合并功能
在 Web.config文件中compilation节点设置debug 的值可以开启或关闭压缩和合并功能。 在下面的XML中, debug设置值为true,可以禁用脚本压缩和合并功能。
<system.web> <compilation debug="true" /> <!-- Lines removed for clarity. --> </system.web>
如果要启用脚本压缩和合并,则设置debug 为false 。你可以通过BundleTable 类的EnableOptimizations 属性来覆盖Web.config中的设置。 下面的代码演示了如果通过BundleTable 来覆盖Web.config文件中的设置:
public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); // Code removed for clarity. BundleTable.EnableOptimizations = true; }
注意 :除非设置EnableOptimizations为true或设置Web.config文件中compilption 节点的debug属性为false,否则程序是不会合并和压缩文件的。 此外,系统也不会选择压缩过的脚本,而是选择调试版本。 EnableOptimizations属性的设置将会覆盖Web.config中的设置。
在ASP.NET Web窗体和Web Pages中使用脚本合并和压缩功能
- 优化Web Pages 请点击这里查看 Adding Web Optimization to a Web Pages Site.。
- 优化 ASP.NET Web窗体项目请点击这里查看 Adding Bundling and Minification to Web Forms。
在ASP.NET MVC中使用压缩和合并功能
在本节中,我们将创建一个ASP.NET MVC项目,来体验压缩和合并功能。 首先,创建一个新的ASP.NET MVC Internet项目名为MvcBM ,其他设置默认。
打开App_Start \BundleConfig.cs文件并查找RegisterBundles方法,该方法是用来创建、注册和配置需要压缩和优化的脚本文件的。下面的代码显示了部分RegisterBundles方法。
public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); // Code removed for clarity. }
上面的代码我们创建了一个新的命名为~/bundles/jquery 的JavaScript bundle,并包含了合适的 脚本文件(用于压缩和合并,这里不包含.vsdoc),还可以通过通配符或关键字来匹配脚本文件夹下不同版本的脚本文件。 ASP.NET MVC 4 中,系统默认集成了jquery-1.7.1.js 库。在release版本中,系统将选择jquery-1.7.1.min.js 包含在项目中。 脚本合并框架遵循以下几个原则:
- 当“FileX.min.js” 和“FileX.js” 同时存在时,在release版本中,系统将选择文件名包含“.min”的脚本
- 请选择文件名中不包含“.min”的文件进行调试。
- 请过滤文件名中包含“.vsdoc”的文件(如jQuery-1.7.1-vsdoc.js),该文件仅仅用于VS的智能感知提示。
{version}关键字是用来包含不同版本的jQuery库的。 在这个例子中,使用通配符存在以下几个好处:
- 允许你在不改变原有配置的情况下通过NuGet 更新你的jQuery版本。
- 自动选择调试版本和文件名中带有“.min”的发行版本。
使用CDN
CDN jQuery 库将取代本地jQuery库。
public static void RegisterBundles(BundleCollection bundles) { //bundles.Add(new ScriptBundle("~/bundles/jquery").Include( // "~/Scripts/jquery-{version}.js")); bundles.UseCdn = true; //enable CDN support //add link to jquery on the CDN var jqueryCdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js"; bundles.Add(new ScriptBundle("~/bundles/jquery", jqueryCdnPath).Include( "~/Scripts/jquery-{version}.js")); // Code removed for clarity. }
在上面的代码中,在发行版中将使用CDN 上的jQuery库,而本地调试的时候使用本地版本jQuery库。 当使用CDN 版本时,你应该建立一个容错机制,预防CDN加载失败的情况。 下面的代码演示了如果在CDN 中的jQuery库加载失败的情况下如何加载本地版本。
</footer> @Scripts.Render("~/bundles/jquery") <script type="text/javascript"> if (typeof jQuery == 'undefined') { var e = document.createElement('script'); e.src = '@Url.Content("~/Scripts/jquery-1.7.1.js")'; e.type = 'text/javascript'; document.getElementsByTagName("head")[0].appendChild(e); } </script> @RenderSection("scripts", required: false) </body> </html>
创建一个Bundle
Bundle类Include方法接受一个字符串或数组类型,其中每个字符串是一个虚拟的文件路径。其中RegisterBundles方法在App_Start /BundleConfig.cs 文件中演示了如何将多个文件添加到一个文件中:
bundles.Add(new StyleBundle("~/Content/themes/base/css").Include( "~/Content/themes/base/jquery.ui.core.css", "~/Content/themes/base/jquery.ui.resizable.css", "~/Content/themes/base/jquery.ui.selectable.css", "~/Content/themes/base/jquery.ui.accordion.css", "~/Content/themes/base/jquery.ui.autocomplete.css", "~/Content/themes/base/jquery.ui.button.css", "~/Content/themes/base/jquery.ui.dialog.css", "~/Content/themes/base/jquery.ui.slider.css", "~/Content/themes/base/jquery.ui.tabs.css", "~/Content/themes/base/jquery.ui.datepicker.css", "~/Content/themes/base/jquery.ui.progressbar.css", "~/Content/themes/base/jquery.ui.theme.css"));
Bundle类的IncludeDirectory方法可以添加一个目录中的所有的(和所有子目录)相匹配文件。 Bundle类IncludeDirectory API是如下所示:
public Bundle IncludeDirectory( string directoryVirtualPath, // The Virtual Path for the directory. string searchPattern) // The search pattern. public Bundle IncludeDirectory( string directoryVirtualPath, // The Virtual Path for the directory. string searchPattern, // The search pattern. bool searchSubdirectories) // true to search subdirectories.
在页面中可以使用Render方法来引用创建好的包(JavaScript对应了Scripts.Render 而CSS对应的是Styles.Render )。下面的代码演示了在Views\Shared\_Layout.cshtml 文件中如果引用CSS和JavaScript的:
<!DOCTYPE html> <html lang="en"> <head> @* Markup removed for clarity.*@ @Styles.Render("~/Content/themes/base/css", "~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> @* Markup removed for clarity.*@ @Scripts.Render("~/bundles/jquery") @RenderSection("scripts", required: false) </body> </html>
请注意Render方法的参数是一个字符串或数组类型,它允许你在一个方法中添加多个引用。 一般情况下,你可以使用Render方法来自动引用文件而省略了其他HTML标签。你可以使用Url的方法来生成一个文件URL绝对路径。 假设你想使用的HTML5 异步属性。 下面的代码演示了如何使用Url方法引用Modernizr 包。
<head> @*Markup removed for clarity*@ <meta charset="utf-8" /> <title>@ViewBag.Title - MVC 4 B/M</title> <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" /> <meta name="viewport" content="width=device-width" /> @Styles.Render("~/Content/css") @* @Scripts.Render("~/bundles/modernizr")*@ <script src='@Scripts.Url("~/bundles/modernizr")' async> </script> </head>
使用通配符“*”来匹配文件
在Include 和 IncludeDirectory方法中可以通过通配符“*”来匹配路径中某一些相同元素的文件。匹配时不区分路径中的大小写。 IncludeDirectory方法具有搜索子目录的功能。
假如一个项目下有如下JavaScript文件:
- Scripts\Common\AddAltToImg.js
- Scripts\Common\ToggleDiv.js
- Scripts\Common\ToggleImg.js
- Scripts\Common\Sub1\ToggleLinks.js
下面的表演示了使用通配符来添加如图所示文件到项目中:
使用通配符 | 匹配到的文件或异常提示 |
Include("~/Scripts/Common/*.js") | AddAltToImg.js, ToggleDiv.js, ToggleImg.js |
Include("~/Scripts/Common/T*.js") | 无效的匹配模式,通配符只能在文件的前缀或后缀中使用。 |
Include("~/Scripts/Common/*og.*") | 无效的匹配模式,一个路径中只能使用一个通配符。 |
"Include("~/Scripts/Common/T*") | ToggleDiv.js, ToggleImg.js |
"Include("~/Scripts/Common/*") | 无效的匹配模式,仅使用一个通配符是没有任何意义的。 |
IncludeDirectory("~/Scripts/Common", "T*") | ToggleDiv.js, ToggleImg.js |
IncludeDirectory("~/Scripts/Common", "T*",true) | ToggleDiv.js, ToggleImg.js, ToggleLinks.js |
通常情况下,我们应该首选通过完整路径来添加文件,其原因如下:
- 使用通配符添加脚本其添加顺序是根据文件名的字母排序来依次添加的,有时候因为脚本相互依赖的情况,这种添加方式可能会出错。 CSS和JavaScript文件经常因为相互依赖而不能按照文件名排序来添加。为了减少潜在的风险,你可以通过添加一个自定义IBundleOrderer来添加文件,但是通过完整文件名来添加文件是最保险的。 例如,将来需要添加新文件时,你可能要通过修改 IBundleOrderer类才能实现。
- 在JavaScript中使用通配符来添加一个目录的所有脚本到项目中可能会引起JavaScript的错误提示。
- 在加载CSS文件时,可能会出现加载重复的情况,如下所示的例子:
bundles.Add(new StyleBundle("~/jQueryUI/themes/baseAll") .IncludeDirectory("~/Content/themes/base", "*.css"));
通配符“*.CSS”可以匹配文件夹下所有的CSS文件,包括Content\themes\base\jquery.ui.all.css 。Jquery.ui.all.css可能在其他文件中已经存在。
包缓存
合并后的系统设置了HTTP过期时间为一年。如果你重复打开了一个也没,在服务器的版本为未改变的情况下,服务器将会返回一个HTTP 304状态码,这样浏览器会加载缓存中的文件。 你也可以在IE浏览器下通过按Ctrl + F5来强制刷新页面,这时浏览器会重新请求文件,而不是从缓存中读取(此时服务器响应的HTTP状态码为200)。
下面的图像显示了在Fiddler中“Caching”选项卡缓存文件的情况 :
请求的路径为
http://localhost/MvcBM_time/bundles/AllMyScripts?v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81
其中AllMyScripts是优化后的包名,V = r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81是当前包的一个特定字符,用于缓存当前的文件。 只要服务器端包不改变,ASP.NET应用程序将一直使用AllMyScripts包中的该标记。 如果包中的文件存在变化,ASP.NET程序将会生成一个新的字符串,以便于刷新缓存,使客户端得到新的文件。
If you run the IE9 F12 developer tools and navigate to a previously loaded page,
IE incorrectly shows conditional GET requests made to each bundle and the server returning HTTP 304.
You can read why IE9 has problems determining if a conditional request was made
in the blog entry Using CDNs and Expires to Improve Web Site Performance. (该段不会翻译)。
在LESS, CoffeeScript, SCSS, Sass中使用脚本优化功能
合并和压缩框架提供了一个机制来处理SCSS 、Sass 、LESS和CoffeeScript并转换并合并到优化包中。 例如,添加 LESS 文件到你的MVC4项目:
- 创建一个文件夹用于添加你的LESS文件。 下面的示例演示了如何使用 Content\MyLess folder。
- 通过NuGet来添加合适的包到你的项目中:
- 添加一个类,并实现 IBundleTransform 接口。然后可以通过下面的代码把LESS添加到你的项目中:
using System.Web.Optimization; public class LessTransform : IBundleTransform { public void Process(BundleContext context, BundleResponse response) { response.Content = dotless.Core.Less.Parse(response.Content); response.ContentType = "text/css"; } }
- 创建一个Bundle对象,然后添加 LessTransform 和CssMinify ,接下来在 App_Start\BundleConfig.cs 中使用 RegisterBundles 方法来注册你的方法。
var lessBundle = new Bundle("~/My/Less").IncludeDirectory("~/My", "*.less"); lessBundle.Transforms.Add(new LessTransform()); lessBundle.Transforms.Add(new CssMinify()); bundles.Add(lessBundle);
- 调用Bundle包如下所示:
@Styles.Render("~/My/Less");
合并的注意事项
当你使用Bundle对象来创建一个包时应该用"bundle" 来做前缀,这样可以防止路由冲突 。
一旦你的包中存在一个文件更新,程序会生成一个新的查询参数来使客户端在下一次访问的时候获取到最新的版本。在传统的单独列出文件引用程序中,客户端仅会重新请求修改过的文件,但是在这种新的方式下,会重新下载整个包中的文件,因此,如果你的包经常变动,使用这种合并方式也许并不是一个好的选择。
文件的合并和压缩主要是为了改善页面在第一次加载的时候文件下载所消耗的时间。当该页面加载完毕,当再一次打开该页面时,浏览器会从缓存中读取这些缓存资源(JavaScript,CSS和图像),所以,在打开同一站点的其他页面,这种方式并不能提高网站的性能(PS:因为不合并和压缩的文件也可以被缓存)。如果文件的过期设置不正确,浏览器会重复请求该文件,这种情况下合并和压缩文件会提高非第一次打开网页的性能。 有关详细信息,请参阅Using CDNs and Expires to Improve Web Site Performance 。
通过CDN可以改善浏览器同站点同时请求限制的问题。在CDN环境下,客户端将从不同的域名来请求暑假,这个时候同一网站的资源会被缓存在不同的主机商,从而加快网站打开速度,同时CDN还提供了数据缓存的功能。
System.Web.Optimization命名空间包含在System.Web.Optimization.DLL文件中。 它压缩功能利用的是WebGrease库(WebGrease.dll),然后使用Antlr3.Runtime.dll。
Additional Resources
· Video: Bundling and Optimizing by Howard Dierking
· Adding Web Optimization to a Web Pages Site.
· Adding Bundling and Minification to Web Forms.
· Performance Implications of Bundling and Minification on Web Browsing by Henrik F Nielsen @frystyk
· Using CDNs and Expires to Improve Web Site Performance by Rick Anderson @RickAndMSFT
· Minimize RTT (round-trip times)
Contributors
· Hao Kung
· Diana LaRose
原文地址:http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification