Bundling and Minifying an AngularJS Application with ASP.NET MVC
Bundling and minifying a website's scripts and stylesheets reduces page load time and asset size. Here's my project setup for bundling and minifying scripts of an AngularJS application hosted within an ASP.NET MVC site. If you're new to bundling and minification, make sure to read my introduction to bundling and minification first.
If you want to follow along while reading this post, I recommend you check out this little demo application I've put together. It showcases the various parts involved in creating and rendering script bundles. Oh, and I couldn't help but make it Lord of the Rings-themed. Forgive me if you're not a fan!
#Project Structure
Let's take a look at the project structure I've used in the demo application:
As you can see, it's mostly the default structure for an ASP.NET MVC application except for a few differences. Rather than putting my JavaScript files in the Scripts
folder and both images and CSS files in the Content
folder, I like to nest all assets for the browser underneath Client
to separate them from my server-side code.
Within the Client folder, we're entering the realms of front-end web development, so I like to adapt my naming strategy to lowercase-hyphenated-casing rather than PascalCasing. Angular services are an exception to this rule because I prefer the file names to exactly correspond to the internal names under which the services are registered.
All JavaScript libraries and frameworks we use live underneath vendor. Scripts belonging to our Angular application, on the other hand, are located within the app folder with the app.js file containing the main module definition. To reduce load times and payload size of our application, we want to go ahead and automatically minify all files within the app folder and bundle them together.
(We're not going to bundle the libraries and frameworks in this post. Those should be fetched from a CDN to take advantage of the possibility that they're already in the user's browser cache. Of course, you should still define a fallback script just in case the CDN is unreachable for whatever reason.)
#Installing the Optimization Framework
We're going to use the ASP.NET Web Optimization Framework to bundle together and minify all scripts of an AngularJS application hosted within an ASP.NET MVC site. It can be installed from NuGet via the following command:
Install-Package Microsoft.AspNet.Web.Optimization
Besides dealing with JavaScript files, the Web Optimization Framework can also bundle and minify CSS files (and even other types of files, given you provide a custom bundle transformation). For the purpose of bundling and minifying our Angular demo application, we're sticking to only JavaScript bundles in this post, though.
#Bundling All Angular Application Scripts
Let's create a BundleConfig.cs file underneath the App_Start folder which will define our script bundle within a RegisterScriptBundles
method. Here's how we call it from within Global.asax.cs, passing it the global variable holding all bundles in a collection:
BundleConfig.RegisterScriptBundles(BundleTable.Bundles);
Here's a first stab at the implementation:
using System.Web.Optimization;
namespace AngularMvcBundlingMinification
{
public static class BundleConfig
{
public static void RegisterScriptBundles(BundleCollection bundles)
{
const string ANGULAR_APP_ROOT = "~/Client/scripts/app/";
const string VIRTUAL_BUNDLE_PATH = ANGULAR_APP_ROOT + "main.js";
var scriptBundle = new ScriptBundle(VIRTUAL_BUNDLE_PATH)
.IncludeDirectory(
ANGULAR_APP_ROOT,
searchPattern: "*.js",
searchSubdirectories: true
);
bundles.Add(scriptBundle);
}
}
}
The ANGULAR_APP_ROOT
points to our app folder, and the VIRTUAL_BUNDLE_PATH
holds the name of the bundled script file we'll emit later. We're then creating an instance of the ScriptBundle
class and adding to it all JavaScript files underneath app using the IncludeDirectory
method. To do that, we specify the pattern *.js
and a recursive directory traversal.
A nice side effect of this wildcard syntax is that you don't need to explicitly add new Angular scripts to the script bundle. If you define new services within the app folder, the Web Optimization Framework will pick up on those new files automatically. It's a joy to work with!
Our bundle now contains all the files we need, but what about their order? We can't register Angular services on a module that doesn't exist yet. Therefore, we have to somehow make sure the module definition comes first.
#Ensuring Correct File Order in the Bundle
If an Angular service attempts to register itself with a module that doesn't exist, the framework will complain and we'll see the following error message in the browser console:
The solution to this problem is quite simple, actually. We have to change the file inclusion order to ensure the app.js
file is included first:
var scriptBundle = new ScriptBundle(VIRTUAL_BUNDLE_PATH)
.Include(ANGULAR_APP_ROOT + "app.js")
.IncludeDirectory(
ANGULAR_APP_ROOT,
searchPattern: "*.js",
searchSubdirectories: true
);
Fortunately, the Web Optimization Framework won't include the app.js script twice, even though the *.js
pattern passed to the IncludeDirectory
method matches the file name, too. It will instead recognize it has already seen the file and simply disregard any additional includes of app.js
.
#Rendering the Script Bundle
Now that we've defined a bundle for our Angular application, we need to render the appropriate <script>
tags in our Razor layout view. We do that by calling the Scripts.Render()
static method (found in the System.Web.Optimization
namespace) with the virtual path identifying the script bundle.
To avoid having to manually reference this namespace at the top of the containing Razor file, we're going to include it in the Web.config file within the Views folder:
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<!-- ... -->
<add namespace="System.Web.Optimization" />
</namespaces>
</pages>
</system.web.webPages.razor>
Inside your Razor views, IntelliSense should now suggest the Scripts.Render
method:
Now that that's taken care of, let's take a look at the <script>
tags we want to render. Depending on the value of the debug
attribute on the <compilation>
tag in your ASP.NET application's main Web.config file, the Web Optimization Framework will emit different forms of <script>
tags.
If debug="true"
is set, every script file in the bundle will be requested through a separate <script>
tag. Also, the scripts won't be minified. This helps during development time because you're working with the original source file which you can easily debug into:
<!-- Angular application scripts -->
<script src="/Client/scripts/app/app.js"></script>
<script src="/Client/scripts/app/controllers/ElvenRingsController.js"></script>
<script src="/Client/scripts/app/controllers/FellowshipController.js"></script>
<script src="/Client/scripts/app/directives/wikipediaLink.js"></script>
In case debug="false"
is set or the attribute is removed through a Web.config transformation (such as Web.Release.config), each bundle will be emitted as only one <script>
tag referencing the minified and concatenated bundle file. Also, the Web Optimization Framework will include a cachebreaker in the URL:
<!-- Angular application scripts -->
<script src="/Client/scripts/app/main.js?v=82p3oFlAKRu4Bx3_mEBzPrRCr1IEEJY_AfBpok4CIx01"></script>
Browsers are thus forced to request and use the latest bundle version rather than a possibly outdated one from their cache.
#Dependency Resolution with Minified Angular Code
Before we finish up, there's one more thing we need to take care of, and it's about not breaking Angular's dependency resolver when minifying the service script files.
Angular infers a controller's dependencies from the names of the arguments passed to its constructor function. This is why you can simply list all dependencies in the constructor and have those parameters "magically" filled with appropriate values:
(function () {
angular
.module("lordOfTheRings")
.controller("FellowshipController", FellowshipController);
function FellowshipController($scope) {
$scope.fellowship = {
companions: [
"Frodo",
"Sam",
"Merry",
"Pippin",
"Gandalf",
"Aragorn",
"Legolas",
"Gimli",
"Boromir",
],
};
}
})();
As long as you use this controller in its unminified version, the $scope
parameter will be injected correctly. If we minify the above code, however, the output looks something along the lines of this (with line breaks added for legibility):
!(function () {
function o(o) {
o.fellowship = {
companions: [
"Frodo",
"Sam",
"Merry",
"Pippin",
"Gandalf",
"Aragorn",
"Legolas",
"Gimli",
"Boromir",
],
};
}
angular.module("lordOfTheRings").controller("FellowshipController", o);
})();
Note that the $scope
argument name has been shortened to o
. Now the dependency resolution, being purely based on argument names, won't work correctly anymore. Of course, the Angular team is aware of this issue and offers a minification-safe solution.
#Minification-Safe Angular Services
While JavaScript minifiers will shorten identifiers where possible, they won't modify any string literals in your code. The idea is to provide the dependency names as a separate array of strings which will survive the minification process. My favorite approach to pass this array to the constructor function is through the $inject
property:
angular
.module("lordOfTheRings")
.controller("FellowshipController", FellowshipController);
FellowshipController.$inject = ["$scope", "$http", "$q"];
function FellowshipController($scope, $http, $q) {
// ...
}
Another approach is outlined in the Angular documentation. It uses a more compact syntax that I've found to be slightly harder to read with several dependencies. Besides that, both approaches work exactly the same: They ensure your dependencies are resolved correctly despite minification.
Related Links: