Thursday 3 January 2013

Dead-Simple Minification and Combination of CSS and JS files

In this post I'm going to give a quick example of how to compress and combine CSS and JS files in a build step, and to link these up in your .net application using razor. It's so damn easy that there's no reason why your next project shouldnt use it. I'm going to assume that you already know about MSBuild.

A special note: Since we're dealing with combining files, it is worth noting that your CSS needs to be done right (of course!). If you have overriding styles for certain pages, you'll need to make sure that your CSS targeting is more specific otherwise your pages might look strange.

Lets begin....


First, get YUI: http://yuicompressor.codeplex.com/documentation

I put YUI under a root tools folder: \Tools\YUI\


MSBuild

The following file will compress all files under \css and \scripts folders, into single files: minified.css and minified.js (respectively). See the properties MinifyCssOutput and MinifyJSOutput if you want to change these file names. You can exclude files using the properties MinifyJsExclusions and MinifyCssExclusions.

The following code should be self explanatory:

 <?xml version="1.0" encoding="utf-8"?>  
 <Project DefaultTargets="RunAll" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  
   <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>  
   <UsingTask TaskName="CssCompressorTask" AssemblyFile="..\..\Tools\YUI\Yahoo.Yui.Compressor.Build.MsBuild.dll" />  
   <UsingTask TaskName="JavaScriptCompressorTask" AssemblyFile="..\..\Tools\YUI\Yahoo.Yui.Compressor.Build.MsBuild.dll" />  
   
   <!-- Settings -->  
   <PropertyGroup>  
        
     <!-- Define output files -->  
     <MinifyCssOutput>$(TargetPath)Css\Minified.css</MinifyCssOutput>  
     <MinifyJsOutput>$(TargetPath)Scripts\Minified.js</MinifyJsOutput>  
   
     <!-- Define files to exclude-->  
     <MinifyJsExclusions>$(TargetPath)scripts\jquery-1.8.2.min.js;$(TargetPath)scripts\somefile.js</MinifyJsExclusions>  
     <MinifyCssExclusions>$(TargetPath)Css\SomeFolder\*.css;$(TargetPath)scripts\somefile.css</MinifyCssExclusions>  
   </PropertyGroup>  
   
   <Target Name="RunAll">  
     <Message Text="RunAll"/>  
   
     <!-- Ensure output files do not already exist -->  
     <Delete Files="$(MinifyCssOutput)" />  
     <Delete Files="$(MinifyJsOutput)" />  
       
     <CallTarget Targets="Minify"/>  
   </Target>  
   
   <Target Name="Minify">  
     <Message text="Compressing JavaScript and CSS files"/>  
       
     <!-- Define files to include -->  
     <CreateItem Include="$(TargetPath)Scripts\**\*.js" Exclude="$(MinifyJsExclusions)" >  
       <Output TaskParameter="Include" ItemName="JsFiles"/>  
     </CreateItem>  
     <CreateItem Include="$(TargetPath)Css\**\*.css" Exclude="$(MinifyCssExclusions)">  
       <Output TaskParameter="Include" ItemName="CssFiles"/>  
     </CreateItem>  
   
     <!-- Do compression -->  
     <CssCompressorTask  
        SourceFiles="@(CssFiles)"  
        DeleteSourceFiles="false"  
        OutputFile="$(MinifyCssOutput)"  
        CompressionType="Standard"  
        LoggingType="Info"  
        PreserveComments="false"  
        LineBreakPosition="-1"  
     />  
   
     <JavaScriptCompressorTask  
         SourceFiles="@(JsFiles)"  
        DeleteSourceFiles="false"  
        OutputFile="$(MinifyJsOutput)"  
        CompressionType="Standard"  
        LoggingType="Info"  
        LineBreakPosition="-1"  
     />  
   </Target>  
 </Project>  
   


The file can be called from your main MSBuild file using:
 <MSBuild Projects="$(MSBuildStartupDirectory)Build\Minify.msbuild"/>  


