一、简介
在对网站进行开发时,为了给开发人员以好的可读性,需要对代码进行良好的排版,给变量起有意义的名字,进行合理的注释。然而,当网页呈现在用户面前时,之前提及的都不再重要,而另一方面,网页的呈现速度则变得很重要,这时候,又需要对代码进行压缩(以减少文件大小,节省网络带宽,从而加快了页面的加载时间),删掉不必要的空白字符,缩短变量名,删除注释等。如果网站上线后仍然一直处于持续更新状态,则每次在发布前压缩代码就变成了重复性的劳动。如何简化甚至省略掉这个重复性劳动呢?
二、解决方案一
使用外置的代码压缩程序,配置它在每次网站编译前执行。(这是一种简化重复性劳动的方法)
我看到微软项目中使用了一个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),请点击下面的链接下载:
AjaxMin.zip (115.25 kb)
[donate: www.zizhujy.com]