利用IHttpModule與Response.Filter,實作簡單的HTML壓縮器

我們都知道有JavaScript與CSS的壓縮實作,但對於HTML壓縮的實作在網路上比較少人在討論(而且有很多程式碼都是錯誤的),但其實說實在的,在現代JavaScript與CSS技術氾濫的推播下,大家對於JS、CSS的Minify都已經很有sense,可是反過頭來看,乘載的HTML母體,卻充斥著更多大量的空白、跳位、換行。不相信的話,下面這張微軟的網站原始碼截圖出來給你感受一下(這些換行與空白只是原始碼中的冰山一角)!

我這邊示範的範例還是以WebForm為主,如果你是採用MVC的話,可以考慮把類別掛在ActionFilter就可以了。

在Web.Config中呼叫IHttpModule

首先,打開你的Web.Config,找到system.webServer>modules,然後把等一下想要寫的IHttpModule類別用力插進去就對了。

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
    <add name="ChangeContent" type="Slashview.HttpResponseContent" />
  </modules>
</system.webServer>

實作IHttpModule類別,並利用Response.Filter掛上過濾類別

沒有甚麼技術性,就是把IHttpModule叫出來掛上事件後,對Response.Filter再掛一個類別,接下來就交給ASP.NET的機制,等到最終要Flush前,ASP.NET會自動去Call你定義的Filter,等於對你的類別事件進行委派啦!

namespace Slashview
{
  //實作IHttpModule來壓縮Http Response輸出的HTML字串
  public class HttpResponseContent : System.Web.IHttpModule
  {
    public void Init(System.Web.HttpApplication oContext)
    { oContext.BeginRequest += OnBeginRequest; }

    public void OnBeginRequest(object sender, System.EventArgs e)
    {
      System.Web.HttpApplication oContext = (System.Web.HttpApplication)sender;
      //如果是ASPX程式就套用過濾器(這個時期抓不到在後期才改變ContentType的ASPX程式,例如:打圖、打JSON...,因此在此不做無意義ContentType的判斷)
      if (oContext.Request.CurrentExecutionFilePathExtension == ".aspx")
      { oContext.Response.Filter = new CompressHtmlFilter(oContext.Response); }
    }

    public void Dispose() { }

    //利用InnerClass建立一個過濾器(在裡面實作壓縮)
    private class CompressHtmlFilter : System.IO.MemoryStream
    {
      private System.Web.HttpResponse _oResponse;
      private readonly System.IO.Stream _oFilter;

      public CompressHtmlFilter(System.Web.HttpResponse oResponse)
      { _oResponse = oResponse; _oFilter = oResponse.Filter; }

      public override void Write(byte[] aryBuffer, int iOffset, int iCount)
      {
        //如果最終ContentType是text/html」才處理
        if (_oResponse.ContentType == System.Web.MimeMapping.GetMimeMapping(".html"))
        {
          string cTemp = System.Text.Encoding.UTF8.GetString(aryBuffer);
          /* 以下條件均取代為空字串
           * 換行符號:出現1次(含以上)
           * 註解區段:會排除中括號是因為header有時會出現BrowserHack,例如[if lt IE 9]
           */
          cTemp = System.Text.RegularExpressions.Regex.Replace(cTemp, @"([\r\n]+)|(<!--[^\[\]]*?-->)", string.Empty);
          /* 以下條件均取代為一個空白
           * 空白符號:出現2次(含以上)
           * 跳位符號:出現1次(含以上)
           */
          //空白字元只要出現兩個(含以上),就會被取代為一個空白
          cTemp = System.Text.RegularExpressions.Regex.Replace(cTemp, @"( {2,})|(\t+)", " ");
          //將運算過的字串輸出
          _oFilter.Write(System.Text.Encoding.UTF8.GetBytes(cTemp), iOffset, System.Text.Encoding.UTF8.GetByteCount(cTemp));
        }
        else  //一律不處理丟出
        { _oFilter.Write(aryBuffer, iOffset, iCount); }
      }
    }
  }
}

程式寫完後,去重新整理你的aspx網頁,就可以看到你的HTML都被壓縮啦!

對於這支HTML壓縮程式想要補充的事項

  1. 其實空白、跳位、換行字元出現的頻率非常高,肯定名列字典檔的前三名,這意味著gzip對於這些字元的壓縮率非常的良好,若你還是有HTML壓縮的需求,應該要往安全性的方向考量。(例如:增加MVVM渲染前程式碼的不可閱讀性)
  2. 文章標題之所以會稱為「簡單」,最主要就是想要示範一下結構可以這樣設計,但這不意味著程式可以直接被你Copy上線運行(建議如果非必要,盡量不要在大型網站實作這一塊)。請試著想看看:你網頁中inline式Javascript的//註解,被你把換行都拿掉後會出現甚麼事情。
  3. 承上,你確定//只會出現在Javascript的註解裡面嗎?想看看有幾種出現方法呢?
  4. 外掛Response.Filter對於輸出效能一定會有影響,大量的文字比對操作,也會增加你伺服器記憶體的負荷。
  5. HTML壓縮程式每個網站運行的方式都不一樣(跟團隊Codeing Style也有正相關),所以世界上沒有一個良好又通用的HTML壓縮程式,你應該視自己的需求調整程式碼。
  6. Response.Filter傳入的資料是採用區塊式(Chunked)的,大小大約是16KB(16352 Bytes),因此你得到的字串並不是網頁全貌,如果你已經做好準備在這個地方動手腳,那你可能要先來這個網頁讀一下資料(Capturing and Transforming ASP.NET Output with Response.Filter)。這邊一定要特別注意,有非常多的網站提供的程式碼根本都沒有注意到自己正在解析Chunked級的資料,還以為自己很簡單的實作出一套機制,有很多隱性的Bug就藏在這個細節裡面。
  7. IHttpModule有很多事件,但可以讓你及時掛上Response.Filter並且可以正常調用運作的其實很少(多數的事件都處於生命週期的晚期,已經來不急了),最下方補一張IHttpModule事件列表讓你自己實驗參考一下。如果有頁面是在最終輸出時期才決定自己要輸出Content-Type的型態,建議事件掛在PostRequestHandlerExecute會比BeginRequest來的有效率。

補充資料:附上Http Request(Http Module) Liftcycle生命週期,讓讀者可以有更透徹的認識。

延伸閱讀

ASP.NET MVC WebForm HTML Compression IHttpModule HttpModule Response.Filter LifeCycle