Monthly Archives: April 2014

How to lazy-load almost anything in AngularJS

I was asked to build a module web framework which would allow various teams to plug-in their parts as they become available with none or minimal affects on other existing parts. Each new part could be represented by a tab or a menu section. While each part relies on some common/shared services, frameworks or components, each also has unique dependencies. For example, all parts can rely on jquery, but the first part relies on Highcharts library for charting, while another part uses SlickGrid to represent data. Firstly, I had to pick libraries and frameworks to crate a foundation for the site. Foundation components would be either be used directly or relied upon by all the parts in the system. After careful evaluation, I picked the following

  • jQuery
  • AngularJS
  • RequireJS
  • BootstrapUI with Angular extensions

To wire up Angular and Require, I used the seed example found here: https://github.com/tnajdek/angular-requirejs-seed. Thanks to the configuration file used by RequireJS, my main index.html file only has a single script dependency:

<script data-main="app/js/main" src="app/lib/requirejs/require.js"></script>

The app/js/main.js file defines all the script dependencies required by the website. This file would need to be modified as dependencies get added by the parts of the system. For example, if a new part coming online requires another library, that library would have to be registered in this file along with its dependencies.

require.config({
	paths: {
		jquery: '../lib/{path to jquery}',
		angular: '../lib/{path to angular}',
		angularRoute: '../lib/{path to angular ui router}',
		angularBootstrap: '../lib/{path to bootstrap ui}',
		highcharts: '../lib/{path to highcharts}',
		'highcharts-export': '../lib/{path to highcharts export}',
		'highcharts-drilldown': '../lib/{path to highcharts drilldown}',
		'jquery-ui':'../lib/{path to jquery ui}',
		'jquery-drag-and-drop': '../lib/{path to jquery drag-n-drop}',
		'slickgrid-core': '../lib/{path to slickgrid core}',
		'slickgrid-cellselectionmodel': '../lib/{path to slickgrid cellselection model}',
		'slickgrid-rowmovemanager': '../lib/{path to slickgrid rowmovemanager}',
		'slickgrid-grid':'../lib/{path to slickgrid}'
	},

//Dependencies defined below
	shim: {
		'angular' : {
			'exports' : 'angular',
			'deps': ['jquery']
	},
		'angularRoute': ['angular'],
		'angularBootstrap': ['angular'],
		'highchart-theme': ['highcharts'],
		'highcharts-export': ['highcharts'],
		'highcharts-drilldown': ['highcharts'],
		'jquery-ui':['jquery'],
		'jquery-drag-and-drop':['jquery-ui'],
		'slickgrid-core':['jquery-drag-and-drop'],
		'slickgrid-cellselectionmodel':['slickgrid-core'],
		'slickgrid-rowmovemanager':['slickgrid-core'],
		'slickgrid-grid':['slickgrid-cellselectionmodel', 'slickgrid-rowmovemanager']
	},
        //Not sure about the significance of this piece below
	priority: [
		'angular'
	]
});

//After configuration, let's go running the app!

//http://code.angularjs.org/1.2.1/docs/guide/bootstrap#overview_deferred-bootstrap
window.name = 'NG_DEFER_BOOTSTRAP!';

require(['angular',
	'app',
	'app-config',
	'controllers'], function(angular, app) {
	'use strict';

	angular.element().ready(function() {
		angular.resumeBootstrap([app['name']]);
	});
});

