改善System.Web.Optimization進行壓縮時,引發將重要註解移除、CSS變數解析出錯、壓縮排序混亂之問題

System.Web.Optimization的好大家都知道,但是缺點也是無敵多,與其說是缺點,應該是說創造者賦予它太多相關的規定制度,或者是對於其彈性使用零封裝參數來對應,這在大家實際的應用過程中,是一種非常痛苦的經驗。

把重要註解全部移除,但某些情境需要

在Javascript、Css的原始檔案裡面,註解通常被區分成兩種,一種是真正的註解,另外一種是版權(版本)宣告,並不是說真的要尊重版權到將其保留(jQuery團隊也不見得會去責難你這間小公司),而是有時候我們真的需要這個plug-in套件的相關資訊時,很不幸的這些資訊都被移除了。當然,你可以選擇關閉不要用壓縮它,或者直接跑回Server端的資訊查看即可,但是,這樣的需求在某些環境下,確實有其存在的必要。

一個典型的Javascript、Css重要註解(Important Comments)如下:

/*!
 * 這裡是重要註解,通常記錄著套件名稱、版權與版本資訊
 */

但...是的,System.Web.Optimization預設就將註解全部移除掉,且沒有留下任何修改通道的參數。唯一的解決方式就是自己把System.Web.Optimization.IBundleBuilder介面全部實作一次,蠻無奈的。

CSS變數(Css Variables;Css CustomProperties)的寫法,導致解析出錯

這算是一種語法解析器未更新,導致於新的語法出現時,引發剖析失敗的問題。例如在Bootstrap ver.4以後,就開始使用新式的Css Variables寫法,例如下面的範例

:root {
	--global-color: #666;
	--pane-padding: 5px 42px;
}

這樣的「--」寫法將會導致System.Web.Optimization裡面的CSS語法解析器崩潰,進而跳出Exception。同樣的,這樣的問題System.Web.Optimization並沒有留一條退路讓你注入一些設定文件,你必須自己實作System.Web.Optimization.IBundleBuilder介面,然後在Microsoft.Ajax.Utilities.CssSettings()中,將其IgnoreAllErrors。

System.Web.Optimization引入Javascript順序問題

Javascript這種東西本身就有次序性問題,例如最常見的jQuery Framework一定是擺放在第一位,接著才有可能是套件或其餘的框架之類的。System.Web.Optimization最大的問題就是他自己有實作自己的順序引入規則,但這些規則都是用名稱來決定的,所以往往你會不小心踩到雷,例如你裡面有一個套件名稱相近,會被它識別為JS核心框架,於是它就先將這個錯誤的套件先引入了。

我認為這個特性蠻好笑的,大概是「過度設計」的最佳典範。試想,會使用System.Web.Optimization來進行整體網站打包作業的,不知道前端的引入順序有幾人?(如果你真的不知道順序,那你還會用System.Web.Optimization嗎?)內建的優先框架排序作業,在根本上就是一個完全沒有必要的設計。

這個問題算是有解,可以使用注入的方式,把自己實作的介面掛上去即可。

將以上問題的解決方式,綜合成下列的類別程式碼:

namespace Slashlook.Bundle
{
	/* -----
	 * 本類別用來實作保留Javascript、Css之重要註解(Important Comments)
	 * 如果沒有此需求,應該回歸使用System.Web.Optimization.XXXBundle。
	 * -----
	 */

	/* ***** Javascript 保留重要註解 實作區 ***** */

	/// <summary>
	/// (繼承)Javascript Bundle
	/// </summary>
	public class CommentScriptBundle : System.Web.Optimization.Bundle
	{
		public CommentScriptBundle(string virtualPath) : base(virtualPath)
		{ this.Builder = new CommentScriptBundler(); }

		public CommentScriptBundle(string virtualPath, string cdnPath) : base(virtualPath, cdnPath)
		{ this.Builder = new CommentScriptBundler(); }
	}

