Efficient Script Loading with AngularJS

Lately I have been putting my efforts towards creating a web front end for my Anime Information API.  To sharpen my skills, I decided to use Angular and really try to push things to develop good practices for organizing large amounts of JavaScript.  Central to this is, separating code into different files to ease maintenance.  But this resulted in this:

image

Having to add a tag each time a new file was added is painful.  Not only that its horribly inefficient, this approach will load code that the user may never even execute, so its a waste.

Luckily, this problem exists outside of AngularJS and has been addressed in many different ways, the most common way is through the AMD style loading supporting through libraries like RequireJS.  RequireJS allows to define our modules in terms of their implementation and the dependencies of that implementation.  It then takes care of managing scripts, including downloading those which are not yet downloaded.

This does result in some additional code being added to our code files and a bit of a learning curve to understand how we must organize AngularJS to allow RequireJS to work.  Frankly, it is very surprising that this sort of functionality isnt already built into AngularJS.  If it is, I could not find evidence of this, and the presence of the AngularJS RequireJS seed project would indicate I am not the only one who could find no dynamic script loading in AngularJS.

Setting Up

So, the idea with RequireJS is we can define our scripts as an application.  So, with RequireJS all of the lines above should be reduced to:

image

Pretty neat eh? But getting here isn’t easy.  The seed project can help, but I already have an existing solution to try to integrate this in.  With this line RequireJS will look for the main.js file in the app folder, this file will define the definitions needed by RequireJS.  Here is the contents of that file.

require.config({
 paths: {
  bootstrap: '../scripts/bootstrap',
  angular: 'scripts/angular/angular.min',
  angularRoute: 'scripts/angular/angular-route.min',
  uiBootstrap: 'scripts/ui-bootstrap-tpls-0.11.2.min',
  text: '../scripts/requirejs/text',
  underscore: '../scripts/underscore-min'
 },
 shim: {
  'bootstrap': ['jquery'],
  'angular': { 'exports': 'angular' },
  'angularRoute': ['angular'],
  'uiBootstrap': ['angular', 'bootstrap'],
  'underscore': { 'exports': '_' }
 }
});

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

require(['angular', 'app', 'routes'], function(angular, app, routes) {
 var $html = angular.element(document.getElementsByTagName('html')[0]);

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

I removed some of the definitions to make this shorter.  What you need to know is the paths section defines the scripts to be used while the shim section allows you to define dependencies between those script files (example: bootstrap require jquery be loaded).  In addition, most scripts are not written to be AMD complaint, thus for those we can define what the script exports which can be used within RequireJS.  An example of this is the angular object used within AngularJS apps.

Once you understand this the top section above becomes fairly self explanatory.  The next section is a bit tricky as it defines your bootstrap for RequireJS, or what gets run first.  To understand this, look at the three values in the JS array fed to require.

  • angular – this matches the name from the shim section, so RequireJS will ensure the file indicated in the paths section is loaded
  • app – since there is no shim configuration for this, RequireJS will attempt to load app.js in the same folder as main.js.  You could use directories here if you like, such as ‘code/app’ would attempt to look, from the level of main.js for app.js in the directory ‘code’.
  • routes – same as above.  Looks for routes.js

So now, the code hands off control to the familiar app.js that we see so often with Angular apps.  Here is the full contents of the file:

define([
    'angular',
    'views/controllers',
    'repository',
    'services',
    'filter',
    'angularRoute',
    'uiBootstrap'
    ], function (angular, controllers, repos) {
        return angular.module('animeApp', [
            'ngRoute',
            'animeApp.controllers',
            'animeApp.repositories',
            'animeApp.services',
            'animeApp.filters',
            'ui.bootstrap'
        ]);
});

RequireJS will attempt to match the name of the dependencies to those listed in the shim configuration, in this case angular, angularRoute, and uiBootstrap.  The other dependencies will be looked up based on the path to the JS file.

The interesting part of this comes in the callback.  We setup the module configuration for Angular as normal.  This pattern will be pervasive throughout the remainder of this.  We use define to ensure the scripts containing the bits needed are loaded.  Within the function we roll our Angular as normal.  We will see more examples of this later.

Lets’s look at the filter.js file to see this in action:

define(['angular', 'angularSanitize', 'angularUnderscore', 'repository'], function(ng) {
 return ng.module('animeApp.filters', ['ngSanitize', 'underscore'])
  .filter('termHighlight', ['$sce', function($sce) {
      return function(input, term) {
          var regex = new RegExp("(" + term + ")", "ig");
          return $sce.trustAsHtml(input.replace(regex, "<b class='highlight'>$1"));
      }
  }])
  .filter('filterCategoriesBySettings', ['_', 'settings.repository', function(_, settingsRepo) {
      return function(categories) {
       if (categories === undefined || categories.length == 0)
        return [];

          var showHentai = settingsRepo.getShowAdult();
          return _.filter(categories, function(category) {
              return category.isHentai === showHentai;
          });
      }
  }]);
});

Now, if we look at the define call you can see references to the shim configuration (angularSanitize, angularUnderscore), we also references repository.js which ensures we can use settings.repository in the Angular code.

This strategy is applicable to your controllers, services, and other modules of the application.  Of course, the major problem with doing this is you end up with all of your controllers/services/filters/etc in the same file.  This, to me, deviates from the goal of Angular, which is to break apart files so you dont have these massive JS files.  Our goal needs to be to further this approach, but maintain the file separation that Angular makes so easy.  That will be in part 2.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s