Integrating AngularJS With a Large Google Closure Codebase

At Zoosk we maintain a large single page client app consisting of several hundred thousand lines of Javascript using the Google Closure library. Closure is a great all-around framework for large-scale Javascript applications, but one of its weaknesses is that it lacks modern data binding patterns, which tends to necessitate verbose boilerplate code. AngularJS is a relatively new framework on the scene and has been rapidly gaining popularity because of its simple yet powerful APIs for binding data models to the view. Several months ago, the Zoosk web team began experimenting with integrating Angular into our codebase by porting some of our existing pages to it. This post contains a summary of some of the issues we encountered and how we chose to solve them.

Compiling with Closure Compiler in Advanced Mode

We use the Closure Compiler to minify our Javascript using the ADVANCED_OPTIMIZATIONS flag to get the smallest code output possible. This creates several issues since the minified names no longer map to the keywords that Angular expects. The first problem is properties placed on the $scope won’t be accessible from the views. One way to solve this is by referencing all $scope properties in Javascript using bracket notation. The main disadvantage of this is every time these properties are referenced in Javascript, the caller needs to remember to use bracket notation.

An alternate solution is using the @expose annotation on these $scope properties so they won’t be minified. This allows you to reference exposed properties using dot notation everywhere. The disadvantage of this method is it potentially prevents other properties with the same name from being minified if the compiler can’t prove that they aren’t related. We also experienced an issue with the compiler throwing an “incomplete alias created for namespace” error if an exposed property shared a name with an existing namespace. For example, exposing $scope.date will cause the compiler to complain about goog.date having an incomplete alias. In this case we had to rename the property to something not used by a namespace such as $scope.dateModel to resolve the error.

The compiler also minifies function argument names which breaks the dependency injector. The $injector service uses some clever regex to extract the arguments and find the corresponding provider which doesn’t work with the minified names. One of the ways described in the docs is to declare the dependencies in an array of strings for the injectors:

MyController.$inject = ['$scope', 'greeter'];
view raw gistfile1.js hosted with ❤ by GitHub

With advanced mode we need to take this one step further and use bracket notation since the $inject property name will be minified:

MyController['$inject'] = ['$scope', 'greeter'];
view raw gistfile1.js hosted with ❤ by GitHub

A similar problem happens when using the directive definition object to declare a directive.

myDirective = function() {
return {
restrict: 'A',
link: function($scope, $element) {
$element.css('color', 'red');
}
};
};
view raw gistfile1.js hosted with ❤ by GitHub

The object property names will be minified and prevent Angular from parsing it correctly. You can use quotes around the property names to prevent minification.

myDirective = function() {
return {
'restrict': 'A',
'link': function($scope, $element) {
$element.css('color', 'red');
}
};
};
view raw gistfile1.js hosted with ❤ by GitHub

Model Modification Outside of Angular

Native Angular APIs such as ngClick and $http internally wrap calls with $scope.$apply which kicks off a $digest loop to check for model changes. Model changes that take place outside of Angular do not have this automatic wrapping so we need to notify it that a change has taken place by calling $scope.$apply manually. However, calling $scope.$apply when you’re already in a $digest loop will throw an exception. This becomes a problem when integrating with Closure because the async primitives (particularly, goog.async.Deferred) may fire callbacks synchronously or asynchronously. Therefore, for a given callback, you can’t be sure if you’re already in a digest or not. One solution from Alex Vanston is something he calls “safeApply” which performs this check before calling $scope.$apply. We are using a slightly modified version for the purpose of making requests to our back-end API.

zoosk.services.shared.SafeApplyFactoryFactory = function($rootScope) {
return function safeApplyFactory(fn, opt_context, var_args) {
var factoryArgs = [].slice.call(arguments, 2);
return function safeApplyWrapper() {
var factoryAndWrapperArgs = factoryArgs.concat([].slice.call(arguments));
var fnCall = typeof(fn) == 'function' ? function() {
return fn.apply(opt_context, factoryAndWrapperArgs);
} : angular.noop;
var phase = $rootScope.$root.$$phase;
if (phase != '$apply' && phase != '$digest') {
return $rootScope.$apply(fnCall);
}
return fnCall();
}
}
}
view raw gistfile1.js hosted with ❤ by GitHub

