When writing large JavaScript applications, one of the simplest things you can do is split the code into several files. This improves the maintainability of the code, but increases the chances of losing or mistaking the insertion of the script tag into the main HTML document. Dependency tracking is difficult with the growth in the number of project files. This problem is present in large AngularJS applications so far. We have a variety of tools that take care of loading dependencies in an application.
In this article, we will look at using RequireJS with AngularJS to simplify dependency loading. We will also look at how to use Grunt to generate files containing RequireJS modules.
Brief Introduction to RequireJS
RequireJS is a JavaScript library that helps in lazy loading of JavaScript dependencies. Modules are regular JavaScript files with some RequireJS "syntactic sugar". RequireJS implements the Asynchronous Module Definition specified in CommonJS. RequireJS offers a simple API for creating modules and accessing them.
RequireJS requires a master file containing basic configuration data, such as module paths and gaskets. The following fragment shows the framework of the main.js file:
')
require.config({ map:{
There is no need to specify all application modules in the paths section. They can be loaded using relative paths. To declare a module, we must use the define () block.
define([
The module may not have any dependencies. Usually, an object is returned at the end of a module, but this is not necessary.
Implementing dependencies in AngularJS versus dependency management in RequireJS
One of the common questions I hear from AngularJS developers is about the difference in dependency management in AngularJS and RequireJS. Here it is important to remember that the purpose of both libraries is completely different. The dependency injection system built into AngularJS works with the objects required in the components; while dependency management in RequireJS deals with modules or javascript files.
When RequireJS tries to load a module, it first checks all dependencies and loads them. Objects of loaded modules are cached and provided when the same modules are requested again. On the other hand, AngularJS supports an injector with a list of imet and its corresponding objects. The object is added to the injector when the component is created and will be provided when it is referenced via the registered name.
Using RequireJS and AngularJS together
The code included in the article that can be
downloaded here is a simple application containing 2 pages. It has the following external dependencies:
- RequireJS
- jQuery
- AngularJS
- Angular route
- Angular resource
- Angular ui ngGrid
These files should be uploaded directly to the page in the order shown here. We also have five own files containing the code of the necessary components of AngularJS. Let's see how these files are defined.
Defining AngularJS Components as RequireJS Modules
Any component of AngularJS consists of:
- function declarations
- dependency injection
- registration in the Angular module
Of these three tasks, we will perform the first two inside separate modules (RequireJS), while the third task will be performed as a separate module, which is responsible for creating the AngularJS module.
First, let's define the configuration block. The configuration block does not depend on any other blocks and at the end returns the config function. But, before loading the config module inside another module, we have to load everything that is needed for the configuration block. The following code is contained in the config.js file:
define([],function(){ function config($routeProvider) { $routeProvider.when('/home', { templateUrl: 'templates/home.html', controller: 'ideasHomeController' }) .when('/details/:id',{ templateUrl:'templates/ideaDetails.html', controller:'ideaDetailsController'}) .otherwise({redirectTo: '/home'}); } config.$inject=['$routeProvider']; return config; });
Note the dependency injection method used in this snippet. I used $ inject to get the embedded dependencies, since the config function declared above is a simple JavaScript function. Before closing the module, we return the config function, so that it can be passed to the dependent module for further use.
We follow this approach to identify any other type of AngularJS components, and we also have no component-specific code in these files. The following snippet shows the controller definition:
define([], function() { function ideasHomeController($scope, ideasDataSvc) { $scope.ideaName = 'Todo List'; $scope.gridOptions = { data: 'ideas', columnDefs: [ {field: 'name', displayName: 'Name'}, {field: 'technologies', displayName: 'Technologies'}, {field: 'platform', displayName: 'Platforms'}, {field: 'status', displayName: 'Status'}, {field: 'devsNeeded', displayName: 'Vacancies'}, {field: 'id', displayName: 'View Details', cellTemplate: '<a ng-href="#/details/{{row.getProperty(col.field)}}">View Details</a>'} ], enableColumnResize: true }; ideasDataSvc.allIdeas().then(function(result){ $scope.ideas=result; }); } ideasHomeController.$inject=['$scope','ideasDataSvc']; return ideasHomeController; });
The Angular module for an application depends on each of the modules defined up to this point. This file gets objects from all other files and hooks them to the AngularJS module. This file may or may not return anything as a result, it can be referenced from anywhere using angular.module (). The following snippet defines the Angular module:
define(['app/config', 'app/ideasDataSvc', 'app/ideasHomeController', 'app/ideaDetailsController'], function(config, ideasDataSvc, ideasHomeController, ideaDetailsController){ var app = angular.module('ideasApp', ['ngRoute','ngResource','ngGrid']); app.config(config); app.factory('ideasDataSvc',ideasDataSvc); app.controller('ideasHomeController', ideasHomeController); app.controller('ideaDetailsController',ideaDetailsController); });
This Angular application cannot be launched using the ng-app directive, since the necessary scripts are loaded asynchronously. The correct approach here is to use manual startup. This should be done in a special file called main.js. Here you need to first load the file with the definition of the Angular module. The code for this file is shown below.
require(['app/ideasModule'], function() { angular.bootstrap(document, ['ideasApp']); } );
Configuring Grunt to combine RequireJS modules
When deploying large JavaScript applications, script files should be combined and minified to optimize their download speed. Tools like Grunt can be useful for automating these tasks. It has a variety of tasks defined to make any front-end deployment process easier. It has the grunt-contrib-requirejs task to merge the files of the RequireJS modules in the correct order and then minify the resulting file. Like any other Grunt task, it can be configured to behave differently for each stage of deployment. The following configuration can be used in our demo application:
requirejs: { options: { paths: { 'appFiles': './app' }, removeCombined: true, out: './app/requirejs/appIdeas-combined.js', optimize: 'none', name: 'main' }, dev:{ options:{ optimize:'none' } }, release:{ options:{ optimize:'uglify' } } }
This configuration will create an uncompressed file when Grunt is launched with the dev option, and a minified file if Grunt is launched with the release option.
Conclusion
Dependency management becomes difficult when the size of the application exceeds a certain number of files. Libraries like RequireJS make it easier to define dependencies and not worry about the order in which files are loaded. Dependency management becomes an integral part of JavaScript applications. AngularJS 2.0 will have built-in support for AMD.
UPDATE: It would be interesting to hear in the comments which dependency managers you use and what you think is the best option.