Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Complete solution to Combine, Minify and GZIP your Styles AND Scripts on Asp.Net MVC3 using Razor

Sorry for my bad English, but i guess it won't be a problem. I just wan't to share a nice Helper Class that i made to combine, minify and gzip our scripts and styles using the Microsoft Ajax Minifier. Before start, download the ICSharpCode.SharpZipLib. This is an open source lib to use gzip.

Let's start by the web.config (i'll focus at IIS7). Here we are saying to our application that any request made to cssh or jsh extensions, will be forwarded to the class MinifierHelper. I chosen to use these extensions (cssh and jsh) to if we want, for any reason, don't minify a specific script or style, use it the way you use normally.

<system.webServer>
  <handlers>
    <remove name="ScriptHandler" />
    <remove name="StyleHandler" />
    <add name="ScriptHandler" verb="*" path="*.jsh" type="Site.Helpers.MinifierHelper" resourceType="Unspecified" />
    <add name="StyleHandler" verb="*" path="*.cssh" type="Site.Helpers.MinifierHelper" resourceType="Unspecified" />
  </handlers>
</system.webServer>

I use the folders Scripts and Styles to store the files. I don't use the folder Content as suggested by Visual Studio.

The next step is to configure global.asax. We have to tell our application to not route these folders. Add these lines to your RegisterRoutes Method.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("Scripts/{*path}");
    routes.IgnoreRoute("Styles/{*path}");
    ...
}

OK. Now i'll show how to use our class. In the View:

<link href="/Styles/Folder/File.cssh" type="text/css" rel="stylesheet" />
<script src="/Scripts/Folder/File.jsh" type="text/javascript"></script>

In my example, i made the logic to all my scripts and styles to be inside folders inside the Scripts/Styles. Ex: Site -> Scripts -> Home -> index.css. I use the same structure used to the Views to the Scripts and Styles. Ex: Site -> Views -> Home -> index.cshtml. You can change this pattern if you want.

Now the code to make the magic:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
using System.Web.SessionState;
using ICSharpCode.SharpZipLib.GZip;
using Microsoft.Ajax.Utilities;

namespace Site.Helpers
{
    public abstract class MinifierBase : IHttpHandler, IRequiresSessionState
    {
        #region Fields

        private HttpContext context;
        private byte[] responseBytes;
        private bool isScript;

        protected string fileName;
        protected string folderName;
        protected List<string> files;

        #endregion

        #region Properties

        public bool IsReusable
        {
            get { return false; }
        }

        #endregion

        #region Methods

        public static string setUrl(string url)
        {
            var publishDate = ConfigurationManager.AppSettings["PublishDate"];
            return url + "h" + ((publishDate != null) ? "?id=" + publishDate : "");
        }

        public void ProcessRequest(HttpContext context)
        {
            this.context = context;
            this.isScript = context.Request.Url.PathAndQuery.Contains("/Scripts/");
            this.process();
        }

        private void process()
        {
            if (this.context.Request.QueryString.HasKeys())
            {
                string url = this.context.Request.Url.PathAndQuery;

                if (this.context.Cache[url] != null)
                {
                    this.responseBytes = this.context.Cache[url] as byte[];
                }
                else
                {
                    this.writeResponseBytes();
                    this.context.Cache.Add
                    (
                        url,
                        this.responseBytes,
                        null,
                        DateTime.Now.AddMonths(1),
                        Cache.NoSlidingExpiration,
                        CacheItemPriority.Low,
                        null
                    );
                }
            }
            else
            {
                this.writeResponseBytes();
            }

            this.writeBytes();
        }

        private void writeResponseBytes()
        {
            using (MemoryStream ms = new MemoryStream(8092))
            {
                using (Stream writer = this.canGZip() ? (Stream)(new GZipOutputStream(ms)) : ms)
                {
                    var sb = new StringBuilder();
                    var regex = new Regex(@"^/.+/(?<folder>.+)/(?<name>.+)\..+");
                    var url = regex.Match(this.context.Request.Path);
                    var folderName = url.Groups["folder"].Value;
                    var fileName = url.Groups["name"].Value;

                    this.getFileNames(fileName, folderName).ForEach(delegate(string file)
                    {
                        sb.Append(File.ReadAllText(this.context.Server.MapPath(file)));
                    });

                    var minifier = new Minifier();
                    var minified = string.Empty;

                    if (this.isScript)
                    {
                        var settings = new CodeSettings();

                        settings.LocalRenaming = LocalRenaming.CrunchAll;
                        settings.OutputMode = OutputMode.SingleLine;
                        settings.PreserveImportantComments = false;
                        settings.TermSemicolons = true;

                        minified = minifier.MinifyJavaScript(sb.ToString(), settings);
                    }
                    else
                    {
                        var settings = new CssSettings();

                        settings.CommentMode = CssComment.Important;
                        settings.OutputMode = OutputMode.SingleLine;

                        minified = minifier.MinifyStyleSheet(sb.ToString(), settings);
                    }

                    var bts = Encoding.UTF8.GetBytes(minified);

                    writer.Write(bts, 0, bts.Length);
                }

                this.responseBytes = ms.ToArray();
            }
        }

