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?
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.
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.
Yes. Angular does use . ts files by default. But if you write simple javascript code in them it will still work.
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.
tl;dr; the final solution is in the last section or just look at this plunk
$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.
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 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)
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>');
});
});
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.
We can improve on the previous plunk by adding a ui-router decorator for a very elegant solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With