Pull to refresh

HttpHandler для сжатия и компрессии *.js и *.css файлов

Reading time8 min
Views709
Всем известно, что «большая часть времени уходит на загрузку компонентов страницы: картинок, таблиц стилей, скриптов, flash… Уменьшение количества этих компонентов уменьшает количество запросов к серверу, необходимых до того, как клиентское приложение может отрендерить страницу.» Я всегда сжимал и объединял *.js и *.css файлы вручную, но последнее время меня это стало немного доставать, и я решил упростить этот процесс. Для этого я перерыл кучу всего на гугле и тематических форумах в поисках нужной мне информации, а потом просто собрал всё вместе.
Для сжатия javascript'a я использовал jscompress, немного изменённый для моих нужд.
Вот что получилось:

<%@ WebHandler Language="C#" Class="StaticFilesCombiner" %>

using System;
using System.Net;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Web;
using JSCompress;

public class StaticFilesCombiner: IHttpHandler {
  private readonly static TimeSpan CACHE_DURATION = TimeSpan.FromDays(30);
  private const string JQuery = "jquery-1.2.6.min.js";
  
  public void ProcessRequest (HttpContext context) {
    
    HttpRequest request = context.Request;
    
    // Считывает Имя, contentType и версию.
    // используются для ключа кэша
    string setName = request["s"] ?? string.Empty;
    string contentType = request["t"] ?? string.Empty;
    string version = request["v"] ?? string.Empty;

    bool isCompressed = CanGZip(context.Request);
    bool isJS = contentType.Contains("java");
    UTF8Encoding encoding = new UTF8Encoding(false);

    if (!WriteFromCache(context, setName, version, isCompressed, contentType))
    {
      using (MemoryStream memoryStream = new MemoryStream(5000))
      {
        using (Stream writer = isCompressed ?
          (Stream)(new GZipStream(memoryStream, CompressionMode.Compress)) :
          memoryStream)
        {

          // Считываем файлы из <appSettings> и обрабатываем
          string setDefinition =
            System.Configuration.ConfigurationManager.AppSettings[setName] ?? "";
          string[] fileNames = setDefinition.Split(new[] { ',' },
            StringSplitOptions.RemoveEmptyEntries);
          
          foreach (string fileName in fileNames)
          {
            byte[] fileBytes = GetFileBytes(context, fileName.Trim(), encoding, isJS);
            writer.Write(fileBytes, 0, fileBytes.Length);
          }

          writer.Close();
        }

        byte[] responseBytes = memoryStream.ToArray();
        context.Cache.Insert(GetCacheKey(setName, version, isCompressed),
          responseBytes, null, System.Web.Caching.Cache.NoAbsoluteExpiration,
          CACHE_DURATION);

        WriteBytes(responseBytes, context, isCompressed, contentType);
      }
    }
  }

  /// <summary>
  /// Сжатие CSS.
  /// </summary>
  public static string MinifyCss(string body)
  {
    StringBuilder builder = new StringBuilder(body);
    builder = builder.Replace("  ", string.Empty);
    builder = builder.Replace(Environment.NewLine, string.Empty);
    builder = builder.Replace("\t", string.Empty);
    builder = builder.Replace(" {", "{");
    builder = builder.Replace(" :", ":");
    builder = builder.Replace(": ", ":");
    builder = builder.Replace(", ", ",");
    builder = builder.Replace("; ", ";");
    builder = builder.Replace(";}", "}");
    
    return builder.ToString();
  }
  
  /// <summary>
  /// Сжатие js.
  /// </summary>
  private static string MinifyJS(string notCompressedString, string virtualPath)
  {
    // Не сжимаем JQuery, так как он уже сжат.
    if (virtualPath.Contains(JQuery))
    {
      return notCompressedString;
    }
    JSCompressor jsCOmpressor = new JSCompressor(true)
                    {
                      CompressVariableNames = false,
                      LineFeedRemoval = true
                    };
    return jsCOmpressor.Compress(notCompressedString);
  }

  private static byte[] GetFileBytes(HttpContext context, string virtualPath, Encoding encoding, bool isJS)
  {
    string compressedString;
    string notCompressedString;
    if (virtualPath.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase))
    {
      using (WebClient client = new WebClient())
      {
        notCompressedString = client.DownloadString(virtualPath);
        compressedString = isJS ? MinifyJS(notCompressedString, virtualPath) : MinifyCss(notCompressedString);

        return encoding.GetBytes(compressedString);
      }
    }
    
    string physicalPath = context.Server.MapPath(virtualPath);
    notCompressedString = File.ReadAllText(physicalPath);
    compressedString = isJS ? MinifyJS(notCompressedString, physicalPath) : MinifyCss(notCompressedString);

    return encoding.GetBytes(compressedString);
  }

  private static bool WriteFromCache(HttpContext context, string setName, string version,
    bool isCompressed, string contentType)
  {
    byte[] responseBytes = context.Cache[GetCacheKey(setName, version, isCompressed)] as byte[];

    if (null == responseBytes || 0 == responseBytes.Length) return false;

    WriteBytes(responseBytes, context, isCompressed, contentType);
    return true;
  }

  private static void WriteBytes(byte[] bytes, HttpContext context,
    bool isCompressed, string contentType)
  {
    HttpResponse response = context.Response;

    response.AppendHeader("Content-Length", bytes.Length.ToString());
    response.ContentType = contentType;
    if (isCompressed)
      response.AppendHeader("Content-Encoding", "gzip");

    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.Cache.SetExpires(DateTime.Now.Add(CACHE_DURATION));
    context.Response.Cache.SetMaxAge(CACHE_DURATION);
    context.Response.Cache.AppendCacheExtension("must-revalidate, proxy-revalidate");

    response.OutputStream.Write(bytes, 0, bytes.Length);
    response.Flush();
  }

  private static bool CanGZip(HttpRequest request)
  {
    string acceptEncoding = request.Headers["Accept-Encoding"];
    if (!string.IsNullOrEmpty(acceptEncoding) &&
       (acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate")))
      return true;
    return false;
  }

  private static string GetCacheKey(string setName, string version, bool isCompressed)
  {
    return "StaticFilesCombiner." + setName + "." + version + "." + isCompressed;
  }

  public bool IsReusable
  {
    get
    {
      return true;
    }
  }

}


* This source code was highlighted with Source Code Highlighter.


Для его использования достаточно добавить в web.config всего пару строчек:

<appSettings>
  <add key="Main_Css" value="~/styles/styles.css"/>
  <add key="Set_Css" value="~/styles/Compare.css,~/styles/anycss.css,~/styles/ImageGallery.css,~/styles/styles.css,~/styles/thickbox.css"/>
  <add key="Set_Javascript" value="~/js/jquery-1.2.6.min.js,~/js/basket.js,~/js/jquery.galleria.js,~/js/thickbox-compressed.js"/>
  <add key="SiteName" value="stirka.spb.ru"/>
</appSettings>


* This source code was highlighted with Source Code Highlighter.


Фактически это наборы файлов для объединения и сжатия.

И само использование:

<link type="text/css" rel="Stylesheet" href="StaticFilesCombiner.ashx?s=Main_Css&t=text/css&v=13" />

* This source code was highlighted with Source Code Highlighter.

Tags:
Hubs:
Total votes 10: ↑4 and ↓6-2
Comments10

Articles