Preface
AngularJS is easy to develop, has many features and good effects, resulting in more applications, and some traps are accompanied by. This article lists some common problems that AngularJS is prone to problems. Let’s take a look together below.
1. MVC directory structure
AngularJS, to be blunt, is an MVC framework. Its model is not as clearly defined as the backbone.js framework, but its architecture is aptly. When you work in an MVC framework, it is common to classify it by file type:
templates/ _login.html _feed.htmlapp/ app.js controllers/ LoginController.js FeedController.js directives/ FeedEntryDirective.js services/ LoginService.js FeedService.js filters/ CapatalizeFilter.js
It seems that this seems to be an obvious structure, not to mention that Rails does the same. However, once the app starts to expand, this structure will cause you to open many directories at once. Whether you are using sublime, Visual Studio, or Vim combined with Nerd Tree, you will invest a lot of time constantly sliding up and down in the directory tree.
Unlike dividing files by type, instead, we can divide files by characteristics:
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 easier for us to find all files related to a feature, and thus speed up our development progress. While it may be controversial to put .html and .js files in one place, the time saved is more valuable.
2. Module
It is very common to put everything under the main module. For small apps, there is no problem at the beginning, but you will soon find that something is cheating.
var app = angular.module('app',[]);app.service('MyService', function(){ //service code});app.controller('MyCtrl', function($scope, MyService){ //controller code});After this, a common strategy is to classify objects of the same 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 method is similar to the directory structure mentioned in the first part above: not good enough. According to the same philosophy, it can be classified by characteristics, which leads to 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 we develop a large application, it may not be everything contained on a single page. Putting the same type of features in one module makes it easier to reuse modules across apps.
3. Dependency injection
Dependency injection is one of the best patterns in AngularJS, which makes testing simpler and is clear about relying on any specified object. The injection method of AngularJS is very flexible. The easiest way is to just pass the dependency name into the module's function :
var app = angular.module('app',[]); app.controller('MainCtrl', function($scope, $timeout){ $timeout(function(){ console.log($scope); }, 1000);}); Here, it is obvious that MainCtrl depends on $scope and $timeout .
Everything is beautiful until you are ready to deploy it to production and want to streamline your code. If you use UglifyJS, the previous example will become like this:
var app=angular.module("app",[]);app.controller("MainCtrl",function(e,t){t(function(){console.log(e)},1e3)})How do AngularJS know who MainCtrl depends on? AngularJS provides a very simple workaround, namely, passing dependencies into an array, the last element of the array is a function, and all dependencies are used as its parameters.
app.controller('MainCtrl', ['$scope', '$timeout', function($scope, $timeout){ $timeout(function(){ console.log($scope); }, 1000);}]);Doing so will make the code simplified, and AngularJS knows how to interpret these explicit dependencies:
app.controller("MainCtrl",["$scope","$timeout",function(e,t){t(function(){console.log(e)},1e3)}])3.1 Global dependency
This often happens when writing AngularJS programs: an object has a dependency, and this object binds itself to the global scope, which means that this dependency is available in any AngularJS code, but this destroys the dependency injection model and causes some problems, especially during the testing process.
Using AngularJS makes it easy to encapsulate these global dependencies into modules, so they can be injected like AngularJS standard modules.
Underscore.js is a great library that simplifies Javascript code in a functional style, and you can convert it into a module in the following ways:
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 approach allows the application to continue to develop in the style of AngularJS dependency injection, and can also swap out the underscore during the testing phase.
This may seem trivial and unnecessary, but if your code is using use strict (and it must be used), then this is necessary.
IV. Controller expansion
The controller is AngularJS meat and potatoes, and if you are not careful, you will add too much logic, especially at the beginning. The controller should never operate the DOM or hold the DOM selector, that is where we need to use instructions and ng-model. Similarly, business logic should exist in the service, not the controller.
The data should also be stored in the service unless they are already bound to $scope. The service itself is singleton and exists throughout the life of the application, but the controller is transient between the states of the application. If the data is saved in the controller, when it is instantiated again, it needs to re-get the data from somewhere. Even if the data is stored in localStorage, the search speed is an order of magnitude slower than Javascript variables.
AngularJS works well when following the Single Responsibility Principle (SRP). If the controller is the coordinator between the view and the model, it should contain as little logic as possible, which will also facilitate testing.
5. Service vs Factory
Almost every AngularJS developer is troubled by these nouns when he is beginner, which really doesn't deserve it, because they are just syntactic sugar for almost the same thing!
Here are their definitions in AngularJS source code:
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 code, you can see that the service simply calls the factory function, and the latter calls the provider function. In fact, AngularJS also provides additional provider encapsulation for some values, constants, and decor, which does not lead to similar confusion, and their documentation is very clear.
Since the service only calls the factory function, what's the difference? The clue is in $injector.instantiate : In this function, $injector creates a new instance in the service constructor.
Here is an example showing how a service and a factory do 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 helloWorldService or helloWorldFactory is injected into the controller, they all have a hello method that returns "hello world". The constructor of service is instantiated once when declared, and factory object is passed every time it is injected, but there is still only one factory instance. All providers are singletons.
Since you can do the same thing, why do you need two different styles? Compared to service , factory provides more flexibility because it can return functions, which can be created afterwards. This caters to the concept of factory patterns in object-oriented programming, where a factory can be an object that can create 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 service and two factory , helloFactory returns a function that sets the value of name when a new object is created.
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 you are beginners, it is best to just use service.
Factory is also useful when designing a class with many 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, we can make the privateFunc method unaccessible to the public API of privateFactory . This pattern can be done in service , but is easier in factory .
6. Batarang is not used
Batarang is an excellent Chrome plugin for developing and testing AngularJS apps.
Batarang provides the ability to browse models, which gives us the ability to observe how AngularJS is bound to the scope, which is very useful when handling instructions and isolating a range of bound values.
Batarang also provides a dependency graph, which is useful if we are exposed to an untested code base, which can determine which services should be focused on.
Finally, Batarang provides performance analysis. Angular can be used as a package and has good performance, but it is sometimes not so smooth for an application full of custom instructions and complex logic. Using the Batarang performance tool, you can directly observe which function runs the longest time in a digest cycle. Performance tools can also show a complete watch tree, which is useful when we have a lot of watchers.
7. Too many watchers
In the previous point, we mentioned that AngularJS can be used as a package and has good performance. Since dirty data checks need to be completed in one digest cycle, once the number of watchers grows to about 2000, this cycle will cause significant performance problems. (The number 2000 cannot be said to cause a significant performance drop, but this is a good empirical value. In the AngularJS 1.3 release version, there are already some changes that allow strict control of digest cycles.)
The following "Immediate Execution Function Expression (IIFE)" will print out the number of all watchers on the current page. You can simply paste it into the console and observe the results. This IIFE is derived from 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);})();In this way, the number of watchers is obtained, combined with the watch tree in the Batarang performance section, you should see where duplicate code exists, or where constant data exists and also own watches.
When there is unchanged data and you want to use AngularJS to template it, you can consider using bindonce. Bindonce is a simple directive that allows you to use templates in AngularJS, but it does not add to the watch, which ensures that the number of watches will not grow.
8. Limited range of $scope
Javascript prototype-based inheritance has a subtle difference from class-based inheritance in object-oriented object-oriented, which is usually not a problem, but this subtlety is manifested when using $scope . In AngularJS, each $scope inherits the parent $scope , which is called $rootScope at the highest level. ( $scope is somewhat different from traditional directives. They have a certain scope of action and only inherit explicitly declared properties.)
Due to the characteristics of prototype inheritance, it is not important to share data between parent and child classes, but if you are not careful, it is easy to misuse the property of a parent $scope .
For example, we need to display a username on a navigation bar, which is entered in the login form. The following attempt should work:
<div ng-controller="navCtrl"> <span>{{user}}</span> <div ng-controller="loginCtrl"> <span>{{user}}</span> <input ng-model="user"></input> </div></div>Then the question is...: The user's ng-model is set in the text input. When the user enters the content, which template will be updated? navCtrl or loginCtrl, or all?
If you chose loginCtrl, then you may have understood how prototype inheritance works.
The prototype chain does not work when you retrieve the literal. If navCtrl is also updated at the same time, it is necessary to retrieve the prototype chain; but if the value is an object, this will happen. (Remember, in Javascript, functions, arrays, and objects are all objects)
So in order to get the expected behavior, you need to create an object in navCtrl, which can be referenced by 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 work, and the navCtrl template and $scope and loginCtrl will be updated.
This seems like a pretty artificial example, but this problem can easily arise when you use certain instructions to create a child $scope , such as ngRepeat .
9. Manual testing
Since TDD may not be the way every developer prefers, they do manual testing when they check whether the code works or affects something else.
It makes no sense not to test the AngularJS app. AngularJS is designed to make it testable from scratch, and dependency injection and ngMock modules are proof of this. The core team of AngularJS has developed numerous tools that can take testing to the next level.
9.1 Protractor
Unit testing is the basis of testing work, but considering the increasingly complexity of the app, integration testing is closer to the actual situation. Fortunately, the core team of AngularJS has provided the necessary tools.
We have established Protractor, an end-to-end tester to simulate user interactions, which can help you verify the health of your AngularJS program.
Protractor uses the Jasmine test framework to define tests. Protractor has a very robust API for different page interaction behaviors.
We have some other end-to-end testing tools, but the advantage of Protractor is that it understands how to work with AngularJS code, especially in the $digest cycle.
9.2 Karma
Once we have completed the writing of integration tests with Protractor, the next step is to execute the tests. Waiting for tests to be executed, especially integration tests, is a faint sadness for every developer. The core team of AngularJS also felt extremely distressed, so they developed Karma.
Karma is a tester that helps to turn off feedback loops. Karma can do this because it runs tests when the specified file is changed. Karma will also run tests on multiple browsers, and different devices can also point to the Karma server, which can better cover real-world application scenarios.
10. Use jQuery
jQuery is a cool library, with standardized cross-platform development, and has almost become a necessity for modern web development. However, despite so many excellent features of JQuery, its philosophy is not consistent with AngularJS.
AngularJS is a framework for building apps, while JQuery is a library that simplifies "HTML document operations, event processing, animations and Ajax". This is the most basic difference between the two. AngularJS is committed to the architecture of the program and has nothing to do with HTML pages.
To better understand how to build an AngularJS program, please stop using jQuery. JQuery allows developers to think about problems with existing HTML standards, but as the documentation says, "AngularJS allows you to expand the term HTML in your application."
DOM operations should only be done in instructions, but that doesn't mean they can only be encapsulated with JQuery. Before you use JQuery, you should always think about whether this function is already provided by AngularJS. It is really powerful to create powerful tools when instructions depend on each other.
But a really great JQuery is a necessity when the day may come, but introducing it in the first place is a common mistake.
Summarize
AngularJS is an excellent framework that is always improving with the help of the community. Although AngularJS is still an evolving concept, I hope people can follow the conventions mentioned above to avoid the problems encountered in developing AngularJS applications. I hope the content of this article will be helpful to everyone. If you have any questions, you can leave a message to communicate. Thank you for your support to Wulin.com.