No Big, Giant, Fat Controllers
- Wrap business logic into services
- Controllers should only be used to set up scope and handle view interactions
- No DOM Manipulation inside Controllers
- Use service and directives instead
This controller is trying to take over the world
angular.module('awesome-app')
.controller('itemController', ['$scope', '$http',
function ($scope, $http) {
function transform (items) { /* ... */ }
$scope.getItemList = function () {
//Retrieve list of from remote API server
$http.get('/api/item').then(
function success (success) {
$scope.items = transformItems(res.items);
},
function error (error) {
alert('No item for you!');
});
};
......better
angular.module('awsome-app')
.service('itemRetriever', [$q, $http, function ($q) {
function transform () { ... }
this.get = function () {
var deferred = $q.defer();
$http.get('/api/items').then(function success (res) {
...
});
return deferred.promise;
};
});
.controller('itemController', ['$scope', itemRetriever
function ($scope, itemRetriever) {
itemRetriever.get().then(function (transformedItems) {
$scope.items = transformedItems;
});
$scope.addItem = function (item) {...};
/* ... */
Group your code into bundles (modules)* Why? Separating functionalities into modules makes it easier to isolate and troubleshoot errors during testing
* Keep modularization horizontal instead of vertical
Vertical Modularization
angular.module('app', []);
angular.module('services', []);
angular.module('filters', []);
angular.module('controllers', ['services', 'filters']);
......better
angular.module('app', []);
angular.module('company-api', []);
angular.module('products', ['company-api']);
angular.module('personnels', ['company-api']);
...
All External References Should Come from Dependency Injections
* Put or wrap global variable and constants into angular.value() or angular.constant()* Inject them as needed
Bad for tests
.service('myService', function () {
...
ThirdPartyLib.externalMethod(...);
...
});
......better
angular.module('app')
.factory('ThirdPartyLib', function ($window) {
return $window.ThirdPartyLib;
});
.service('myService', ['ThirdPartyLib',
function (ThirdPartyLib) {
...
}]);
Small Controllers, Small Services
* Break controllers into smaller sub-controllers if it gets too large* Separation of Concerns & Single Responsibility
* Eliminate the word "and" in your service's description
* Services should be loosely coupled
Too tighly coupled
angular.module('app').service('productService',
['$http', 'cartService', 'statsEngine'
function ($http, CartService, statsEngine) {
this.getProduct = function () {...}
this.updateProduct = function (id, product) {...}
this.getProductStatistics = function (id) {...}
this.addProductToCart = function (id) {...}
...
}]);
Would be combersome to mock all the dependencies in tests
......A Better Approach
angular.module('app')
.service('productLoader',
['$http', function ($http) {
this.getProduct = function () {...}
...
}]);
.service('productUpdater',
['$http', function ($http) {
this.purchaseProduct = function () {...}
...
}]);
.service('productStats',
['$http', function ($statEngine) {
this.getProductStats = function () {...}
...
}]);
Use Routes + Resolve Pattern
* Available in $routeProvider or AngularUI Router* Define/divide resource loading by routes. Expose state on routes
* Good for E2E testing
Route Definition
angular.module("app")
.config(["$routeProvider",
function routeConfig($routeProvider) {
$routeProvider.
when("/", {
controller: "itemController",
templateUrl: "view/items.html",
resolve: {
items: function ($itemLoader, $q) {
var deferred = $q.defer();
itemLoader.loadItems().then(
function (items) {
deferred.resolve(helper);
});
return deferred.promise;
}
}
});
}]);
angular.module("app")
.controller('itemController', ['items',
function (items) {
$scope.items = items;
}]);
"items" will be available when controller is initialized
Tips
Use $window, $location, $interval, instead of window, location, setInterval
So that dependencies are isolated
Use $log.info(), .debug(), .error(), etc. instead of console.log
So that your tests won't be litered with log messages
Use angular.copy, angular.extend, angular.forEach
Wrap external js library into their own minimal services
Use $interval instead of $timeout
So that E2E testing won’t return a timeout error
References
Angular Best Practices and anti-patterns https://github.com/angular/angular.js/wiki/Best-Practices
Joe Eames, AngularJS Best Practices, Pluralsight.com
Miško Hevery, Writing Testable Code, http://googletesting.blogspot.ca/2008/08/by-miko-hevery-so-you-decided-to.html
Mark Ethan Trostler, Testable JavaScript, O'Reilly Media, Jan 2013
No comments:
Post a Comment