The Top 10 Mistakes AngularJS Developers Make
1 MVC directory structure
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:templates/
_login.html
_feed.html
app/
app.js
controllers/
LoginController.js
FeedController.js
directives/
FeedEntryDirective.js
services/
LoginService.js
FeedService.js
filters/
CapatalizeFilter.js
This seems like an obvious layout, especially if coming
from a Rails background. 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
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 .html
files with .js
files, but the time savings are more valuable.2 Modules (or lack thereof)
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
});
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']);
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']);
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.3 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);
});
Here, it's very clear that MainCtrl
depends on $scope
and $timeout
. 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)})
Now how does AngularJS know what MainCtrl
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);
}]);
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)}])
3.1 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
use strict
(and it should be!), then this becomes a requirement.4 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 usingng-model
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
$scope
.
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.
5 Service vs Factory
These names cause confusion for almost every AngularJS developer 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);
}]);
}
From the source you can see that service
just calls the factory
function which in turn calls the provider
function. In fact, AngularJS also offers a few additional provider
wrappers with value
, constant
, and decorator
. These other objects don't cause nearly the same level of confusion and the docs are pretty clear on their use cases.Since
service
just calls the factory
function, what makes it different? The clue is in $injector.instantiate
; within this function the $injector
creates a new
instance of the service's constructor function. Here's an example of a service and a factory that accomplish the same thing.
var app = angular.module('app',[]);
app.service('helloWorldService', function(){
this.hello = function() {
return "Hello World";
};
});
app.factory('helloWorldFactory', function(){
return {
hello: function() {
return "Hello World";
}
}
});
When either helloWorldService
or helloWorldFactory
are injected into a controller, they will both have a hello
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 providers
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
new
'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;
};
};
});
Here's an example controller using the service and the two factories. With the helloFactory
returning a function, the name value is set when the object is new
'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();
});
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);
}
};
});
With this example it's possible to have the privateFunc
not accessible to the public API of privateFactory
. This pattern is achievable in services, but factories make it more clear.6 Not 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.
7 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);
})();
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.
8 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$scope
. In AngularJS every $scope
inherits from its parent $scope
with the highest level being $rootScope
. ($scope
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
$scope
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>
Quiz time: when a user types in the text input with the user
ng-model set on it, which template will update? The navCtrl
, loginCtrl
, or both? If you selected
loginCtrl
then you probably already understand how prototypical inheritance works. When looking up literal values, the prototype chain is not consulted. If
navCtrl
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
navCtrl
that can be referenced from loginCtrl
.<div ng-controller="navCtrl">
<span>{{user.name}}</span>
<div ng-controller="loginCtrl">
<span>{{user.name}}</span>
<input ng-model="user.name"></input>
</div>
</div>
Now since user
is an object, the prototype chain will be consulted and the navCtrl
's template and $scope
will be updated along with loginCtrl
. This may seem like a contrived example, but when working with directives that create child
$scope
's like ngRepeat
this issue can arise easily.9. Manual 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
ngMock
module are evidence of this. The core team has developed a couple of tools that can bring testing to the next level.9.1 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
$digest
cycles.9.2. 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.
10 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.
评论
发表评论