Tuesday, 6 November 2012

Cassette Bundling and Minification

Introduction

It is nice when you discover features that have been added to Visual Studio which match best practice, things like bundling and minification to speed up web page downloads. However, what is not good is realising that even though it looks like these resources have cache-busting, they do not. This removes the whole ability to set long expiration times for scripts and css and only download them when they change.

Cassette

I then discovered Cassette (getcassette.net) which provides the same functionality but also includes cache busting for any changed files.
First note is the documentation is pretty weak at the moment, although the Cassette google group seems to be supported well by the author(s). The other issue, related to the first, is that it is easy to get an error in the configuration which doesn't always manifest very obviously and therefore requires some poking around to work out what is actually wrong.

Setup

Setup is pretty easy using nuget (the package manager in visual studio) this adds a few assemblies and some web.config as well as a file called CassetteConfiguration.cs. As with all these nuget packages, be careful that the correct project is selected at the top of the package manager console before getting the package.
Secondly, remove either the Cassette config from system.web (IIS6) or that from system.webserver (IIS7) depending on what you run it on. I think this was causing some issue for me but it is possible that it is safe to keep both sections in and my issues were elsewhere.

Configuration

You will see a couple of examples in the configuration class but be aware that there are lots of different flavours of the Add function and these do different things. If you get these wrong, you will generally get an internal server error which will not print on your screen and which may or may not be raised in the debugger. The best is to start one at a time and bring things in.
Specifically, you need to be careful to distinguish between directory names, file names and alias names which are the first arguments to the Add functions.

Note here that bundling is a compromise between having a single downloaded file (which requires less connections and overhead to download) but not wanting any change to any of these files to require the whole lot to be downloaded again. Per-directory is a good compromise otherwise consider unchanging content (framework js and css) vs app-specific files. You can also consider common content vs page-specific content.

Example 1 - Add everything under directory Content but keep them in individual files:

bundles.AddPerIndividualFile<StylesheetBundle>("Content");

Example 2 - Add two specific files under directory Content to a single bundle:

bundles.Add<StylesheetBundle>("Content", new[] {
                "Site.css",
                "smallmobile.css"
            });
note that the Content here is both an alias and the start of the file paths.

Example 3 - Add two specific files under Scripts where the alias name is NOT the same as the start of the paths.

bundles.Add<ScriptBundle>("MsAjaxJs", new[]{
                "Scripts/WebForms/MsAjax/MicrosoftAjax.js",
                "Scripts/WebForms/MsAjax/MicrosoftAjaxWebForms.js"});
note that these include the base level Scripts folder name in their paths

Example 4 - Add everything under directory Content but bundle them per sub-directory.

bundles.AddPerSubDirectory<StylesheetBundle>("Content");

Example 5 - Add the following files with a named marker to allow them to be rendered in a specific place on the page

bundles.Add<ScriptBundle>("PPMouse", new []{
                "Scripts/imageinject.js",
                "Scripts/jcanvas.min.js"},
                b => b.PageLocation = "mouse");
note this location is commonly going to be "head" or "body" to distinguish scripts which must be in the head from those that can be in the body. In this example, I used mouse because it is page specific and I don't want it rendered via the master page into every page on the site.

Referencing

Before you can render these scripts and css, you need to reference them. This causes Cassette to do various checking and processing on the files and generates their unique filenames for cache busting. Referencing is fairly straight-forward, as you can see from the following:

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="PixelPinControl.SiteMaster" %>

<%
    Bundles.Reference("Content");            // Standard reference
    Bundles.Reference("Modernizr", "head");   // Reference from named position (head)
    Bundles.Reference("MsAjaxJs", "body");    // Two references from named position (body)
    Bundles.Reference("WebFormsJs", "body");
%>

<!DOCTYPE html>
<html lang="en">
<head runat="server">
note this doesn't render anything to the page at this position.
In order to then render the correct links, you use the Render method like the following:
<head runat="server">
    <!-- snip -->
    <%: Bundles.RenderStylesheets() %>
    <%: Bundles.RenderScripts("head") %>
In this example, we are rendering all style sheets (since we did not qualify the call to the RenderStylesheets) and then render only the scripts that are required in the head position. There is obviously a similar call in the body for the body scripts.

Page Specific Content

When using page-specific content, simple ensure your bundles are separated correctly and  then reference and render the content in the same place on your page. For example, my site uses a master page where the above code is used but I have a page which needs my mouse scripts and these are only allowed inside the content placeholders (body in my case):
<asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
    <% Bundles.Reference("PPMouse", "mouse"); %>
    <%: Bundles.RenderScripts("mouse") %>

Post a Comment