The service is called with fn as the callback, opt_context as the “this” object, and an arbitrary number of arguments can be passed in after that which fn will be called with.

As an aside, while $scope.$apply(fn); vs fn(); $scope.$apply(); may seem to produce identical results, there is actually an advantage of using the former. When $scope.$apply wraps the function call, any exceptions thrown from the call can be caught and handled by your $exceptionHandler.

Error Reporting

Angular makes it incredibly easy to integrate your existing error reporting system. We created a provider whose $get method returns an adapter object that we can use to log errors. We then overrode the native $exceptionHandler by calling provider() in the module configuration with our $exceptionHandler provider. We wrote this as a provider rather than a factory so that we could do additional configuration via the config() method in the individual application modules.

Templating

In Closure we have been using the standard Closure template system called soy for markup generation. The compiler takes the soy markup and generates a Javascript function which accepts an object for passing data to the view, and returns a string or DOM node. We chose to continue using soy so we could leverage our existing templates in Angular. In addition, soy has localization tools to extract strings for translation, which Angular lacks. However, using Angular interpolation in soy markup leads to messy templates since both delineate tags with curly brackets. It is possible to change this in Angular by configuring the $interpolateProvider, but we ran into some issues that prevented us from going forward with this. To use soy templates in Angular, we call the soy template function to get the DOM node then store them in the $templateCache in the module config run() function. They can then be referenced like a regular Angular html template.

Data Binding

To propagate model changes to the UI layer in Closure, we use a homebrewed data binding implementation based on goog.pubsub.PubSub. While quite powerful, these bindings need to be manually set up each time and only solve the problem of getting data to the view from the model, and not the other way around, which often times is a bigger challenge. Angular data bindings solve both of these issues with continuous updates that can go in either direction, allowing the model to always be the single source of truth. We continue to use pubsub in Angular components that need to interact with our Closure code.

Code Structure

We organize our Angular project similar to our current structure where files are grouped by their functionality. We like this approach since it allows us to easily reuse these components across multiple applications. Our Angular files are kept separate from the Closure files, with top level folders for controllers, directives, services, tests, and modules. The controllers, directives, and services folders are further subdivided by module to group components that are related in functionality and/or depend on each other. The modules folder contains files with the configuration blocks for our Angular modules. We use modules for two main purposes; large application modules which are used to boot up our various applications on Zoosk, and smaller component modules for groups of controllers, directives, and services which are used together. These components level modules are then declared as dependencies for the application modules.

Angular Framework Modules

One of the nice aspects of Angular is it doesn’t force you to use all of the framework components. In our case, we already have a well-tested router in Closure that we continue to use in Angular. We also have a great deal of code written to deal with constructing, sending requests and parsing responses from our back-end API. At this time it isn’t feasible for us to migrate these to the $http services since many of our features are not using Angular yet so the code needs to be callable both with and without Angular available. In the future, the Angular team plans to split each component of the framework into separate modules so you won’t even need to load code for framework components you aren’t using.

Memory Leaks

With single page applications, users perform full page reloads less often, leading to greater opportunity for memory leaks to accumulate and cause performance issues in an application. Managing memory leaks should always be a concern, but using multiple frameworks creates more opportunities for memory leaks to creep in. When an Angular component is torn down, it will destroy all the Angular components associated with it along with any jqLite event listeners or data. However, events that are attached to these components outside of Angular or other behaviors added on top will not be disposed and continue to consume memory. To prevent these leaks, add a listener on the scope for the $destroy event where you can dispose these components and allow the garbage collector to free this memory later.

Unsolved Problems

One thing that we’ve been having issues with is html5shiv with Angular. In IE < 9 we’ve consistently had problems when a template that contains HTML5 tags is cloned, such as in ngTransclude, html5shiv will get confused and construct the DOM incorrectly. Our short term solution has been to avoid using HTML5 is transcluded templates or avoiding transclusion altogether.

At a high level, we’ve found that we are able to ship features much faster using Angular and on average, the Angular rewrites weigh in at about 1/3 the amount of code that it took in Closure. Overall we have been very pleased with the power Angular brings and plan to start building all new features with it.