Cet article a pour but de résumer les principaux éléments à connaître quand on débute avec AngularJS. Cela ne dispense pas d’aller voir des tutoriels ou la documentation pour approfondir le sujet.
Concepts
Les principaux concepts d’Angular 1.6 qui seront développés dans cette fiche de rappels :
Architecture du code
Voir les conseils proposés par Angular Seed sur github ou encore ceux de John Papa.
Quelques exemples, à adapter en fonction de la taille du projet.
Modulaire | Orienté micro-service |
---|---|
app/ app/app.css app/app.js app/index.html app/components/component1/componentController.js app/components/component1/componentController.spec.js app/components/component1/component-directive.js app/components/component1/component-directive.spec.js app/components/component1/some.filter.js app/module1/module1.module.js app/module1/config-routes.js app/module1/view1/view.html app/module1/view1/viewController.js app/module1/view1/viewController.spec.js app/module1/view1/someService.js app/module1/view1/someService.spec.js lib/**.js xxxx.conf.js e2e-tests/protractor-conf.js e2e-tests/scenarios.js e2e-tests/mock-data.json |
app/ app/app.css app/app.js app/index.html app/components/component1/componentController.js app/components/component1/componentController.spec.js app/components/component1/component-directive.js app/components/component1/component-directive.spec.js app/components/component1/some.filter.js app/views/view1.html app/ctrl/view1Controller.js app/ctrl/view1Controller.spec.js lib/**.js xxxx.conf.js e2e-tests/protractor-conf.js e2e-tests/scenarios.js e2e-tests/mock-data.json |
Conventions
Fichiers :
- Ecriture des fonctions sous la norme IIFE (Immediately-Invoked Function Expression) pour protéger la visibilité des variables.
- Les contrôleurs :
xxxxController.js
- Les tests unitaires :
xxxx.spec.js
. Reprend le nom du controleur ou du service. Dans le même package. - Les directives, les filtres :
xxxx-directive.js
,xxxx-filter.js
Variables :
- Préfixées de
$
: Service fourni par Angular. Ex.$scope, $routeParams, ...
vm
: ViewModel. Utilisé dans les contrôleurs, évite l’utilisation du$scope
et des problèmes d’héritage ($parent
).- Modules : Nom = Nom de l’application + « . » + nom du module. Ex. hello.module1
- Contrôleurs : la méthode d’initialisation s’appelle
activate()
en général. - Séparer clairement le contexte public (valeurs de retour) du contexte privé dans les fonctions.
- Directives : préfixer les noms pour éviter les surcharges (Ex.
my
:myBadge
). - Liens Href :
#!/myRoute
remplace#/myRoute
à partir d’Angular 1.6.
Initialisation : Exemple de Index.html
<!DOCTYPE html> <!-- Définition de l'application Angular (Angular n'est pas exécuté en dehors de l'élément ng-app) --> <html ng-app="hello"> <!-- ng-strict-di pour forcer la protection contre la minification du code (injections explicites) --> <head> <link href="app.css" rel="stylesheet" /> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.js"></script> <script src="https://code.angularjs.org/1.5.8/angular-route.js"></script> <script src="app.js"></script> <!-- Directives, Modules, Controllers, routes, etc. --> </head> <body> <!-- Si pas associé dans le routage (ngRoute), on peut définir le controller ici via ng-controller="controllerName" --> <div ng-view=""></div> </body> </html>
Initialisation du contexte Angular : app.js
(function() { var app = angular.module("hello", ['ngRoute', 'hello.module1', 'hello.module2', ...]); // Route par défaut (si aucun module n'a traité) app.config(function($routeProvider) { $routeProvider .otherwise({ redirectTo: "/" }) }); }());
Les modules : module1/Module1Module.js
(function() { 'use strict'; // Création du module module1 avec lien vers dépendances si nécessaire angular.module("app.module1", []); }());
Le routing : module/config-routes.js
ngRoute
nécessite le paquet angular-route.js.
(function() { 'use strict'; // Configuration du module module1 : angular.module("app.module1", ['ngRoute']).config(config); function config($routeProvider) { $routeProvider .when("/", { templateUrl: "module1/views/view.html", controller: "ViewCtrl" }) .when("/module1/:username", { templateUrl: "module1/views/view.html", controller: "UserCtrl", // Appelle la fonction au moment de résoudre le routage : resolve: { myResolverService: myResolverService } }); } myResolverService.$inject = ['$log']; function myResolverService($log) { ... } ());
Les Services : module1/MyService.js
Quelques services fournis par angular : $http ; $interval ; $timeout ; $log ; $animate ; $location ; $browser ; $q ; $window ; $anchorScroll ; $routeParams ; $controller(jasmine) ;
…
(function() { 'use strict'; // Création du service. NB : Un service est indépendant du contexte vue/contrôleur. // .factory() => Singleton ; contrairement à .service() angular.module("app.module1").factory("myService1", MyService); // Utilisation des services angular http et log : MyService.$inject = ['$http', '$log']; function MyService($http, $log) { // Un service retourne un objet fournissant des services (fonctions). var service = { getUserProfile: getUserProfile, } return service; // Séparation zone publique / Privée /////////////////////////// function getUserProfile(username) { // local vars... var selection = null; // return une promise return $http.get("...").then(function(res) { // service code selection = { }; return { selection: selection, } }); } }; }());
Test des services : module1/MyService.spec.js
describe("Chapter 1 : User Service", function() { var service, http; var mockData = '{ "users": [{"id":1}, {"id":2}, {"id":3}] }'; // Chargement du module beforeEach(module('app.module1')); // Injection du service à tester et du service angular $http beforeEach(inject(function(myService1, $httpBackend) { service = myService1; http = $httpBackend; })); // Test 1 it("Should ...", function(done) { var testData = function(data) { expect(data.users.length).toBe(3); }; var testFail = function(error) { expect(error).toBeUndefined(); }; // Mock de la méthode http.get appelée depuis myService1 http.expectGET(...).respond(200, mockData); // Espionnage : var mySpy = spyOn(service, 'getUserProfile'); // .and.callFake(...); service.getUserProfile(null).then(testData).catch(testFail).finally(done); // Espionnage : expect(spy).toHaveBeenCalled(); http.flush(); }); });
Les contrôleurs : module1/MyController.js
(function() { 'use strict'; // Enregistrement du contrôleur auprès du module hôte angular.module("app.module1").controller("MyCtrl", MyCtrl); // MyCtrl va utiliser MyService et le service $log d'angular. MyCtrl.$inject = ['myService1', '$log']; function MyCtrl(myService, $log) { // local vars var vm = this; vm.sortBy = "name"; vm.users = []; activate(); // Séparation public / privé /////////// function activate() { getUsers(); } function getUsers(username) { myService.getUserProfile(username).then(function(obj) { vm.users = obj.users; }); } function search() { ... } function sortBy(attribute) { ... } }; }());
Test des contrôleurs : module1/MyController.spec.js
// Jasmine test file describe("Chapitre 1 : My Controller ", function() { var $ctrl; var scope; // Chargement du module au début du test beforeEach(module('app.module1')); // Injection d'un constructeur du contrôleur (pour créer un mock) beforeEach(inject(function($controller, $rootScope) { $ctrl = $controller; scope = $rootScope.$new(); })); // Test unitaire 1 it("should .... ", function() { var ctrl = $ctrl('MyCtrl'); expect(ctrl.value).toBe(42); }); });
Les directives : directives/badge-directive.js
(function() { angular.module('hello').directive('myBadge', getBadge); function getBadge() { return { restrict: 'E', // [E]lement ; [A]ttribute ; [C]lass ; Co[M]ment. Default: EA bindToController: true, // Association avec les variables du $scope parent du contrôleur. scope: { user: '=user', error: '=error' }, templateUrl: "common/directives/badge.html" } } }());
Les directives : directives/badge.html
<div> <div ng-if="!error"> <img ng-if="user.avatar_url" ng-src="{{user.avatar_url}}" alt="user"/> </div> <!-- ngIf --> <div nf-if="error" style="color: red;">{{error}}</div> </div>
Les directives : module1/view.html
<!-- NB : myBadge en javascript devient my-badge en html --> <my-badge user="vm.foundUser" error="vm.error"></my-badge>
Les filtres : view.html
Quelques filtres fournis par Angular : currency ; date ; filter ; json ; limitTo ; lowercase ; uppercase ; number ; orderBy
, …
<!-- vm.filtername = écriture de la valeur saisie dans le contrôleur --> <input type="search" ng-model="vm.filterName" /> <!-- Filtrage des utilisateurs par rapport à la valeur saisie filterName --> <p>There are {{(vm.users | filter:vm.filterName).length}} matching users.</p> <!-- ... --> <!-- Filtrage par rapport à la valeur saisie + tri par ordre défini par le contrôleur (Ex. +name/-name) --> <tr ng-repeat="user in vm.users | filter:vm.filterName | orderBy:vm.sortWay+vm.sortAttr"> <!-- ... --> </tr>
Les watchers
$scope.watch("localVar", function(newValue, oldValue) { ... });
Les test unitaires avec Jasmine : SpecRunner.js
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.js"></script> <script src="https://code.angularjs.org/1.5.8/angular-route.js"></script> <script src="https://code.angularjs.org/1.6.2/angular-mocks.js"></script> <!-- source files --> <script src="module1/Module1Module.js" type="text/javascript" charset="utf-8"></script> <script src="module1/MyService1.js" type="text/javascript" charset="utf-8"></script> <script src="module1/MyCtrl.js" type="text/javascript" charset="utf-8"></script> <!-- spec files --> <script type="text/javascript" src="duke/DukesCtrl.spec.js"></script> <script type="text/javascript" src="duke/DukeService.spec.js"></script> </head> <body></body> </html>
Tests
Inscription des tests dans le cycle de vie du développement :
Tests unitaires avec Jasmine.js
Tests unitaires de non régression destinés à être lancés pendant la phase de développement (ou avant, en TDD). Ciblent un composant (contrôleur) particulier.
Structure d’un test
describe("Title", function() { // only => Isoler un test ou un chapitre (describe.only) it[.only]("Should blah blah blah"), function() { expect(...); }); }
Tests HTTP
// Mock d'une réponse HTTP : $httpBackend.when("GET", "path/to/resource").respond(200, {data}); // Test asynchrone myService.get("...").then(function(data) { expect(...); }) .catch(function(reason) { expect(...); }); $httpBackend.flush();
Tests d’intégration (end-to-end) avec Protractor.js
Utilise le moteur Selenium pour lancer les navigateurs et simuler le comportement de l’utilisateur. Gère les clics et la saisie clavier. Un test correspond à un scénario sur l’application globale (non couplé fortement à un contrôleur en particulier).
Le fichier de configuration spec.js
permet d’initialiser le contexte du test (navigateur, taille écran, position du navigateur, etc.
Pour tester en headless : cf. projet headless Chrome XVFB + Docker
Installation (voir intro-to-protractor sur github) :
npm install -g protractor webdriver-manager update # Démarrage d'un serveur HTTP Jetty port 4444 webdriver-manager start # Initialisation du projet npm install bower install start mongodb run grunt
Exemple de script de test, au format Jasmine :
describe("Chapitre 1", function() { it("Should ....", function() { browser.get("...."); var firstElement = element.all(by.binding('name')).first(); firstElement.getText().then(...); firstElement.click(); browser.waitForAngular(); ... } });
Les locators disponibles (sélection des composants dans la vue) : by.binding ; by.model ; by.css ; by.buttonText ; by.repeater ; by.options ; by.id ; by.linkTest ; by.name ; by.tagName ; by.xPath ;
…
Conseils Archi : Créer une page PageObjects pour faire l’association entre les éléments de la vue et les scénarios de tests. Le fichier ‘spec’ utilise les constantes définies dans la PageObjets pour référencer les objets / méthodes. Une seule référence à la fois ; pas de duplication de code ; refactoring simplifié lorsque la vue change.
// create.page.js : module.exports = function() { this.name = element(by.model(item.name)); ... } // create.spec.js : var CreatePage = require('create.page.js'); ...
Ecosystème
Outils principaux gravitant autour de l’écosystème AngularJS :
Architecture
Architecture type d’un projet Angular.
« Échaffaudeur » de nouveaux projets, par code. Liste des générateurs sur yeoman.io/generators
# Générer un nouveau projet Angular (avec Gulp et Bower) yo yang yo yang --help # Installer Yeoman : npm install -g yo # Générer un controleur dans un projet existant, avec la vue : yo yang:ngc HelpCtrl --view # Vérifier l'installation yodoctor # Outil interactif yo
Environnement de développement
Éditeur en ligne permettant d’effectuer des tests rapides sans installation nécessaire.
L’outil de Microsoft.
L’éditeur de JetBrains, 30 jours gratuits.
Eclipse IDE.
Outils de builds
Récupère les librairies nécessaires à partir d’un fichier de configuration.
Automate de builds.
Minification du code.
Intégration Continue (CI)
Intégration continue Travis CI.
Permet l’hébergement de ses projets Angular sur le Cloud.
Librairies de composants graphiques
Editeur de code compatible Angular.
Librairie CSS standard. Pour Angular 2.
Thèmes gratuits pour Bootstrap.
Construction d’applications mobiles natives ; Angular friendly.
Composants avancés avec angular-ui
Regroupe les librairies suivantes : angular ; angular-animator ; angular-route ; bootstrap ; lodash ; moment ; spin.js ; angular-bootstrap
.
- ui-bootstrap :
- ui-router :
- ui-module :
- ui-utils :
Regroupe un ensemble de composants pour Angular, non dépendants de jQuery
Remplace ngRoute. Gestion par ‘state’. $state, $stateProvider.
Ex. UIGmap (Google Map) ; UI-Calendar (Agenda) ; UI-Aca (ACE Editor).
Outils sans dépendances. Ex. scroll ; highlight ; format ; toggle ; …
Material design avec angular-material
Installation de Angular-material :
npm install angular-material npm init npm install -g jspm bower install angular-material
Quelques ressources à voir :
Outils de tests
Pour effectuer des tests unitaires.
Pour effectuer des tests d’intégration.
Batterie de tests pour Angular, utilisant Jasmine. Fournit les tests unitaires (Jasmine) ; un File Watcher ; Multiple browsers ; indépendant des frameworks ; rapide ; en lignes de commandes.
Permet de voir en simultané dans son navigateur les changements apportés à son code.
Code coverage.