Top 10 mistakes made when developing on AngularJS

Original author: Mark Meyer
  • Transfer
AngularJS is currently one of the most popular javascript frameworks. Its use simplifies the development process, making AngularJS an excellent tool for creating small web applications, but the framework’s capabilities are not limited to this and allow you to develop large applications full of diverse functionality. The combination of ease of development and a large number of features led to widespread distribution, and along with the distribution there were typical, often encountered errors. This topic describes the most common errors encountered when developing large projects on AngularJS.

1. Folder structure corresponding to MVC applications

AngularJS is an MVC framework. Despite the fact that the models in it are not defined as explicitly as in the case of backbone.js, the overall architectural style remains the same. A common practice when using MVC frameworks is to group files according to the following pattern:
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 approach is common, especially among developers with experience working on RoR. Nevertheless, with the growth of the application, the use of such a folder structure leads to the fact that at each point in time you have to keep several folders open. Whatever you use - Sublime, Visual Studio or Vim with NerdTree - when you move through the directory tree, you will constantly spend time scrolling. To avoid this, you can group files by functionality, and not by type:
app/
    app.js
    Feed/
        _feed.html
        FeedController.js
        FeedEntryDirective.js
        FeedService.js
    Login/
        _login.html
        LoginController.js
        LoginService.js
    Shared/
        CapatalizeFilter.js
This folder structure makes it much easier to search for related files related to the same feature, which can speed up the development process. Yes, it may seem controversial to store html files in one folder along with js, but the effect of saving time can be more important.

2. Modules (or lack thereof)

Often, at the beginning of project development, all the functionality is added to a single module. Until some point, this approach works, but as the project develops, the code becomes unmanageable.
var app = angular.module('app',[]);
app.service('MyService', function(){
    //service code
});
app.controller('MyCtrl', function($scope, MyService){
    //controller code
});
The next widespread approach is to group objects by their type:
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 approach also scales not in the best way, like the directory structure from point 1. To achieve better scalability, we will follow the same concept of breaking code into features:
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']);
Dividing the functionality into different modules also makes it possible to reuse code in various projects.

3. Dependency injection

Dependency injection is one of the best features provided by AngularJS. DI makes testing easier and cleaner. AngularJS is very flexible on how dependencies can be implemented. The easiest way is to pass the dependency to the function as a parameter:
var app = angular.module('app',[]);
app.controller('MainCtrl', function($scope, $timeout){
    $timeout(function(){
        console.log($scope);
    }, 1000);
});
From the code it is clear that MainCtrl depends on $ scope and $ timeout. This works great until the project goes into production and you want to minify your code. Using UglifyJS to the above code will result in the following:
var app=angular.module("app",[]);app.controller("MainCtrl",function(e,t){t(function(){console.log(e)},1e3)})
Now AngularJS does not know what MainCtrl really depends on. To prevent this from happening, there is a very simple solution - to pass the dependencies as an array of strings, with the last element in the form of a function that accepts all of the listed dependencies as parameters:
app.controller('MainCtrl', ['$scope', '$timeout', function($scope, $timeout){
    $timeout(function(){
        console.log($scope);
    }, 1000);
}]);
The code above will be converted by the minifier to code that AngularJS can already correctly interpret:
app.controller("MainCtrl",["$scope","$timeout",function(e,t){t(function(){console.log(e)},1e3)}])

3.1. Global dependencies

Often, when developing an AngularJS application, it becomes necessary to use objects available anywhere in the application. This breaks down a well-built model based on dependency injection and leads to bugs and complicates the testing process. AngularJS allows you to wrap such objects in modules so that they can be embedded like regular AngularJS modules. For example, the excellent Underscore.js library can be wrapped in a module as follows:
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 use a single style with the mandatory implementation of dependencies and leaves it possible to test modules in isolation from the functionality of their dependencies.

4. Inflating controllers

Controllers are the foundation of AngularJS. And often, especially beginners, write too much logic in the controllers. Controllers should not manipulate the DOM or contain DOM selectors; there are directives for this. Likewise, business logic must be in the services. Data should also be stored in services (unless data is tied to $ scope), because services, unlike controllers, are singletones whose lifetime coincides with the lifetime of the application itself. When developing controllers, it is best to follow the principle of sole responsibility (SRP) and consider the controller as the coordinator between the view and the model, in this case there will be a minimum of logic in it.

5. Service vs Factory