	/// <summary>
	/// (實作介面)Javascript Bundler
	/// </summary>
	public class CommentScriptBundler : System.Web.Optimization.IBundleBuilder
	{
		public virtual string BuildBundleContent(System.Web.Optimization.Bundle oBundle, System.Web.Optimization.BundleContext oContext, System.Collections.Generic.IEnumerable<System.Web.Optimization.BundleFile> oFiles)
		{
			var oContent = new System.Text.StringBuilder();
			foreach (var oItem in oFiles)
			{
				var oFile = new System.IO.FileInfo(System.Web.HttpContext.Current.Server.MapPath(oItem.VirtualFile.VirtualPath));
				var oMini = new Microsoft.Ajax.Utilities.Minifier();
				string cTemp = "";
				using (var oSR = oFile.OpenText()) { cTemp = oSR.ReadToEnd(); }
				string cSuccess = oMini.MinifyJavaScript(
					cTemp,
					new Microsoft.Ajax.Utilities.CodeSettings()
					{
						RemoveUnneededCode = true,
						StripDebugStatements = true,
						PreserveImportantComments = true,
						TermSemicolons = true
					}
				);
				if (oMini.ErrorList.Count > 0)
				{ oContent.Insert(0, PrependErrors(cTemp, oMini.ErrorList)); }
				else
				{ oContent.Append(cSuccess); }
			}
			return oContent.ToString();
		}
		//準備錯誤資訊
		private string PrependErrors(string cFile, System.Collections.Generic.ICollection<Microsoft.Ajax.Utilities.ContextError> oErrors)
		{
			var oContent = new System.Text.StringBuilder();
			oContent.Append("\r\n/* Minification Error \r\n");
			oContent.Append(string.Join(" \r\n", oErrors));
			oContent.Append("\r\n Minification Error */\r\n");
			oContent.Append(cFile);
			return oContent.ToString();
		}
	}

	/* ***** CSS 保留重要註解  實作區 ***** */

	/// <summary>
	/// (繼承)Css Bundle
	/// </summary>
	public class CommentStyleBundle : System.Web.Optimization.Bundle
	{
		public CommentStyleBundle(string virtualPath)	: base(virtualPath)
		{ this.Builder = new CommentStyleBundler(); }

		public CommentStyleBundle(string virtualPath, string cdnPath)	: base(virtualPath, cdnPath)
		{ this.Builder = new CommentStyleBundler(); }
	}

	/// <summary>
	/// (實作介面)Css Bundler
	/// </summary>
	public class CommentStyleBundler : System.Web.Optimization.IBundleBuilder
	{
		public virtual string BuildBundleContent(System.Web.Optimization.Bundle oBundle, System.Web.Optimization.BundleContext oContext, System.Collections.Generic.IEnumerable<System.Web.Optimization.BundleFile> oFiles)
		{
			var oContent = new System.Text.StringBuilder();
			foreach (var oItem in oFiles)
			{
				var oFile = new System.IO.FileInfo(System.Web.HttpContext.Current.Server.MapPath(oItem.VirtualFile.VirtualPath));
				var oMini = new Microsoft.Ajax.Utilities.Minifier();
				string cTemp = "";
				using (var oSR = oFile.OpenText()) { cTemp = oSR.ReadToEnd(); }
				string cSuccess = oMini.MinifyStyleSheet(
					cTemp,
					new Microsoft.Ajax.Utilities.CssSettings()
					{
						CommentMode = Microsoft.Ajax.Utilities.CssComment.Important,
						IgnoreAllErrors = true
					}
				);
				if (oMini.ErrorList.Count > 0)
				{ oContent.Insert(0, PrependErrors(cTemp, oMini.ErrorList)); }
				else
				{ oContent.Append(cSuccess); }
			}
			return oContent.ToString();
		}
		//準備錯誤資訊
		private string PrependErrors(string cFile, System.Collections.Generic.ICollection<Microsoft.Ajax.Utilities.ContextError> oErrors)
		{
			var oContent = new System.Text.StringBuilder();
			oContent.Append("\r\n/* Minification Error \r\n");
			oContent.Append(string.Join(" \r\n", oErrors));
			oContent.Append("\r\n Minification Error */\r\n");
			oContent.Append(cFile);
			return oContent.ToString();
		}
	}

	/* ***** 其他 實作區 ***** */

	/// <summary>
	/// 實作IBundleOrderer介面,藉以重新依照我們自己的意願進行檔案排序
	/// </summary>
	public class CommentBundleOrder : System.Web.Optimization.IBundleOrderer
	{
		public virtual System.Collections.Generic.IEnumerable<System.Web.Optimization.BundleFile> OrderFiles(System.Web.Optimization.BundleContext oContext, System.Collections.Generic.IEnumerable<System.Web.Optimization.BundleFile> oFiles)
		{ return oFiles; }
	}
	
}

使用方式範例:

public static void Register(System.Web.Optimization.BundleCollection oBundles)
{
	Slashlook.Bundle.CommentScriptBundle oJsBundle = new Slashlook.CommentScriptBundle("cKey");
	oJsBundle.Orderer = new Slashlook.Bundle.CommentBundleOrder();
	oJsBundle.Include(
		"~/include/jquery.js",
		"~/include/bootstrap.js"
	);
	oBundles.Add(oJsBundle);
}
System.Web.Optimization ScriptBundle StyleBundle ImportantComments CssVariables IncludeSort