When I was writing code for AngularJS applications in past I spend a lot of time writing code. In the early phases of a project, the directory structure doesn’t matter too much and many people tend to ignore best practices and I must say that I was no different, this allows the developer to code rapidly, but in the long term will affect code maintainability and crate unexpected issues.
AngularJS is one of the most popular JavaScript frameworks available today. One of AngularJS's goals is to simplify the development process which makes it great for prototyping small apps, but its power allows scaling to full featured client side applications. The combination ease of development, breadth of features, and performance has led to wide adoption, and with wide adoption comes many common pitfalls.
AngularJS is still relatively new and developers are still figuring out what works and doesn’t. There are many great ways to structure an app and we’ll borrow some principles from existing mature frameworks but also do some things that are specific to Angular.
In this article I will try to cover bast practices to follow while developing for both small or large AngularJS applications . I am sure many developers will agree with me that there is no perfect way to build a app , it can be always better . I will be writing from experience and lessons learned from past projects I’ve worked, to help newbie AngularJS developers.
I am discussing the best practices below:
1. Directory Structure
First of all, let’s go over what not to do.
AngularJS is, for lack of a better term, an MVC framework. The models aren't as clearly defined as a framework like backbone.js, but the architectural pattern still fits well.When working in an MVC framework a common practice is to group files together based on file type.
app/
----- controllers/
---------- mainController.js
---------- otherController.js
----- directives/
---------- mainDirective.js
---------- otherDirective.js
----- services/
---------- userService.js
---------- itemService.js
----- js/
---------- bootstrap.js
---------- jquery.js
----- app.js
views/
----- mainView.html
----- otherView.html
index.html
----- controllers/
---------- mainController.js
---------- otherController.js
----- directives/
---------- mainDirective.js
---------- otherDirective.js
----- services/
---------- userService.js
---------- itemService.js
----- js/
---------- bootstrap.js
---------- jquery.js
----- app.js
views/
----- mainView.html
----- otherView.html
index.html
However once the app begins to scale, this layout causes many folders to be open at once. Whether using Sublime, Visual Studio, or Vim with Nerd Tree, a lot of time is spent scrolling through the directory tree.
Instead of keeping the files grouped by type, group files based on features:
app/
----app.js
----Feed/
--------_feed.html
--------FeedController.js
--------FeedEntryDirective.js
--------FeedService.js
----Login/
--------login.html
--------LoginController.js
--------LoginService.js
----Shared/
--------CapatalizeFilter.js
assets/
----- img/ // Images and icons for your app
----- css/ // All styles and style related files (SCSS or LESS files)
----- js/ // JavaScript files written for your app that are not for angular
----- libs/ // Third-party libraries such as jQuery, Moment, Underscore, etc.
index.html
----app.js
----Feed/
--------_feed.html
--------FeedController.js
--------FeedEntryDirective.js
--------FeedService.js
----Login/
--------login.html
--------LoginController.js
--------LoginService.js
----Shared/
--------CapatalizeFilter.js
assets/
----- img/ // Images and icons for your app
----- css/ // All styles and style related files (SCSS or LESS files)
----- js/ // JavaScript files written for your app that are not for angular
----- libs/ // Third-party libraries such as jQuery, Moment, Underscore, etc.
index.html
This directory structure makes it much easier to find all the files related to a particular feature which will speed up development. It may be controversial to keep files with files, but the time savings are more valuable.
2. Modularize the Header , Footer , Routes
A good practice here would be to create a subfolder under components, and then a subfolder for the Header and Footer and any additional components that will be shared across many pages.
In the structure above we didn’t do this, but another good practice for very large apps is to separate the routes into separate files. For example you might add a file in the subfolder and there include only the routes relevant to the blog such as , , , etc.
3. Modules
It's common when starting out to hang everything off of the main module. This works fine when starting a small app, but it quickly becomes unmanageable.
var app = angular.module('app',[]);
app.service('MyService', function(){
//service code
});
app.controller('MyCtrl', function($scope, MyService){
//controller code
});
app.service('MyService', function(){
//service code
});
app.controller('MyCtrl', function($scope, MyService){
//controller code
});
A common strategy after this is to group similar types of objects:
var services = angular.module('services',[]);
services.service('MyService', function(){
//service code
});
var controllers = angular.module('controllers',['services']);
controllers.controller('MyCtrl', function($scope, MyService){
//controller code
});
var app = angular.module('app',['controllers', 'services']);
services.service('MyService', function(){
//service code
});
var controllers = angular.module('controllers',['services']);
controllers.controller('MyCtrl', function($scope, MyService){
//controller code
});
var app = angular.module('app',['controllers', 'services']);
This strategy scales as well as the directory structures from part 1: not great. Following the same concept of grouping features together will allow scalability.
var sharedServicesModule = angular.module('sharedServices',[]);
sharedServices.service('NetworkService', function($http){});
var loginModule = angular.module('login',['sharedServices']);
loginModule.service('loginService', function(NetworkService){});
loginModule.controller('loginCtrl', function($scope, loginService){});
var app = angular.module('app', ['sharedServices', 'login']);
sharedServices.service('NetworkService', function($http){});
var loginModule = angular.module('login',['sharedServices']);
loginModule.service('loginService', function(NetworkService){});
loginModule.controller('loginCtrl', function($scope, loginService){});
var app = angular.module('app', ['sharedServices', 'login']);
When working on large applications everything might not be contained on a single page, and by having features contained within modules it's much simpler to reuse modules across apps.
4. Dependency injection
Dependency injection is one of AngularJS's best patterns. It makes testing much simpler, as well as making it more clear upon which any particular object depends. AngularJS is very flexible on how things can be injected. The simplest version requires just passing the name of the dependency into the function for the module:
var app = angular.module('app',[]);
app.controller('MainCtrl', function($scope, $timeout){
$timeout(function(){
console.log($scope);
}, 1000);
});
app.controller('MainCtrl', function($scope, $timeout){
$timeout(function(){
console.log($scope);
}, 1000);
});
Here, it's very clear that depends on and .
This works well until you're ready to go to production and want to minify your code. Using UglifyJS the example becomes the following:
var app=angular.module("app",[]);
app.controller("MainCtrl",function(e,t){t(function(){console.log(e)},1e3)})
app.controller("MainCtrl",function(e,t){t(function(){console.log(e)},1e3)})
Now how does AngularJS know what depends upon? AngularJS provides a very simple solution to this; pass the dependencies as an array of strings, with the last element of the array being a function which takes all the dependencies as parameters.
app.controller('MainCtrl', ['$scope', '$timeout', function($scope, $timeout){
$timeout(function(){
console.log($scope);
}, 1000);
}]);
$timeout(function(){
console.log($scope);
}, 1000);
}]);
This then minifies to code with clear dependencies that AngularJS knows how to interpret:
app.controller("MainCtrl",
["$scope","$timeout",function(e,t){t(function(){console.log(e)},1e3)}])
["$scope","$timeout",function(e,t){t(function(){console.log(e)},1e3)}])
Global dependencies
Often when writing AngularJS apps there will be a dependency on an object that binds itself to the global scope. This means it's available in any AngularJS code, but this breaks the dependency injection model and leads to a few issues, especially in testing.
AngularJS makes it simple to encapsulate these globals into modules so they can be injected like standard AngularJS modules.
Underscore.js is a great library for simplifying Javascript code in a functional pattern, and it can be turned into a module by doing the following:
var underscore = angular.module('underscore', []);
underscore.factory('_', function() {
return window._; //Underscore must already be loaded on the page
});
var app = angular.module('app', ['underscore']);
app.controller('MainCtrl', ['$scope', '_', function($scope, _) {
init = function() {
_.keys($scope);
}
init();
}]);
This allows the application to continue in the AngularJS dependency injection fashion which would allow underscore to be swapped out at test time.
This may seem trivial and like unnecessary work, but if your code is using (and it should be!), then this becomes a requirement.
5. Keep the Names Consistent
This is more of a general tip, but this will save you a headache in the future, when writing components and you need multiple files for the component, try to name them in a consistent pattern. For example, blogView.html, blogServices.js, blogController.js.
6.Controller bloat
Controllers are the meat and potatoes of AngularJS apps. It's easy, especially when starting out, to put too much logic in the controller. Controllers should never do DOM manipulation or hold DOM selectors; that's where directives and using come in. Likewise business logic should live in services, not controllers.
Data should also be stored in services, except where it is being bound to the . Services are singletons that persist throughout the lifetime of the application, while controllers are transient between application states. If data is stored in the controller then it will need to be fetched from somewhere when it is reinstantiated. Even if the data is stored in localStorage, it's an order of magnitude slower to retrieve than from with a Javascript variable.
AngularJS works best when following the Single Responsibility Principle (SRP). If the controller is a coordinator between the view and the model, then the amount of logic it has should be minimal. This will also make testing much simpler.
7.Service vs Factory
These names cause confusion for almost every AngularJS developer (Including me) when starting out. They really shouldn't because they're syntactic sugar for (almost) the same thing!
Here are their definitions from the AngularJS source:
function factory(name, factoryFn) {
return provider(name, { $get: factoryFn });
}
function service(name, constructor) {
return factory(name, ['$injector', function($injector) {
return $injector.instantiate(constructor);
}]);
}
return provider(name, { $get: factoryFn });
}
function service(name, constructor) {
return factory(name, ['$injector', function($injector) {
return $injector.instantiate(constructor);
}]);
}
From the source you can see that just calls the function which in turn calls the function. In fact, AngularJS also offers a few additional wrappers with , , and . These other objects don't cause nearly the same level of confusion and the docs are pretty clear on their use cases.
Since just calls the function, what makes it different? The clue is in ; within this function the creates a instance of the service's constructor function.
Here's an example of a service and a factory that accomplish the same thing.
app.service('helloWorldService', function(){
this.hello = function() {
return "Hello World";
};
});
app.factory('helloWorldFactory', function(){
return {
hello: function() {
return "Hello World";
}
}
});
When either or are injected into a controller, they will both have a method that returns "Hello World". The service constructor function is instantiated once at declaration and the factory object is passed around every time it is injected, but there is still just one instance of the factory. All are singletons.
Why do the two styles exist if they can accomplish the same thing? Factories offer slightly more flexibility than services because they can return functions which can then be 'd. This follows the factory pattern from object oriented programming. A factory can be an object for creating other objects.
app.factory('helloFactory', function() {
return function(name) {
this.name = name;
this.hello = function() {
return "Hello " + this.name;
};
};
});
return function(name) {
this.name = name;
this.hello = function() {
return "Hello " + this.name;
};
};
});
Here's an example controller using the service and the two factories. With the returning a function, the name value is set when the object is 'd.
app.controller('helloCtrl', function($scope, helloWorldService, helloWorldFactory, helloFactory) {
init = function() {
helloWorldService.hello(); //'Hello World'
helloWorldFactory.hello(); //'Hello World'
new helloFactory('Readers').hello() //'Hello Readers'
}
init();
});
init = function() {
helloWorldService.hello(); //'Hello World'
helloWorldFactory.hello(); //'Hello World'
new helloFactory('Readers').hello() //'Hello Readers'
}
init();
});
When starting out it is best to just use services.
Factories can also be more useful when designing a class with more private methods:
app.factory('privateFactory', function(){
var privateFunc = function(name) {
return name.split("").reverse().join(""); //reverses the name
};
return {
hello: function(name){
return "Hello " + privateFunc(name);
}
};
});
var privateFunc = function(name) {
return name.split("").reverse().join(""); //reverses the name
};
return {
hello: function(name){
return "Hello " + privateFunc(name);
}
};
});
With this example it's possible to have the not accessible to the public API of . This pattern is achievable in services, but factories make it more clear.
8.Utilizing Batarang
Batarang is an excellent Chrome extension for developing and debugging AngularJS apps.
Batarang offers Model browsing to get a look inside what Angular has determined to be the models bound to scopes. This can be useful when working with directives and to isolate scopes to see where values are bound.
Batarang also offers a dependency graph. If coming into an untested codebase, this would be a useful tool to determine which services should get the most attention.
Finally, Batarang offers performance analysis. AngularJS is quite performant out of the box, but as an app grows with custom directives and complex logic, it can sometimes feel less than smooth. Using the Batarang performance tool it is quite simple to see which functions are taking the most time in the digest cycle. The performance tool also shows the full watch tree, which can be useful when having too many watchers.
9. Too many watchers
As mentioned in the last point, AngularJS is quite performant out of the box. Because of the dirty checking done in a digest cycle, once the number of watchers exceeds about 2,000 the cycle can cause noticeable performance issues. (The 2,000 number isn't guaranteed to be a sharp drop off, but it is a good rule of thumb. Changes are coming in the 1.3 release of AngularJS that allow tighter control over digest cycles. Aaron Gray has a nice write up on this.)
This Immediately Invoked Function Expression (IIFE) will print out the number of watchers currently on the page. Simply paste it into the console to see how many watchers are currently on the page. This IIFE was taken from Words Like Jared's answer on StackOverflow:
(function () {
var root = $(document.getElementsByTagName('body'));
var watchers = [];
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
angular.forEach(element.data().$scope.$$watchers, function (watcher) {
watchers.push(watcher);
});
}
angular.forEach(element.children(), function (childElement) {
f($(childElement));
});
};
f(root);
console.log(watchers.length);
})();
var root = $(document.getElementsByTagName('body'));
var watchers = [];
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
angular.forEach(element.data().$scope.$$watchers, function (watcher) {
watchers.push(watcher);
});
}
angular.forEach(element.children(), function (childElement) {
f($(childElement));
});
};
f(root);
console.log(watchers.length);
})();
By using this to determine the number of watchers and the watch tree from the performance section of Batarang, it should be possible to see where duplication exists or where unchanging data has a watch.
When there is data that does not change, but you want it to be templated using Angular, consider using bindonce. Bindonce is a simple directive which allows you to use templates within Angular, but this does not add a watch to keep the count from increasing.
10. Scoping $scope's
Javascript's prototype-based inheritance differs from class-based inheritance in nuanced ways. This normally isn't a problem, but the nuances often arise when working with . In AngularJS every inherits from its parent with the highest level being . ( behaves slightly differently in directives, with isolate scopes only inherit properties explicitly declared.)
Sharing data from a parent to a child is trivial because of the prototype inheritance. It is easy however to shadow a property of the parent if caution is not taken.
Let's say we want to have a username displayed in a navbar, and it is entered in a login form. This would be a good first try at how this might work:
<div ng-controller="navCtrl">
<span>{{user}}</span>
<div ng-controller="loginCtrl">
<span>{{user}}</span>
<input ng-model="user"></input>
</div>
</div>
<span>{{user}}</span>
<div ng-controller="loginCtrl">
<span>{{user}}</span>
<input ng-model="user"></input>
</div>
</div>
Quiz time: when a user types in the text input with the ng-model set on it, which template will update? The , , or both?
If you selected then you probably already understand how prototypical inheritance works.
When looking up literal values, the prototype chain is not consulted. If is to be updated simultaneously then a prototype chain lookup is required; this will happen when the value is an object. (Remember, in Javascript, functions, arrays, and objects are objects)
So to get the desired behavior it is necessary to create an object on the that can be referenced from .
<div ng-controller="navCtrl">
<span>{{user.name}}</span>
<div ng-controller="loginCtrl">
<span>{{user.name}}</span>
<input ng-model="user.name"></input>
</div>
</div>
<span>{{user.name}}</span>
<div ng-controller="loginCtrl">
<span>{{user.name}}</span>
<input ng-model="user.name"></input>
</div>
</div>
Now since is an object, the prototype chain will be consulted and the 's template and will be updated along with .
This may seem like a contrived example, but when working with directives that create child 's like this issue can arise easily.
11. Testing
While TDD might not be every developer's preferred development method, every time a developer checks to see if code works or has broken something else he or she is doing manual testing.
There are no excuses for not testing an AngularJS app. AngularJS was designed to be testable from the ground up. Dependency injection and the module are evidence of this. The core team has developed a couple of tools that can bring testing to the next level.
Protractor
Unit tests are the building blocks of a test suite, but as the complexity of an app grows integration tests tease out more realistic situations. Luckily the AngularJS core team has provided the necessary tool.
We have built Protractor, an end to end test runner which simulates user interactions that will help you verify the health of your Angular application.
Protractor uses the Jasmine test framework for defining tests. Protractor has a very robust API for different page interactions.
There are other end to end test tools, but Protractor has the advantage of understanding how to work with AngularJS code, especially when it comes to cycles.
Karma
Once integration tests have been written using Protractor, the tests need to run. Waiting for tests to run, especially integration tests, can be frustrating for developers. The AngularJS core team also felt this pain and developed Karma.
Karma is a Javascript test runner which helps close the feedback loop. Karma does this by running the tests any time specified files are changed. Karma will also run tests in parallel across different browsers. Different devices can also be pointed to the Karma server to get better coverage of real world usage scenarios.
12 . Using jQuery
jQuery is an amazing library. It has standardized cross-platform development and is almost a requirement in modern web development. While jQuery has many great features, its philosophy does not align with AngularJS.
AngularJS is a framework for building applications; jQuery is a library for simplifying "HTML document traversal and manipulation, event handling, animation, and Ajax". This is the fundamental difference between the two. AngularJS is about architecture of applications, not augmenting HTML pages.
In order to really understand how to build an AngularJS application, stop using jQuery. jQuery keeps the developer thinking of existing HTML standards, but as the docs say "AngularJS lets you extend HTML vocabulary for your application."
DOM manipulation should only be done in directives, but this doesn't mean they have to be jQuery wrappers. Always consider what features AngularJS already provides before reaching for jQuery. Directives work really well when they build upon each other to create powerful tools.
The day may come when a very nice jQuery library is necessary, but including it from the beginning is an all too common mistake.
Don’t Forget to Minify
If you do decide to opt in and build your AngularJS apps in a modularized fashion, be sure to concatenate and minify your code before going into production. There are many great extensions for both Grunt and Gulp that will help with this – so don’t be afraid to split code up as much as you need.
You may not want to necessarily have just one giant file for your entire app, but concatenating your app into a few logical files like:
- (for app initialization, config and routing)
- (for all the services)
This will be greatly beneficial for reducing initial load times of your app.
AngularJS is a great framework that continues to evolve with the community. Idiomatic AngularJS is still an evolving concept, but hopefully by following these conventions some of the major pitfalls of scaling an AngularJS app can be avoided.
This article covered some of the best practices in regards to structuring an AngularJS app. It is easy to ignore good practices in order to save time upfront. We all have a tendency to just want to start writing code. Sometimes this passion can hurt us in the long run when our awesome apps grow and become popular and then we’re stuck rewriting or even worse maintaining badly thought out code. I hope this article had some helpful tips.
ReplyDeleteNice and good article.Thanks for sharing this useful information. If you want to learn Angular js course online, please visit below site.
Angular js online training
Angular js online course
Angular js Online Training in Hyderabad
Angular js Online Training in Bangalore
Angular js Online Training in Chennai