In the first installment of this series, I explored a few of the new features in ASP.NET MVC 4, including the new default project templates, mobile templates, and display modes. Since that article, ASP.NET MVC 4 has been released to beta. For brevity’s sake, when I refer to MVC the design pattern, I’m referring to the ASP.NET implementation of the pattern. In this installment, I’m going to focus on one of MVC’s most useful features: integrated JavaScript and CSS bundling and minification.
One of the most important considerations in any Web application is the size of the content rendered to the browser. Bundling and minification handle two important tasks. First, all of the disparate JavaScript and CSS files are combined into one or more files. Second, the JavaScript and CSS code is minified by means of removing all of the carriage returns and line feeds as well as verbose variable names in favor of shorter (less verbose) alternatives.
With bundling, there are fewer resources that need to be rendered to the browser. With minification, the size of such resources is much smaller, often by as much as 70% or more!
These are not new concepts. Tools like JSMin and YUI have been around for several years. You have always been able to incorporate these bundling and minification solutions. What’s new are the integrated features baked into the MVC framework that handle these bundling and minification tasks out of the box.
Bundling and Minification “Out of the Box”
Figure 1 illustrates what you get out of the box for bundling and minification. Like all ASP.NET applications, everything starts in the Global.asax Application_Start() event handling code.
In that code, there’s a call to:
BundleTable.Bundles.RegisterTemplateBundles();
The code for this method is burned into the System.Web.Optimization.dll. Figure 2 illustrates what that code looks like.
The code is straightforward in that bundles are created and files are added to the bundles. There are two ways to add files. One way is to refer to the file specifically. The second way is to specify a directory along with a file pattern search string. In the second method, the code can selectively traverse subdirectories to add content.
Each method has their respective pros and cons. When you specify the files, you know exactly what is getting into your bundled and minified files. This method requires more code. When you add directories, you don’t have to write as much code, but the possibility exists for unwanted code in the bundled and minified file. Choosing between adding files, directories or some combinatin thereof is a matter personal preference.
To use this functionality, there are a few concepts you must be familiar with:
- Bundle Class: Encapsulates one or more files to be bundled and minified
- AddFile() Method: Adds a specifically named file to the bundle
- AddDirectory() Method: Adds files that match a file search string within the specified directory
- Transform Property: An instance of IBundlerTransform, the object in this property performs the minification task
- Path Property: The string that identifies the bundle within the Bundles Member in BundleTables
As you will see in a moment, as is custom in MVC, if you don’t like the default out of the box functionality in MVC, you can substitute your own functionality. First, take a look at what the default functionality gives you. Figure 3 illustrates how the disparate javaScript (JS) and Cascading Style Sheet (CSS) files are rendered to the browser.
Note the file paths:
/Content/css
/Content/themes/base/css
/Scripts/js
These are the paths specified when the bundles were created. Figure 4 illustrates how the minified JavaScript appears.
Why Is a Query String Parameter Added to the JavaScript and CSS Files?
Every time the page is refreshed, the server-side code is executed, causing the _Layout.cshtml Razor Template to evaluate. You always want the client to recognize the latest JavaScript and CSS content. If the file were always named the same, the browser may reference its cache. This behavior can always be configured at the browser level. However, that is not usually a feasible solution because it requires each computer to be specifically configured. If you have the chance to control behaviors that are essential to your applications at the server level, take it! With the addition of the query string, the client will interpret this to be a new file and therefore, rely on its cache.
You might be thinking that the call to ResolveBundleUrl is required for the bundling and minification to work. It’s not. To illustrate, I’ll replace the code for /Content/css with the following:
<link href="~/Content/css" rel="stylesheet"
type="text/css" />
Figure 5 illustrates that bundling and minification still takes place. The only difference is the file name. Without the call to ResolveBundleUrl, the query string parameter is not added.
Essentially, that’s it! That’s what we get for free. But what about debugging? I don’t want my code minified in those cases. Am I stuck?
Of course not! In MVC, with almost no exception, you can override the default out-of-the-box behavior for your own behavior. In the next section, you will see how to create your own custom bundler and minification class.
Creating a Custom Bundler
If there are limitations to how the bundling and minification feature is implemented in the beta, these two would be at the top of the list:
- The code to create the bundles is burned in a dll.
- There is no way to make the default functionality sensitive to debug vs. release modes.
While bundling is something you probably always want, minification isn’t. While debugging, you need the unminified code to be rendered. Out of the box, using the default bundler, you cannot conditionally bundle. Let’s solve that problem now. The custom bundler code in Listing 1 solves the problem.
What’s the Second Bool Variable in AddFile()?
The second Bool Variable is the throwIfNotExist parameter. If the specified file does not exist, you can elect to have an exception thrown. In the default beta implementation, this parameter is set to false.
If you don’t want to be explicit with your files, the code could be simplified to what is illustrated in Listing 2.
The AddDirectory() method has four parameters:
- directoryVirtualPath: the root directory used to search for files to bundle and minify
- searchPattern: specifies the pattern to limit which files are included in the bundle
- searchSubDirectories: if true, the process recursively searches all contained subdirectories under the directoryVirtualPath
- throwIfNotExist: if true, the process throws an exception if the specified directoryVirtualPath does not exist
In this simplified code, every js file under /Scripts and every CSS file under /Content is included.
In both cases, a compiler directive is added to specify the transformer used to drive the minification process. Out of the box, there is a class called NoTransform. As the name implies, this class does not minify. You need such a class because the bundler instance requires a transformer:
var bundle = new Bundle("~/Scripts/js", jstransformer);
The bundler does not care what the transformer does. As long as it gets an instance that conforms to IBundleTransformer, the Bundle instance will be happy. Listing 3 shows what the NoTransform class is.
I Like the YUI Minifier; Can I Use That?
The nice thing about the way bundling and minification was implemented in ASP.NET MVC is that you don’t have to give up using utilities that you are currently using. The implementation works very much like the way IDependencyResolver works. In Version 3, an inversion of control container adapter was added to abstract away the details of any specific IoC container from the framework. The bundling and minification process illustrated here works very much the same way. The process begins with creating a custom instance of IbundlerTransform, as shown in Listing 4.
To take advantage of the YUICompressor Transform Class, the code in Listing 2 that loads the proper transformer must be changed to the following:
bool isDebug;
#if DEBUG
isDebug = true;
#endif
if (isDebug) {
jstransformer = new
NoTransform("text/javascript");
csstransformer = new NoTransform("text/css");
}
else
{
jstransformer = new
YUITransform(contentType.javascript);
csstransformer = new YUITransform(contentType.css);
}
I changed the debug-checking process slightly, because I am gradually moving to a solution that is testable. The next step involves creating a custom abstraction over the base Bundle Class that begins to abstract away the details of which files to add. I’ll leave that exercise to you to explore.
File Ordering within a Bundle
By default, JavaScript files are first ordered alphabetically within a bundle. Then, the files are restacked around known libraries. For example, jQuery - related files occur first in the bundle. Within the jQuery group, the files are sorted alphabetically. For CSS files, the files are first sorted alphabetically. Then, if the files reset.css or normalize.css exist, that content appears at the top of the bundled CSS file. Figure 6 illustrates this behavior in the bundled CSS file. Like everything else, this behavior is completely customizable. The Bundle Class has an Orderer property that conforms to the IBundleOrderer interface.
Conclusion
With each release, the ASP.NET MVC Framework gets better and better. Bundling and minification is an essential process that needs to be in every production Web app that relies on significant JavaScript and CSS resources. Out of the box, the functionality is pretty good. There are, however, some missing pieces. Fortunately, the process was designed with customization and extensibility in mind. With a little effort, it was easy to toggle minification based on whether or not the application was being run under debug mode.
One final point, this article was based on beta software. The usual disclaimer applies - this functionality **may **change when the product is released to manufacturing (RTM).
Listing 1: Custom bundler class
using System;
using System.Linq;
using System.Web.Optimization;
namespace MVC4BundleUI
{
public class MyBundler
{
public static void init()
{
IBundleTransform jstransformer;
IBundleTransform csstransformer;
#if DEBUG
jstransformer = new NoTransform("text/javascript");
csstransformer = new NoTransform("text/css");
#else
jstransformer = new JsMinify();
csstransformer = new CssMinify();
#endif
var bundle = new Bundle("~/Scripts/js", jstransformer);
bundle.AddFile("~/Scripts/<a href="http://jquery-1.6.2.js">jquery-1.6.2.js</a>", true);
bundle.AddFile("~/Scripts/<a href="http://jquery-ui-1.8.11.js">jquery-ui-1.8.11.js</a>", true);
bundle.AddFile("~/Scripts/<a href="http://jquery.validate.unobtrusive.js">jquery.validate.unobtrusive.js</a>", true);
bundle.AddFile("~/Scripts/<a href="http://jquery.unobtrusive-ajax.js">jquery.unobtrusive-ajax.js</a>", true);
bundle.AddFile("~/Scripts/<a href="http://jquery.validate.js">jquery.validate.js</a>", true);
bundle.AddFile("~/Scripts/<a href="http://modernizr-2.0.6-development-only.js">modernizr-2.0.6-development-only.js</a>",
true);
bundle.AddFile("~/Scripts/AjaxLogin.js", true);
bundle.AddFile("~/Scripts/<a href="http://knockout-2.0.0.debug.js">knockout-2.0.0.debug.js</a>", true);
BundleTable.Bundles.Add(bundle);
bundle = new Bundle("~/Content/css", csstransformer);
bundle.AddFile("~/Content/site.css", true);
BundleTable.Bundles.Add(bundle);
bundle = new Bundle("~/Content/themes/base/css", csstransformer);
bundle.AddFile("~/Content/themes/base/jquery.ui.core.css", true);
bundle.AddFile("~/Content/themes/base/jquery.ui.resizable.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.selectable.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.accordion.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.autocomplete.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.autocomplete.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.dialog.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.slider.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.tabs.css", true);
bundle.AddFile("~/Content/themes/base/jquery.ui.datepicker.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.progressbar.css",
true);
bundle.AddFile("~/Content/themes/base/jquery.ui.theme.css",
true);
BundleTable.Bundles.Add(bundle);
}
}
}
Listing 2: Simplied custom bundler code using the AddDirectory() method
public class MyBundler
{
public static void init()
{
IBundleTransform jstransformer;
IBundleTransform csstransformer;
#if DEBUG
jstransformer = new NoTransform("text/javascript");
csstransformer = new NoTransform("text/css");
#else
jstransformer = new JsMinify();
csstransformer = new CssMinify();
#endif
var bundle = new Bundle("~/Scripts/js", jstransformer);
bundle.AddDirectory("~/Scripts/","*.js",true, true);
BundleTable.Bundles.Add(bundle);
bundle = new Bundle("~/Content/css", csstransformer);
bundle.AddDirectory("~/Content/", "*.css", true, true);
BundleTable.Bundles.Add(bundle);
}
}
Listing 3: NoTransform transformation Class
public class NoTransform : IBundleTransform
{
readonly string _contentType;
public NoTransform(string contentType)
{
this._contentType = contentType;
}
public void Process(BundleContext context, BundleResponse
response)
{
response.ContentType = this._contentType;
}
}
Listing 4: YUI Compressor transformation class
using System.IO;
using System.Web.Optimization;
using Yahoo.Yui.Compressor;
namespace Bundler.Utilities
{
public enum contentType
{
javascript,
css
}
public class YUITransform : IBundleTransform
{
readonly string _contentType = string.Empty;
public YUITransform(contentType contentType)
{
if (contentType == contentType.css)
{
this._contentType = "text/css";
}
else
{
this._contentType = "text/javascript";
}
}
public void Process(BundleContext context, BundleResponse bundle)
{
bundle.ContentType = this._contentType;
string content = string.Empty;
foreach (FileInfo file in bundle.Files)
{
using (StreamReader fileReader = new
StreamReader(file.FullName)) {
content += fileReader.ReadToEnd();
fileReader.Close();
}
}
bundle.Content = Compress(content);
}
string Compress(string content) {
if (_contentType == "text/javascript")
{
return JavaScriptCompressor.Compress(content);
}
else
{
return CssCompressor.Compress(content,
CssCompressionType.StockYuiCompressor
);
}
}
}
}