Note: My MSBuild transform files are in a root build folder: \Build\Transforms\, and my YUI files are under \Tools\YUI\, so you'll notice that my AssemblyFile attributes start with ..\..\Tools\YUI. Change this and any other paths to fit your folder structure.



Modifying your layouts

All you need to do is add the following razor code to your layouts...

Put this in your <head>
 @if (HttpContext.Current.IsDebuggingEnabled)  
 {   
      @*put your css files here*@  
      <link href="/css/layout.css" rel="stylesheet" type="text/css" />  
      <link href="/css/someother.css" rel="stylesheet" type="text/css" />  
}   
 else  
 {  
      <link href="/css/minified.css" rel="stylesheet" type="text/css" />  
 }  


Put this in where your javascript files are. I like to put it at the end of <body>. Note: I put jQuery in the <head>. I think this is useful but some may disagree.

 @if (HttpContext.Current.IsDebuggingEnabled)  
 {   
      @*put your js files here*@  
      <script src="/scripts/site.main.js" type="text/javascript"></script>  
      <script src="/scripts/someother.js" type="text/javascript"></script>  
}   
 else  
 {  
      <script src="/scripts/minified.js" type="text/javascript"></script>  
 }  


You probably already noticed that when your application is in debug mode, then all the files will be rendered, otherwise only the minified file is rendered.

If you have nested layouts, that's fine. You can have the "if" statement at all levels, just don't include the "else" part in the child layouts.


Conclusion
Minification and combination of CSS and JS files is dead simple! Do it!

Enjoy!



6 comments:

  1. Any reason why you don't use Client Dependency?

    ReplyDelete
  2. Hey Justin, I know I already answered this over twitter, but I thought I'd answer here too in case other people have the same question...

    In my opinion it's a bit too "cody", especially for non-c# devs. Don't get me wrong... It's extremely powerful and flexible, and as a c# dev I like that. It basically comes down to personal taste about how you want your markup to look.

    We're still at a point where we have to use webforms some times and mvc other times. The webforms version of CD adds so much extra code to your aspx pages. Where, doing it my way you could just use an inline if statement.

    This was obviously much more of an issue when dealing with webforms. The razor version is obviously much cleaner:

    @Html
    .RequiresCss("Content.css", "Styles")
    .RequiresJs("/Js/jquery-1.3.2.min.js", 1)
    .RequiresJs("FooterScript.js", "Scripts", 200);

    @Html.Raw(Html.RenderCssHere(new BasicPath("Styles", "/Css")))


    But something like this may not be entirely obvious. And we also need to consider working with frontend devs and freelancers. They may find it confusing.

    But in 6 months I may change my mind :)


    ReplyDelete
  3. I think it's also worth noting Client Dependency will strip out @font-face and @media. Which breaks embedding fonts and responsive layouts.

    ReplyDelete
  4. I've just tested this, how are you dealing with the order of each file?

    e.g. with css you would want to...

    reset.css
    fonts.css
    layout.css
    carousel.css
    etc

    ReplyDelete
  5. Have been thinking about this...

    ClientDependancy - could quite easily be fixed, the only work that would need to be done, is to removed the regex minification and pass it across to the YUI DLL. (e.g. https://gist.github.com/uniquelau/5264736)

    I've worked through your MSBUILD example as above and started thinking for this to be useful for CSS it really needs to support ordering. This is my response in code ;)

    https://gist.github.com/uniquelau/5292108

    I've added support for essentially an XML file that contains the files to process. This file is relative to the JS/CSS directory so it gives a good split between your /Build idea, and then the /VisualStudioProject. If the _Minify.props is not available then all the files are process by default. As this is a MSBUILD file it's possible to also pass parameters into the YUI for EACH file, so if you wanted to leave comments on one file that would be easy :)

    I'd quite like to look to use a HTML Parser to automatically generate the manifest file,

    _

    In your code you've included Community Tasks, but I'm not sure you need this?

    Look forward to your next post :) Laurie

    ReplyDelete