So we start by requiring Angular, App, App-Config and Controllers, which initializes angular and runs it. Most of the standard angular code has been moved into modules according to AMD specs (https://github.com/amdjs/amdjs-api/wiki/AMD) used by RequireJS. The require() function call above first specifies that it needs to load angular module (defined above), app, app-config and controllers modules, which are not defined. If not explicitely defined, RequireJS will attempt to locate these modules in the same directory as this main.js file inside files whose names would same as names of the modules with ‘.js’ appended. So I must have these files:

  • app.js
  • app-config.js
  • controllers.js

Lets’ take a look at those: app.js

define ([
         'angular',
         'angularRoute',
         'angularBootstrap',
         ], function(angular) {

	'use strict';

	var myApp = angular.module('myApp', ['ui.bootstrap', 'ui.bootstrap.tpls', 'ui.router']);

	return myApp;
});

Notice that I am returning myApp from the app module. That allows me to reference it later. For example, if you look back to the main.js code, you’ll see that a parameter named app is being passed into function inside require() call (line 52). That app is the same as myApp being returned by the app module function (line 11). I need to have access to the app because I reference it from all other modules that depend on angular. The rest of the configuration is done inside the app-config module app-config.js

define(['app',
        'services/site-definition-service',
        'services/lazy-loader']
		, function(app) {

app.config(['$stateProvider', '$controllerProvider', '$urlRouterProvider', '$httpProvider', '$provide', '$compileProvider',
	                    function (stateProvider, cp, urlRouterProvider, httpProvider, provide, compProvider) {

		//These would be used later for lazy-loading controllers, directives and services
		app.$stateProvider = stateProvider;
		app.$controllerProvider = cp;
		app.$provide = provide;
		app.$compileProvider = compProvider;

		urlRouterProvider.otherwise('/');
...

The general function behind omitted code above is to declare states for your website. Each state would represent a part of the site that other developers would contribute. You can postpone defining all states until later by having a service retrieve your site configuration and register states based on configuration you specify in the service response. Look here for more info: . Controllers that you are declaring during normal bootstrapping of Angular site can simply be declared inside AMD modules such as this example below. Because controllers module is referenced as a dependency at startup, we can do this: controllers.js


define(['angular', 'app', './services/site-definition-service'], function(angular, app) {

	'use strict';

	app.controller('SomeController', ['$scope',
	                                 function (scope) {
		...
	}]);

Notice that the controller references something called site-definition-service. This is another javascript module which defines a service necessary to retrieve definition of the site. Such definition is a simple JSON structure which allows developers to configure states. As part of state configuration you can specify a resolve {} directive to resolve additional dependencies. Dependencies could be specified as an array of module names, which RequireJS would recognize when you configure them back in the main.js file. Here’s an example lf lazy-loading state registration:


define(['app',
        'services/lazy-loader'], function(app) {

//app.$stateProvider is used to lazy-register states
app.$stateProvider.state('stateName', {
			url: '/myPart',
			resolve: {
				'loadDependencies': function ($stateParams, LazyLoader) {
					return LazyLoader.loadDependencies('stateName');
				},
			},

and here’s what lazy loading service code would look like: lazy-loader.js

  
 define(['angular', 'app', 'require', './services/site-definition-service'], function (angular, app, requirejs) {

	'use strict';
	app.service('LazyLoader', ['$cacheFactory', '$http', '$rootScope', '$q', 'SiteDefinitionService',
                      function (cacheFactory, http, rootScope, q, siteDefService) {
             var self = this;

             this.loadDependencies = function(stateName) {            

               var deferred = q.defer();
               http.get('rest/sitedefinition/' + stateName).success(function (data, status, headers, config) {
                  var deps = data.dependencies; //array
                  if(deps &amp;&amp; deps instanceof Array) {
                     loadDependenciesFromArray(deps, deferred);
                  } else {
                     deferred.resolve();
                  }
               });

               return deferred.promise();
            }

		this.loadDependenciesFromArray = function(depArr, deferred){
			requirejs(depArr, function() {
				deferred.resolve();
			});

	}]);
});

Examples of lazy-loading dependencies

Directives

custom-directive.js

define(['app'], function(app) {

	'use strict';
	//app.$compileProvider is used to lazy-register directives
	app.$compileProvider.directive('customDirective', 
    ...

Services

part2-service.js

define(['app'], function(app) {
	'use strict';

	//app.$provide is used to lazy-register services
	app.$provide.service('Part2Service', 
    ...

Controllers

part2-controller.js

define(['app'], function(app) {

	'use strict';
	//app.$controllerProvider is used to lazy-register controllers.  Part2Service was loaded as a dependency earlier
	app.$controllerProvider.register('Part2Controller', ['$scope', 'Part2Service',
               function (scope, svc) {
    ...

To lazy load CSS files you’ll need a custom directive. I’ll cover this in another post. This post has gotten big and convoluted. If something is unclear, please comment, I’ll adjust the content as necessary. Thank you.

Unit Testing Custom Assembly Resolution

For my project at work I had to implement custom assembly resolution in a framework that supports dynamically loaded dlls stored at various locations, relative to the main executable. As an aside, this is achieved by listening to the AssemblyResolve event on the Current AppDomain (AppDomain.Current.AssemblyResolve). The handler should figure out the file name based on the assembly name and load the file using the Assembly.LoadFrom() method.


AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(HandleAssemblyResolve);

private static Assembly HandleAssemblyResolve(object sender, ResolveEventArgs args)
{
  //look at args.Name to figure out the filename your assembly would be defined in
  // ... skipping as its business specific
  //
  return LoadAssemblyFrom(foundPath, fileName);
}

private static Assembly LoadAssemblyFrom(string foundPath, string fileName)
{
   string text = Path.Combine(foundPath, fileName);
   Trace.TraceInformation("Found file {0}", text);
   return Assembly.LoadFrom(Path.Combine(foundPath, text));
}

But how does one go about unit testing this? The problem that .NET poses here is that once an assembly loaded into an appdomain, it cannot be unloaded from it. So the only way we can test this is by creating an appdomain to host loaded assemblies during testing and then unloaded it when the test is complete.

To accomplish this, I wrote this utility method called RunInAnotherAppDomain. Notice that it does not use the Action delegate, but instead uses a special CrossAppDomainDelegate class to enable calls across App Domains.


private void RunInAnotherAppDomain(CrossAppDomainDelegate actionToRun)
{
  var dom = AppDomain.CreateDomain("test", AppDomain.CurrentDomain.Evidence, 
                 AppDomain.CurrentDomain.BaseDirectory, string.Empty, false);

  dom.DoCallBack(actionToRun);
  AppDomain.Unload(dom);
}

In order to test this, I needed an assembly to play with. The easiest thing to do is to add a reference to another assembly from the project containing your unit tests. Because of the default Visual studio behavior, your assembly would be copied into the same directory as the unit test dll itself during compilation. I had created Library project, compiled it committed it to the library directory of my project and referenced it from the unit test project. The name of the dll was Dummy.dll and, and the name of the assembly was simply “Dummy”. Because it’s not actually used by the code directly (no code from dummy is actually being used, the assembly will not be loaded automatically by the process, which leaves open for me to play with.

The first test I would want to write would expect the assembly to be loaded normally, just by asking right out of the box.


[Test]
public void ShouldLoadAssembly()
{
  RunInAnotherAppDomain(() => 
  {
     //This method uses default .NET assembly resolution rules to load
     var assembly = Assembly.Load("Dummy");  
     Assert.IsNotNull(assembly);
  }
}

In the next test, I would move the assembly DLL to another location and will attempt to run same test and would expect it to fail:

[Test, ExpectException(ExpectedException = typeof(FileNotFoundException))]
public void ShouldFailLoadAssembly()
{
  MoveDummyAssemblyAway(); 
  try
  {
    ShouldLoadAssembly();
  }
  finally
  { 
    ReturnDummyAssemblyBack();
  } 
}
 
private static void MoveDummyAssemblyAway()
{ 
   if(!Directory.Exists("test"))
   { 
       Directory.CreateDirectory("test");
   }

   File.Copy("Dummy.dll" "test\\Dummy.dll", true);
   File.Delete("Dummy.dll");
}

private static void ReturnDummyAssemblyBack()
{
  if(Directory.Exists("test"))
  {  
      File.Copy("test\\Dummy.dll", "Dummy.dll", true);
      File.Delete("test\\Dummy.dll");
      Directory.Delete("test");
  }
}

I am now ready to test my custom assembly resolution logic.


[Test]
public void ShouldLoadAssemblyWithCustomResolution()
{  
  MoveDummyAssemblyAway(); 
  try
  {
    RunInAnotherAppDomain(() =>
    {
       //This sets up resolution for the current AppDomain.  
       //This lambda runs in the "test" appdomain, so it'll work
       MyCustomAssemblyResolver.Init();
       
       var assembly = Assembly.Load("Dummy");  
       Assert.IsNotNull(assembly); 
    }
  }
  finally
  { 
    ReturnDummyAssemblyBack();
  } 

If my custom resolution logic inside MyCustomAssemblyResolver class works, the test should pass.