    private List<String> getFileNames(string fileName, string folderName = "")
    {
        this.files = new List<String>();
        this.fileName = fileName;
        this.folderName = folderName;

        if (folderName == "Global" && fileName == "global-min")
        {
            if (this.isScript) this.addGlobalScripts();
            else this.addDefaultStyles();
        }
        else
        {
            var flags = BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance;
            var mi = this.GetType().GetMethod
            (
                "add" +
                this.folderName +
                CultureInfo.CurrentCulture.TextInfo.ToTitleCase(fileName).Replace("-", "") +
                (this.isScript ? "Scripts" : "Styles"),
                flags
            );

            if (mi != null)
            {
                mi.Invoke(this, null);
            }
            else
            {
                if (this.isScript) this.addDefaultScripts();
                else this.addDefaultStyles();
            }
        }

        return files;
    }

    private void writeBytes()
    {
        var response = this.context.Response;

        response.AppendHeader("Content-Length", this.responseBytes.Length.ToString());
        response.ContentType = this.isScript ? "text/javascript" : "text/css";

        if (this.canGZip())
        {
            response.AppendHeader("Content-Encoding", "gzip");
        }
        else
        {
            response.AppendHeader("Content-Encoding", "utf-8");
        }

        response.ContentEncoding = Encoding.Unicode;
        response.OutputStream.Write(this.responseBytes, 0, this.responseBytes.Length);
        response.Flush();
    }

    private bool canGZip()
    {
        string acceptEncoding = this.context.Request.Headers["Accept-Encoding"];
        return (!string.IsNullOrEmpty(acceptEncoding) && (acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate")));
    }

    protected abstract void addGlobalScripts();
    protected abstract void addGlobalStyles();
    protected abstract void addDefaultScripts();
    protected abstract void addDefaultStyles();

    #endregion
}

That's the base class. Now we will create the Helper class that inherits from the base class. That's the class where we choose which scripts should be added.

public class MinifierHelper : MinifierBase
{
    #region Methods

To combine and minify global scripts/styles add the current line to your View:

<link href="@MinifierHelper.setUrl("/Styles/Global/global-min.css")" type="text/css" rel="stylesheet" />
<script src="@MinifierHelper.setUrl("/Scripts/Global/global-min.js")" type="text/javascript"></script>

It'll invoke the methods addGlobalScripts/addGlobalStyles in our MinifierHelper class.

    protected override void addGlobalScripts()
    {
        this.files.Add("~/Scripts/Lib/jquery-1.6.2.js");
        this.files.Add("~/Scripts/Lib/jquery-ui-1.8.16.js");
        this.files.Add("~/Scripts/Lib/jquery.unobtrusive-ajax.js");
        this.files.Add("~/Scripts/Lib/jquery.validate.js");
        ...
    }

    protected override void addGlobalStyles()
    {
        this.files.Add("~/Styles/Global/reset.css");
        this.files.Add("~/Styles/Global/main.css");
        this.files.Add("~/Styles/Global/form.css");
        ...
    }

To minify specific script/style (specific to the page) add the current line to your View:

<link href="@MinifierHelper.setUrl("/Styles/Curriculum/index.css")" type="text/css" rel="stylesheet" />

The MinifierHelper class will try to find a method with the name "add" + FolderName + FileName + "Styles". In our case it will look for addCurriculumIndexStyles. In my example it exists, so the method will be triggered.

    public void addCurriculumIndexStyles()
    {
        this.files.Add("~/Styles/Global/curriculum.css");
        this.files.Add("~/Styles/Curriculum/personal-info.css");
        this.files.Add("~/Styles/Curriculum/academic-info.css");
        this.files.Add("~/Styles/Curriculum/professional-info.css");
    }

If the class don't find that specific method, it'll trigger the default method. The default method minify a script/style using the same folder/name specified.

protected override void addDefaultScripts()
{
    this.files.Add("~/Scripts/" + this.folderName + "/" + this.fileName + ".js");
}

protected override void addDefaultStyles()
{
    this.files.Add("~/Styles/" + this.folderName + "/" + this.fileName + ".css");
}

Don't forget to close the region and class.

    #endregion
}

Thats it. I hope you guys have understood.

I forgot to tell one last thing. In the web.config add a key in AppSettings with the name PublishDate. I put in the value a string with the full date and time (ex: 261020111245). The goal is to be unique. This key will be used to cache our minified scripts. If you don't create this key, your application won't use cache. I recommend to use this. So everytime you update your scripts/styles, update your PublishDate also.

<add key="PublishDate" value="261020111245" />
like image 787
Rodrigo Manguinho Avatar asked Oct 26 '11 14:10

Rodrigo Manguinho


1 Answers

The Mindscape Web Workbench is a great tool for doing the items you are looking for.

http://visualstudiogallery.msdn.microsoft.com/2b96d16a-c986-4501-8f97-8008f9db141a

Here is a good blog post about it:

http://visualstudiogallery.msdn.microsoft.com/2b96d16a-c986-4501-8f97-8008f9db141a

like image 91
detroitpro Avatar answered Jan 03 '23 22:01

detroitpro