改善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 Slashview.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)
{
  Slashview.Bundle.CommentScriptBundle oJsBundle = new Slashview.Bundle.CommentScriptBundle("cKey");
  oJsBundle.Orderer = new Slashview.Bundle.CommentBundleOrder();
  oJsBundle.Include(
    "~/include/jquery.js",
    "~/include/bootstrap.js"
  );
  oBundles.Add(oJsBundle);
}
System.Web.Optimization ScriptBundle StyleBundle ImportantComments CssVariables IncludeSort