一、简介
在对网站进行开发时,为了给开发人员以好的可读性,需要对代码进行良好的排版,给变量起有意义的名字,进行合理的注释。然而,当网页呈现在用户面前时,之前提及的都不再重要,而另一方面,网页的呈现速度则变得很重要,这时候,又需要对代码进行压缩(以减少文件大小,节省网络带宽,从而加快了页面的加载时间),删掉不必要的空白字符,缩短变量名,删除注释等。如果网站上线后仍然一直处于持续更新状态,则每次在发布前压缩代码就变成了重复性的劳动。如何简化甚至省略掉这个重复性劳动呢?
二、解决方案一
使用外置的代码压缩程序,配置它在每次网站编译前执行。(这是一种简化重复性劳动的方法)
我看到微软项目中使用了一个Crunch.exe的程序,在网站项目配置它在每次编译前执行,专门用来压缩指定的js/css文件。这个方法的确可以奏效,但是我认为它有很多缺点:
- 要引用外置程序,这很讨厌
- 要在网站项目做配置,使得它在每次编译前执行
- 要维护一个xml文件,以指定哪些js/css文件需要压缩。这很不灵活,每次新增一个需要压缩的文件,都要去修改这个xml文件
- 要配置一些难记的命令
- 由于这个Crunch.exe程序的引入,使得网站项目所在的路径中,任何文件夹名不得含有空格!否则这个程序将会执行失败,导致整个项目的编译失败!这点最令人讨厌!我曾吃过亏,在一个微软项目(给MSN的客户NBC做的网站)中,某一天我发现好端端的网站工程,老是编译失败,百思不得其解,找了很久才发现是由于这个原因!
三、解决方案二
使用HttpHandler,针对js/css文件的请求,给予压缩响应。(这从根本上省掉了发布网站前的代码压缩工作!)
这个是我从BlogEngine.NET项目中学来的。分别写好JavaScriptHandler和CssHandler,再在Web.Config中做配置,将对js文件的请求,交由JavaScriptHandler来处理,而将对css文件的请求,交由CssHandler处理。
四、解决方案二原理剖析
这个解决方案的两个Handler,分别对js文件和css文件进行压缩,不用担心影响服务器性能,因为它设置了缓存,除非源文件有修改,否则就一直调用压缩好的缓存版代码给予响应,所以针对同一代码文件的所有请求,只需要压缩一次。
这两个Handler还会对请求的查询字符串进行检查,看看是否有?minify=true这样的查询字符串存在,如果有才压缩,如果没有就不压缩。这是一个非常灵活的做法,即你可以在网站中灵活地配置是否要压缩某个代码文件。也就是说,只要你配置minify=true,则用户浏览你的网页时会非常快。如果他/她感觉你的网站很酷,想要学习一下,那么它会去查看你的源代码,这时候,他/她只需要将minify=true删除,便能够查看到你的开发版代码(可读性非常好)。(当然,如果你不愿意分享,则不要采用这种方式,你可以采用解决方案一)
对于JavaScriptHandler,这里引用了一个JavascriptMinifier的工具类。在下面的实现一节里给出了它的源代码。
五、解决方案二的实现
1. 引用JavaScriptMinifier类,下面给出它的源码,你可以直接添加到你的网站工程中。这里将它封装在了zizhujy.Utility命名空间内,你也可以修改成你自己的命名空间,只要在后面引用时也作相应修改就好。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Ajax.Utilities;namespace zizhujy.Utility { /// <summary> /// Helper class for performing minification of Javascript and CSS. /// </summary> /// <remarks> /// /// This class is basically a wrapper for the AjaxMin library(lib/AjaxMin.dll). /// http://ajaxmin.codeplex.com/ /// /// There are no symbols that come with the AjaxMin dll, so this class gives a bit of intellisense /// help for basic control. AjaxMin is a pretty dense library with lots of different settings, so /// everyone's encouraged to use it directly if they want to. /// /// </remarks> public sealed class JavascriptMinifier {
private Microsoft.Ajax.Utilities.Minifier ajaxMinifier = new Microsoft.Ajax.Utilities.Minifier(); /// <summary> /// Creates a new Minifier instance. /// </summary> public JavascriptMinifier() { this.RemoveWhitespace = true; this.PreserveFunctionNames = true; this.VariableMinification = VariableMinification.None; } #region "Methods" /// <summary> /// Builds the required CodeSettings class needed for the Ajax Minifier. /// </summary> /// <returns></returns> private CodeSettings CreateCodeSettings() { var codeSettings = new CodeSettings(); codeSettings.MinifyCode = false; codeSettings.OutputMode = (this.RemoveWhitespace ? OutputMode.SingleLine : OutputMode.MultipleLines); // MinifyCode needs to be set to true in order for anything besides whitespace removal // to be done on a script. codeSettings.MinifyCode = this.ShouldMinifyCode; if (this.ShouldMinifyCode) { switch (this.VariableMinification) { case VariableMinification.None: codeSettings.LocalRenaming = LocalRenaming.KeepAll; break; case VariableMinification.LocalVariablesOnly: codeSettings.LocalRenaming = LocalRenaming.KeepLocalizationVars; break; case VariableMinification.LocalVariablesAndFunctionArguments: codeSettings.LocalRenaming = LocalRenaming.CrunchAll; break; } // This is being set by default. A lot of scripts use eval to parse out various functions // and objects. These names need to be kept consistant with the actual arguments. codeSettings.EvalTreatment = EvalTreatment.MakeAllSafe; // This makes sure that function names on objects are kept exactly as they are. This is // so functions that other non-minified scripts rely on do not get renamed. codeSettings.PreserveFunctionNames = this.PreserveFunctionNames; } return codeSettings; } /// <summary> /// Gets the minified version of the passed in script. /// </summary> /// <param name="script"></param> /// <returns></returns> public string Minify(string script) { if (this.ShouldMinify) { if (String.IsNullOrEmpty(script)) { return string.Empty; } else { return this.ajaxMinifier.MinifyJavaScript(script, this.CreateCodeSettings()); } } return script; } #endregion #region "Properties" /// <summary> /// Gets or sets whether this Minifier instance should minify local-scoped variables. /// </summary> /// <remarks> /// /// Setting this value to LocalVariablesAndFunctionArguments can have a negative impact on some scripts. /// Ex: A pre-minified jQuery will fail if passed through this. /// /// </remarks> public VariableMinification VariableMinification { get; set; } /// <summary> /// Gets or sets whether this Minifier instance should preserve function names when minifying a script. /// </summary> /// <remarks> /// /// Scripts that have external scripts relying on their functions should leave this set to true. /// /// </remarks> public bool PreserveFunctionNames { get; set; } /// <summary> /// Gets or sets whether the <see cref="BlogEngine.Core.JavascriptMinifier"/> instance should remove /// whitespace from a script. /// </summary> public bool RemoveWhitespace { get; set; } private bool ShouldMinifyCode { get { // return true; return ((!PreserveFunctionNames) || (this.VariableMinification != VariableMinification.None)); } } private bool ShouldMinify { get { return ((this.RemoveWhitespace) || (this.ShouldMinifyCode)); } } #endregion } /// <summary> /// Represents the way variables should be minified by a Minifier instance. /// </summary> public enum VariableMinification { /// <summary> /// No minification will take place. /// </summary> None = 0, /// <summary> /// Only variables that are local in scope to a function will be minified. /// </summary> LocalVariablesOnly = 1, /// <summary> /// Local scope variables will be minified, as will function parameter names. This can have a negative impact on some scripts, so test if you use it! /// </summary> LocalVariablesAndFunctionArguments = 2 }
}
2. 在网站工程中添加JavaScriptHandler类,下面给出它的源码:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.IO; using System.Security; using System.Web.Caching; using zizhujy.Utility; using System.Net;namespace zizhujy.HttpHandlers { /// <summary> /// Removes whitespace in all stylesheets added to the handler of the HTML document /// </summary> /// <remarks> /// /// This handler uses an external library to perform minification of scripts. /// See the zizhujy.Utility.JavascriptMinifier class for more details. /// /// </remarks> public class JavaScriptHandler : IHttpHandler { #region Properties
/// <summary> /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"></see> instance. /// </summary> /// <value></value> /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.</returns> public bool IsReusable { get { return false; } } #endregion #region Implemented Interfaces /// <summary> /// Enables processing of HTTP Web requests by a custom /// HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface. /// </summary> /// <param name="context"> /// An <see cref="T:System.Web.HttpContext"/> object that provides /// references to the intrinsic server objects /// (for example, Request, Response, Session, and Server) used to service HTTP requests. /// </param> public void ProcessRequest(HttpContext context) { var request = context.Request; string path = request.Path; if (string.IsNullOrEmpty(path)) { return; } string rawUrl = request.RawUrl.Trim(); string cacheKey = context.Server.HtmlDecode(rawUrl); string script = (string)context.Cache[cacheKey]; bool minify = ((request.QueryString["minify"] != null) && (request.QueryString["minify"].ToString().Trim() != "false")); if (string.IsNullOrEmpty(script)) { script = RetrieveLocalScript(path, cacheKey, minify); } if (string.IsNullOrEmpty(script)) { return; } SetHeaders(script.GetHashCode(), context); context.Response.Write(script); } #endregion #region Methods /// <summary> /// Retrieves the local script from the disk /// </summary> /// <param name="file">The file name.</param> /// <param name="cacheKey">The key used to insert this script into the cache.</param> /// <param name="minify">Whether or not the local script should be minified</param> /// <returns>The retrieved local script.</returns> private static string RetrieveLocalScript(string file, string cacheKey, bool minify) { if(StringComparer.OrdinalIgnoreCase.Compare(Path.GetExtension(file), ".js") != 0) { throw new SecurityException("No access"); } try{ var path = HttpContext.Current.Server.MapPath(file); if(File.Exists(path)){ string script; using (var reader = new StreamReader(path)){ script = reader.ReadToEnd(); } script =ProcessScript(script, file, minify); HttpContext.Current.Cache.Insert(cacheKey, script, new CacheDependency(path)); return script; } }catch(Exception ex) { } return string.Empty; } /// <summary> /// Call this method for any extra processing that needs to be done on a script resource before /// being wriiten to the response. /// </summary> /// <param name="script"></param> /// <param name="filePath"></param> /// <param name="shouldMinify"></param> /// <returns></returns> private static string ProcessScript(string script, string filePath, bool shouldMinify) { if ((shouldMinify)) { var min = new JavascriptMinifier(); min.VariableMinification = VariableMinification.LocalVariablesOnly; return min.Minify(script); } else { return script; } } private static void SetHeaders(int hash, HttpContext context) { var response = context.Response; response.ContentType = "text/jvascript"; var cache = response.Cache; cache.VaryByHeaders["Accept-Encoding"] = true; cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7)); cache.SetMaxAge(new TimeSpan(7, 0, 0, 0)); cache.SetRevalidation(HttpCacheRevalidation.AllCaches); var etag = string.Format("\"{0}\"", hash); var incomingEtag = context.Request.Headers["If-None-Match"]; cache.SetETag(etag); cache.SetCacheability(HttpCacheability.Public); if (string.Compare(incomingEtag, etag) != 0) { return; } response.Clear(); response.StatusCode = (int)HttpStatusCode.NotModified; response.SuppressContent = true; } #endregion }
}
3. 在网站工程中添加CssHandler类,下面给出它的源码:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.IO; using System.Security; using System.Web.Caching; using System.Text.RegularExpressions; using System.Net;namespace zizhujy.HttpHandlers { /// <summary> /// Removes whitespace in all stylesheets added to the header of the HTML document. /// </summary> public class CssHandler : IHttpHandler { #region Properties
/// <summary> /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"></see> instance. /// </summary> /// <value></value> /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.</returns> public bool IsReusable { get { return false; } } #endregion #region Implemented Interfaces /// <summary> /// Enables processing of HTTP Web request by a custom /// HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface. /// </summary> /// <param name="context"> /// An <see cref="T:System.Web.HttpContext"/> object that provides /// references to the intrinsic server objects /// (for example, Request, Response, Session, and Server) used to server HTTP requests. /// </param> public void ProcessRequest(HttpContext context) { var request = context.Request; string path = request.Path; if (!string.IsNullOrEmpty(path)) { if (StringComparer.InvariantCultureIgnoreCase.Compare(Path.GetExtension(path), ".css") != 0) { throw new SecurityException("Invalid CSS file extension"); } string cacheKey = request.RawUrl.Trim(); string css = (string)context.Cache[cacheKey]; bool minify = ((request.QueryString["minify"] != null) && (request.QueryString["minify"].ToString().Trim() != "false")); if (String.IsNullOrEmpty(css)) { css = RetrieveLocalCss(path, cacheKey, minify); } // Make sure css isn't empty if (!string.IsNullOrEmpty(css)) { // Configure response headers SetHeaders(css.GetHashCode(), context); context.Response.Write(css); } else { context.Response.Status = "404 Bad Request"; } } } #endregion #region Methods /// <summary> /// This will make the browser and server keep the output /// in its cache and thereby improve performance. /// </summary> /// <param name="hash"> /// The hash number. /// </param> /// <param name="context"> /// The context. /// </param> private static void SetHeaders(int hash, HttpContext context) { var response = context.Response; response.ContentType = "text/css"; var cache = response.Cache; cache.VaryByHeaders["Accept-Encoding"] = true; cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7)); cache.SetMaxAge(new TimeSpan(7, 0, 0, 0)); cache.SetRevalidation(HttpCacheRevalidation.AllCaches); var etag = string.Format("\"{0}\"", hash); var incomingEtag = context.Request.Headers["If-None-Match"]; cache.SetETag(etag); cache.SetCacheability(HttpCacheability.Public); if (String.Compare(incomingEtag, etag) != 0) { return; } response.Clear(); response.StatusCode = (int)HttpStatusCode.NotModified; response.SuppressContent = true; } /// <summary> /// Retrieves the local CSS from the disk /// </summary> /// <param name="file"> /// The file name. /// </param> /// <param name="cacheKey"> /// The key used to insert this script into the cache. /// </param> /// <returns> /// The retrieve local css. /// </returns> private static string RetrieveLocalCss(string file, string cacheKey, bool minify) { var path = HttpContext.Current.Server.MapPath(file); try { string css; using (var reader = new StreamReader(path)) { css = reader.ReadToEnd(); } css = ProcessCss(css, minify); HttpContext.Current.Cache.Insert(cacheKey, css, new CacheDependency(path)); return css; } catch { return string.Empty; } } /// <summary> /// Call this method to do any post-processing on the css before its returned in the context response. /// </summary> /// <param name="css"></param> /// <returns></returns> private static string ProcessCss(string css, bool minify) { if (minify) { css = StripWhitespace(css); return css; } else { return css; } } /// <summary> /// Strips the whitespace from any .css file. /// </summary> /// <param name="body"> /// The body string. /// </param> /// <returns> /// The strip whitespace. /// </returns> private static string StripWhitespace(string body) { body = body.Replace(" ", " "); body = body.Replace(Environment.NewLine, String.Empty); body = body.Replace("\t", string.Empty); body = body.Replace(" {", "{"); body = body.Replace(" :", ":"); body = body.Replace(": ", ":"); body = body.Replace(", ", ","); body = body.Replace("; ", ";"); body = body.Replace(";}", "}"); // sometimes found when retrieving CSS remotely body = body.Replace(@"?", string.Empty); // body = Regex.Replace(body, @"/\*[^\*]*\*+([^/\*]*\*+)*/", "$1"); body = Regex.Replace( body, @"(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,}(?= )|(?<=&ndsp;)\s{2,}(?=[<])", String.Empty); // Remove comments from CSS body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty); return body; } #endregion }
}
4. 在Web.Config中作映射配置:
如果是IIS 7.5 或以上,则只需要作如下配置即可:
<?xml version="1.0" encoding="utf-8"?><!--
有关如何配置 ASP.NET 应用程序的详细信息,请访问
http://go.microsoft.com/fwlink/?LinkId=152368
-->
<configuration>
... <system.webServer> <validation validateIntegratedModeConfiguration="false"/> <modules runAllManagedModulesForAllRequests="true"/> <handlers> <add name="ZiZhuJYJavaScriptHandler" path="*.js" verb="*" type="zizhujy.HttpHandlers.JavaScriptHandler, zizhujy" resourceType="Unspecified" preCondition="integratedMode"/> <add name="ZiZhuJYCssHandler" path="*.css" verb="*" type="zizhujy.HttpHandlers.CssHandler, zizhujy" resourceType="Unspecified" preCondition="integratedMode"/> </handlers> </system.webServer> ...
</configuration>
如果是IIS 5.1,则要按这样的格式作配置:
<?xml version="1.0" encoding="utf-8"?><!--
有关如何配置 ASP.NET 应用程序的详细信息,请访问
http://go.microsoft.com/fwlink/?LinkId=152368
-->
<configuration>
... <system.web> <httpHandlers> <add path="*.js" verb="*" type="zizhujy.HttpHandlers.JavaScriptHandler, zizhujy" validate="false"/> <add path="*.css" verb="*" type="zizhujy.HttpHandlers.CssHandler, zizhujy" validate="false"/> </httpHandlers> </system.web> ...
</configuration>
5. 通过在需要压缩的文件名后面添加查询字符串?minify=true来启动压缩:
<!DOCTYPE html><html>
<head> ...
<link href="/Content/css/functionGraffitiStyle.css?minify=true" rel="stylesheet" type="text/css" /> <link href="/Scripts/syntaxhighlighter_3.0.83/styles/shCore.css?minify=true" rel="stylesheet" type="text/css" /> <link href="/Scripts/syntaxhighlighter_3.0.83/styles/shThemeDefault.css?minify=true" rel="Stylesheet" type="text/css" /> <script src="/Scripts/flot/jquery.flot.js?minify=true" type="text/javascript"></script> <script src="/Scripts/FunctionGraffiti/jGraffiti-Math.js?minify=true" type="text/javascript"></script> <script src="/Scripts/FunctionGraffiti/jGraffiti.js?minify=true" type="text/javascript"></script>
...
</head>
<body>
...
</body>
</html>
六、解决方案二的总结
个人认为解决方案二是代码压缩的最佳实践方案,它同时对开发人员和用户友好。和解决方案一相比,它有这些优点:
- 它是内包的,不需要引用外部的奇怪程序,封装性好
- 它不插手编译过程,它是在第一次响应请求时进行压缩,之后调用缓存版本
- 不用维护另外的xml文件,只需要对那些你想要压缩的文件名后面添加一个查询字符串?minify=true即可启动压缩
- ?minify=true这个查询字符串太直观太好记了
- 没有格外的文件夹名要求,因此不会因为它的失败引起奇怪的编译过程错误
七、相关文件下载
解决方案二需要在项目中引用一个DLL文件(AjaxMin.dll),请点击下面的链接下载:
[donate: www.zizhujy.com]