OdeToCode IC Logo

AngularJS - Controllers, Dependencies, and Minification

Wednesday, March 13, 2013

In working through a series of posts with AngularJS I've been using a controller function like so:

var VideoController = function($scope, Video) {
    // ... stuff 
};

The problem is that the function is in global scope. Some people will wonder what sort of sick mind creates a framework that forces developers to pollute the global scope.

Fortunately, you can register a controller constructor with angular.module instead. This is the same module API we used to create a resource service in a previous post.

angular.module("videoApp", ["videoService"])
       .controller("VideoController", function ($scope, Video) {

    // stuff ...
    
});

Not the prettiest syntax, unfortunately, but the controller constructor is out of the global scope and the world is a safer place. However, there is (and was) a problem lurking under the surface. Stay in suspense while we first look at dependencies in AngularJS.

Dependencies

Angular provides services that allow you to register dependencies and inject dependencies as function parameters. If you've worked with IoC containers in a language like C#, you'll be comfortable with the dependency injection in Angular. There is one significant difference, however. In languages like C# a container can examine a constructor method with reflection and find the types of the constructor parameters. Knowing that a constructor needs an ILogger or an IRepository will usually give a container enough information to figure out how to satisfy the dependency.

One of the many challenges for an IoC container in JavaScript is that method parameters and variables don't have an associated type. The question to think about is what to do with a function like the following, where $http represents a dependency to inject.

var ctor = function($http) {
    // stuff ...         
};

With no type information to go on, Angular relies on the next best thing, which is the name of the parameter. $http is the right name to use when you want Angular's built-in service that wraps an XmlHttpRequest object. The $http service offers communication methods  like get, post, put, delete, and jsonp. At test time you can pass in your own test double for the real $http service to avoid network calls in unit tests. Every service registered with Angular will have a specific name, like $http, $timeout (wraps setTimeout), and $location (wraps the address bar). You can register custom services, too.

Using well known names seems simple enough until you start thinking about how Angular actually discovers the parameter names. There is nothing built into JavaScript that will let you grab the names of a function's parameters. The answer is in the Angular source where you'll find a function named annotate, which relies on 4 regular expressions:

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

The annotate function will take a function object and invoke it's toString method to get the code for the function. It then strips out all the comments, parses out the function arguments as a string, and splits the string on the comma character. The result is an array with parameter names inside. Sounds crazy, but it works!

Now to the caveat.

Minification

One of the techniques a JavaScript minifier will use to reduce the amount of code sent to the client is to change the names of local variables and parameters to be as small as possible. A parameter named "$http" might come out of a minifier with the name "n" to save at least 4 bytes.

Once you know how dependency injection with Angular relies on parameter names, you'll realize a minifier can destroy Angular's ability to inject dependencies. Indeed, once you've minified a file you might see exceptions like the following:

Error: Unknown provider: nProvider <- n

The solution is to provide 'annotations' – an array that contains the names of the dependency a function needs, and the function itself.

angular.module("videoApp", ["videoService"])
       .controller("VideoController", ["$scope", "Video", function($scope, Video) {
    // ... stuff
}]);

A minifier won't change the $scope and Video string literals in the array, but it can change the function parameter names (and that's ok, because Angular will now look at the names in the array to resolve dependencies in the same order). In the previous samples (with a constructor function in the global scope), we could have also solved the problem by adding an $inject property to the function. Angular will look for an $inject property and use the names here instead of parsing the names out of the function definition.

VideoController.$inject = ['$scope', 'Video'];

Summary

Heavy stuff in the post, so just remember two things:

- You don't need to declare functions in the global scope to work with AngularJS. Many of the tutorials use this approach just to simplify the code.

- Think about the impact of minification from the start. Since most script files end up minified at some point, start describing your dependencies using the array approach (or $inject approach) from day 1. More details here.