These naming conventions make every newbie in AngularJS embarrassing, although in reality they are almost the same. Let's see the source code for AngularJS:
function factory(name, factoryFn) { 
    return provider(name, { $get: factoryFn }); 
}
function service(name, constructor) {
    return factory(name, ['$injector', function($injector) {
      return $injector.instantiate(constructor);
    }]);
}
The service function simply calls the factory function, in which the call to the provider function is wrapped. If service just calls the factory function, what is the difference between the two? The point is $ injector.instantiate, inside which $ injector creates a new instance of the service constructor function. An example of a service and a factory performing the same actions:
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 helloWorldService or helloWorldFactory are injected into the controller, they will both have a single method that returns “Hello World”. Since all providers are singleton, we will always have only one instance of the service and one instance of the factory. So why do factories and services exist simultaneously if they perform the same function? Factories provide more flexibility because they can return a function that can create new objects. In OOP, a factory is an object that creates other objects:
app.factory('helloFactory', function() {
    return function(name) {
        this.name = name;
        this.hello = function() {
            return "Hello " + this.name;
        };
    };
});
Here is an example of a controller using a service and two factories:
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();
});
Factories can also be useful when developing classes with 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);
        }
    };
});

6. Failure to use Batarang

Batarang is a Chrome browser extension for developing and debugging AngularJS applications. Batarang allows you to:
  • view models attached to scopes
  • build a dependency graph in the application
  • analyze application performance
Despite the fact that the performance of AngularJS is not bad "out of the box", with the growth of the application, with the addition of custom directives and complex logic, the application may begin to slow down. Using Batarang it is easy to figure out which function spends a lot of time when called. Batarang also displays a watch tree, which can be useful when using a large number of watchers.

7. Too many observers

As noted above, AngularJS is pretty productive out of the box. But, when the number of observers reaches the number 2000, the $ digest cycle, in which the data changes are checked, may begin to slow down the application. Although reaching 2000 does not guarantee a slowdown, it is a good starting point from which to worry. Using the following code, you can find out the number of observers per page:
(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);
})();
Using the code above and the Batarang observer tree, you can see if you have duplicate observers or observers over immutable data. In the case of immutable data, you can use the bindonce directive so as not to increase the number of observers on the page.

8. Inheritance of scopes ($ scope's)

JS prototype-based inheritance is different from classical class inheritance. Usually this is not a problem, but these nuances can occur when working with scopes. In AngularJS, a regular (non-isolated) $ scope is inherited from the parent to the oldest ancestor $ rootScope. A common data model shared by a parent scope with a child is easily organized thanks to prototype inheritance. In the following example, we want the username to appear simultaneously in two span elements after the user enters his name.
{{user}}
{{user}}
Now the question is: when the user enters his name in the text box, in which elements will it be displayed: navCtrl, loginCtrl, or both? If your answer is loginCtrl, you understand how prototype-based inheritance works. No string of prototypes is used in string field searches. To achieve the desired behavior, it is advisable for us to use the object to correctly update the username in the child and parent $ scope. (I recall that in JS, functions and arrays are also objects.)
{{user.name}}
{{user.name}}
Now, since the user variable is an object, the prototype chain will work and the span element in navCtrl will be correctly updated along with loginCtrl. This may seem like an unnatural example, but when working with directives that create child scopes (like ngRepeat), such moments will arise.

9. Using manual testing

Until you start using TDD in your work, you will have to run the project each time and do manual testing to make sure that your code works. There is no excuse for using this approach with AngularJS. AngularJS was originally adjusted so that the code developed on it was testable. DI, ngMock are your best helpers on this. There are also several tools that can take you to the next level.

9.1 Protractor

Unit tests are the basis for building a fully-covered test application, but with the growth of the project, the use of integration tests can be more effective for checking how viable the code is in the application. Fortunately, the AngularJS team has developed a great tool - Protractor, which can simulate user interaction. Protractor uses the Jasmine framework for writing tests and has a good API for describing various interaction scenarios. Among the many different testing tools Protractor has the advantage of understanding AngularJS internals, which is especially useful when dealing with something like $ digest loops.

9.2. Karma

The AngularJS project team was not limited to writing test development tools. The test runner Karma was also developed. Karma allows you to run tests every time you change source files. Karma is able to run tests in parallel across multiple browsers. Various devices can also target the karma server to more fully cover real-world usage scenarios.

10. Using jQuery

jQuery is a great library. It has standardized cross-platform development and has become the standard in modern web development. Despite the fact that jQuery has a large number of features, its philosophy is far from the philosophy of AngularJS. AngularJS is a framework for building applications, while jQuery is just a library that simplifies the process of interacting JavaScript and HTML and provides a convenient API for working with AJAX. This is the fundamental difference between the two. Angular is an approach to building applications, not a way to control the layout of a document. To really understand the principles of building AngularJS applications, you should stop using jQuery. jQuery makes you conform to the existing HTML standard, while angular allows you to extend the HTML standard to the needs of your application.

Conclusion

AngularJS is a great, constantly evolving framework with a great community. I hope my list of popular errors comes in handy in your work.

Also popular now: