Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add require.js to my AngularJS application?

I have controllers in my AngularJS application that are currently coded like this:

app.controller('appController',
    [
        '$state',
        '$timeout',
        'enumService',
        'userService',
        'utilityService',
        appController
    ]);

function appController(
    $scope,
    $state,
    $timeout,
    enumService,
    userService,
    utilityService
) {

    ...

}

What I would like to start doing is to use require.js to handle lazy-loading of the controllers. I learned that I should use something like this:

require(["app"], function (app) {
     app.controller('appController', function appController(
         $scope,
         $state,
         $timeout,
         enumService,
         userService,
         utilityService
     ) {

         ...

     });
});

Can someone please explain to me how the app.controller can get a reference to the services? Do I need to do anything else on the require.js side? Am I on the right track with the way I am coding the appController?

like image 217
Samantha J T Star Avatar asked May 27 '14 13:05

Samantha J T Star


People also ask

Where do I put RequireJS?

To include the Require. js file, you need to add the script tag in the html file. Within the script tag, add the data-main attribute to load the module. This can be taken as the main entry point to your application.

Should I use require in Angular?

No - Don't use require. js OR browserify with Angular. JS there is simply no need to do that - AngularJS has a module system and using another module system above it will make your life unnecessarily hard.

Does AngularJS support JavaScript?

Yes. Angular does use . ts files by default. But if you write simple javascript code in them it will still work.

What is module in RequireJS?

a javascript module loader RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.


1 Answers

tl;dr; the final solution is in the last section or just look at this plunk


Lazy Loading with $injector

The angular-requirejs-seed project illustrates how you can easily implement lazy loading by setting up a lazy function like this:

define([], function() {
    return ['$scope', '$http', 'myInjectable', function($scope, $http, myInjectable) {
        $scope.welcomeMessage = 'hey this is myctrl2.js!';

        // because this has happened async, we've missed the digest cycle
        $scope.$apply();
    }];
});

... and then instantiating the controller like this:

.controller('MyCtrl2', ['$scope', '$injector', function($scope, $injector) {
    require(['controllers/myctrl2'], function(myctrl2) {
        $injector.invoke(myctrl2, this, {'$scope': $scope});
    });
...

Note that the lazily-loaded function is not a controller. It's just a function invoked with the $injector which gives it access to the actual controller's $scope and this, and allows it to access any of the injectables loaded in your app.

You can apply this same technique to a service, factory, or directive.


Lazy-loading Caveats

In most cases, lazy-loading is probably self-defeating. If your goal is to give your users a snappy website, then lazily loading every controller is a bad idea. Once an HTTP connection is established, most internet connections allow a lot of data to flow over the wire in a short period of time. Latency, however can be the real killer. That's why most sites these days use concatenation and minification to package their javascript and decrease the number of network requests, rather than relying on lazy-loading which increases the number of requests.

Consider the architecture of your app. How many re-usable directives will you create? How much code will be shared among various pieces of the app, unsuited for lazy-loading? For many apps, much of the code will be comprised of common components, making lazy-loading pointless.

Lazy loading makes sense in an app with very distinct and separate pieces. Pieces that are so distinct and separate they can be thought of as separate apps. However, even in this case you might consider actually creating separate apps rather than combining them.


BTW, require.js is still useful even if you're not Lazy-loading

Even if you're not lazy-loading, require.js is extremely useful for dependency management. Used in conjunction with the require.js optimizer it's an elegant way to keep track of dependencies and compress+minify your app.

You can also use require.js to load dependencies for running Jasmine unit tests which helps keep your components modular, and speeds up your tests by just loading the dependencies that you need. For unit testing, I create a separate main-test.js file which calls require.config(...) to load app dependencies as well as testing-specific dependencies.



Lazy-loading Architecture

Lazy-loading with angular is rather complicated because angular isn't designed to support lazy-loading. In this section I will attempt to take you on an exploration on how one might coerce angular to support lazy-loading. This isn't a full-blown solution, but I hope to present concepts which are important to understand when building such a application.

Let's begin at the router, as opposed to the angular-requirejs-seed I presented in the first section, it actually makes a lot more sense for lazy-loading to live in your application's router. Using ui-router, we can implement lazy-loading this way:

...
app.$controllerProvider = $controllerProvider;
var lazyPartialDeferred;

$stateProvider
  ...
  .state('lazy', {
    url: "/lazy",
    templateProvider: function() { return lazyPartialDeferred.promise; },
    controller: 'lazyCtrl',
    resolve: {
      load: function($q, $templateCache) {
        var lazyCtrlDeferred = $q.defer();
        lazyPartialDeferred = $q.defer();
        require(['lazy'], function (lazy) {
          lazyCtrlDeferred.resolve();
          lazyPartialDeferred.resolve($templateCache.get('lazy.html'));
        });
        return lazyCtrlDeferred.promise;
      }
    }
  });
...

What we are doing here is deferring instantiation of both the partial (lazy.html) and the controller (lazyCtrl) until after our requirejs module (lazy.js) is loaded. Also, note that we load our view partial, lazy.html directly from the $templateCache. That is, when we loaded lazy.js, the partial itself was included with lazy.js. In theory, we could have just loaded lazy.html separately from lazy.js but for best performance we should compile partials into our js files.

Let's take a look at lazy.js:

define(['angular', 'lazy-partials'], function (angular) {
  var app = angular.module('app');

  var lazyCtrl =  ['$scope', '$compile', '$templateCache', function ($scope, $compile, $templateCache) {
    $scope.data = 'my data';
  }];

  app.$controllerProvider.register('lazyCtrl', lazyCtrl);
});

Keep in mind, the above code represents the uncompiled lazy.js. In production, lazy-partials.js (referenced in the first line above) will actually be compiled into the same file.

Now let's take a look at lazy-partials.js:

// Imagine that this file was actually compiled with something like grunt-html2js
// So, what you actually started with was a bunch of .html files which were compiled into this one .js file...
define(['angular'], function (angular) {
  var $injector = angular.element(document).injector(),
      $templateCache = $injector.get('$templateCache');

  $templateCache.put('lazy.html', '<p>This is lazy content! and <strong>{{data}}</strong> <a href="#">go back</a></p>');
});

Once again, the above code isn't exactly what such a file would really look like. lazy-partials.js would actually be generated automatically from your html files using a build tool plugin like grunt-html2js.

Now, you could, in theory build your entire app using the approach presented thus far. However, it's a little... janky. What we would much rather do in our lazy.js is instantiate a new module, like appLazy = angular.module('app.lazy') and then instantiate our controller, directives, services, etc like appLazy.directive(...).

However, the reason we can't do this is because all of that stuff is initialized (and made available to our app) in the angular.bootstrap method which has already been called by the time lazy.js is loaded. And we can't just call angular.bootstrap(...) again.


BTW, internally angular is doing this to bootstrap modules:

      var injector = createInjector(modules, config.strictDi);

createInjector is an internal function to angular that loops through all of the modules and registers all of their various building blocks.

In lazy.js, we called $controllerProvider.register(..) to lazily register our controller. createInjector also triggers a call to the same function when the app is bootstrapped. Here is a list of various angular building blocks and the way in which they are registered by angular:

provider: $provide.provider
factory: $provide.factory
service: $provide.service
value: $provide.value
constant: $provide.constant.unshift
animation: $animateProvider.register
filter: $filterProvider.register
controller: $controllerProvider.register
directive: $compileProvider.directive

So, is there a way to lazily instantiate modules? Yes, you can register a module and it's sub-modules by iterating through various nested properties of the module object (requires and _invokeQueue), an operation which has been simplified in a lib called ocLazyLoad.

Most of the code presented in this section is available in this plunker.

(Sources of inspiration not mentioned above: Couch Potato, AngularAMD)


The Complete Lazy-loading Solution:

[ ocLazyLoad+ui-router+requirejs ] - plunk

Because ui-router allows us to defer loading of the template and the controller, we can use it in conjunction with ocLazyLoad to load modules on-the-fly between route changes. This example builds off of the principles of the previous section, but by utilizing ocLazyLoad we have a solution that allows our lazily-loaded modules to be structured in the same way as the non-lazy-loaded ones.

The key piece here is our app.config(..) block:

  app.config(function($stateProvider, $locationProvider, $ocLazyLoadProvider) {
    var lazyDeferred;

    $ocLazyLoadProvider.config({
      loadedModules: ['app'],
      asyncLoader: require
    });

    $stateProvider
      ...
      .state('lazy', {
        url: "/lazy",
        templateProvider: function() { return lazyDeferred.promise; },
        controller: 'lazyCtrl',
        resolve: {
          load: function($templateCache, $ocLazyLoad, $q) {
            lazyDeferred = $q.defer();
            return $ocLazyLoad.load({
              name: 'app.lazy', 
              files: ['lazy']
            }).then(function() {
              lazyDeferred.resolve($templateCache.get('lazy.html'));
            });
          }
        }
      });
    ...

lazy.js now looks like this:

define(['angular', 'lazy-partials'], function (angular) {
  var appLazy = angular.module('app.lazy', ['app.lazy.partials']);

  appLazy.controller('lazyCtrl', function ($scope, $compile, $templateCache) {
    $scope.data = 'my data';
  });
});

Notice that there's no longer anything special about this file in terms of lazy-loading. You could just as easily load this file in a non-lazy fashion and it wouldn't know the difference. The same principle applies to lazy-partials.js:

// Imagine that this file was actually compiled with something like grunt-html2js
// So, what you actually started with was a bunch of .html files which were compiled into this one .js file...
define(['angular'], function (angular) {
  angular.module('app.lazy.partials', [])
    .run(function($templateCache) {
      $templateCache.put('lazy.html', '<p>This is lazy content! and <strong>{{data}}</strong> <a href="#">go back</a></p>');
    });
});

>>> check out the fully-functioning plunk <<<


Deployment

When it comes to deployment, the final piece to this puzzle is to use the requirejs optimizer to concatenate and minimize our js files. Ideally, we want the optimizer to skip concatenation of dependencies that are already included in the main app (ie: common files). To accomplish this, see this repo and it's accompanying build.js file.


An even more elegant solution

We can improve on the previous plunk by adding a ui-router decorator for a very elegant solution.

like image 192
Gil Birman Avatar answered Oct 17 '22 03:10

Gil Birman