Top 18 des erreurs les plus courantes commises par les développeurs AngularJS

Publié: 2022-03-11

Les applications d'une seule page exigent que les développeurs frontaux deviennent de meilleurs ingénieurs logiciels. CSS et HTML ne sont plus la principale préoccupation, en fait, il n'y a plus qu'une seule préoccupation. Le développeur front-end doit gérer les XHR, la logique d'application (modèles, vues, contrôleurs), les performances, les animations, les styles, la structure, le référencement et l'intégration avec des services externes. Le résultat qui ressort de tous ceux réunis est l'expérience utilisateur (UX) qui doit toujours être prioritaire.

AngularJS est un framework très puissant. C'est le troisième référentiel le plus étoilé sur GitHub. Il n'est pas difficile de commencer à l'utiliser, mais les objectifs qu'il est destiné à atteindre exigent une compréhension. Les développeurs AngularJS ne peuvent plus ignorer la consommation de mémoire, car elle ne se réinitialisera plus lors de la navigation. C'est l'avant-garde du développement web. Embrassons-le !

Erreurs courantes d'AngularJS

Erreur courante n° 1 : accéder au champ d'application via le DOM

Quelques ajustements d'optimisation sont recommandés pour la production. L'un d'eux est la désactivation des informations de débogage.

DebugInfoEnabled est un paramètre dont la valeur par défaut est true et permet l'accès à la portée via les nœuds DOM. Si vous voulez essayer cela via la console JavaScript, sélectionnez un élément DOM et accédez à sa portée avec :

 angular.element(document.body).scope()

Il peut être utile même lorsque jQuery n'est pas utilisé avec son CSS, mais ne doit pas être utilisé en dehors de la console. La raison étant que lorsque $compileProvider.debugInfoEnabled est défini sur false, l'appel .scope() sur un nœud DOM renverra undefined .

C'est l'une des rares options recommandées pour la production.

Veuillez noter que vous pouvez toujours accéder à l'étendue via la console, même en production. Appelez angular.reloadWithDebugInfo() depuis la console et l'application fera exactement cela.

Erreur courante #2 : Ne pas avoir de point là-dedans

Vous avez probablement lu que si vous n'aviez pas de point dans votre ng-model , vous le faisiez mal. En ce qui concerne l'héritage, cette affirmation est souvent vraie. Les étendues ont un modèle prototype d'héritage, typique de JavaScript, et les étendues imbriquées sont communes à AngularJS. De nombreuses directives créent des portées enfants telles que ngRepeat , ngIf et ngController . Lors de la résolution d'un modèle, la recherche commence sur la portée actuelle et passe par chaque portée parente, jusqu'à $rootScope .

Mais, lors de la définition d'une nouvelle valeur, ce qui se passe dépend du type de modèle (variable) que nous voulons changer. Si le modèle est une primitive, la portée enfant créera simplement un nouveau modèle. Mais si la modification concerne une propriété d'un objet de modèle, la recherche sur les portées parent trouvera l'objet référencé et modifiera sa propriété réelle. Un nouveau modèle ne serait pas défini sur le périmètre actuel, donc aucun masquage ne se produirait :

 function MainController($scope) { $scope.foo = 1; $scope.bar = {innerProperty: 2}; } angular.module('myApp', []) .controller('MainController', MainController);
 <div ng-controller="MainController"> <p>OUTER SCOPE:</p> <p>{{ foo }}</p> <p>{{ bar.innerProperty }}</p> <div ng-if="foo"> <!— ng-if creates a new scope —> <p>INNER SCOPE</p> <p>{{ foo }}</p> <p>{{ bar.innerProperty }}</p> <button ng-click="foo = 2">Set primitive</button> <button ng-click="bar.innerProperty = 3">Mutate object</button> </div> </div>

Cliquer sur le bouton intitulé "Définir la primitive" définira foo dans la portée interne sur 2, mais ne changera pas foo dans la portée externe.

Cliquer sur le bouton intitulé "Modifier l'objet" changera la propriété de la barre de la portée parent. Puisqu'il n'y a pas de variable sur la portée interne, aucune ombre ne se produira et la valeur visible pour la barre sera de 3 dans les deux portées.

Une autre façon de procéder consiste à tirer parti du fait que les étendues parentes et l'étendue racine sont référencées à partir de chaque étendue. Les objets $parent et $root peuvent être utilisés pour accéder à la portée parent et $rootScope , respectivement, directement depuis la vue. C'est peut-être un moyen puissant, mais je n'en suis pas fan en raison du problème de ciblage d'une portée particulière en amont du flux. Il existe un autre moyen de définir et d'accéder aux propriétés spécifiques à une portée - en utilisant la syntaxe controllerAs .

Erreur courante n° 3 : ne pas utiliser la syntaxe controllerAs

Le moyen alternatif et le plus efficace d'affecter des modèles pour utiliser un objet contrôleur au lieu de la portée $ injectée. Au lieu d'injecter de la portée, nous pouvons définir des modèles comme celui-ci :

 function MainController($scope) { this.foo = 1; var that = this; var setBar = function () { // that.bar = {someProperty: 2}; this.bar = {someProperty: 2}; }; setBar.call(this); // there are other conventions: // var MC = this; // setBar.call(this); when using 'this' inside setBar() }
 <div> <p>OUTER SCOPE:</p> <p>{{ MC.foo }}</p> <p>{{ MC.bar.someProperty }}</p> <div ng-if="test1"> <p>INNER SCOPE</p> <p>{{ MC.foo }}</p> <p>{{ MC.bar.someProperty }}</p> <button ng-click="MC.foo = 3">Change MC.foo</button> <button ng-click="MC.bar.someProperty = 5">Change MC.bar.someProperty</button> </div> </div>

C'est beaucoup moins déroutant. Surtout lorsqu'il existe de nombreuses étendues imbriquées, comme cela peut être le cas avec les états imbriqués.

Il y a plus dans la syntaxe controllerAs.

Erreur courante n°4 : ne pas utiliser pleinement la syntaxe controllerAs

Il y a quelques mises en garde concernant la façon dont l'objet contrôleur est exposé. Il s'agit essentiellement d'un objet défini sur la portée du contrôleur, tout comme un modèle normal.

Si vous avez besoin de surveiller une propriété de l'objet contrôleur, vous pouvez surveiller une fonction mais vous n'y êtes pas obligé. Voici un exemple:

 function MainController($scope) { this.title = 'Some title'; $scope.$watch(angular.bind(this, function () { return this.title; }), function (newVal, oldVal) { // handle changes }); }

Il est plus simple de faire simplement :

 function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }

Cela signifie que vous pouvez également accéder à MC à partir d'un contrôleur enfant :

 function NestedController($scope) { if ($scope.MC && $scope.MC.title === 'Some title') { $scope.MC.title = 'New title'; } }

Cependant, pour pouvoir le faire, vous devez être cohérent avec l'acronyme que vous utilisez pour controllerAs. Il existe au moins trois façons de le régler. Vous avez déjà vu le premier :

 <div ng-controller="MainController as MC"> … </div>

Cependant, si vous utilisez ui-router , la spécification d'un contrôleur de cette manière est sujette à erreur. Pour les états, les contrôleurs doivent être spécifiés dans la configuration de l'état :

 angular.module('myApp', []) .config(function ($stateProvider) { $stateProvider .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/path/to/template.html' }) }). controller('MainController', function () { … });

Il existe une autre façon d'annoter :

 (…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })

Vous pouvez faire la même chose dans les directives :

 function AnotherController() { this.text = 'abc'; } function testForToptal() { return { controller: 'AnotherController as AC', template: '<p>{{ AC.text }}</p>' }; } angular.module('myApp', []) .controller('AnotherController', AnotherController) .directive('testForToptal', testForToptal);

L'autre façon d'annoter est également valable, quoique moins concise :

 function testForToptal() { return { controller: 'AnotherController', controllerAs: 'AC', template: '<p>{{ AC.text }}</p>' }; }

Erreur courante n° 5 : ne pas utiliser les vues nommées avec UI-ROUTER pour l'alimentation »

La solution de routage de facto pour AngularJS était, jusqu'à présent, le ui-router . Supprimé du noyau il y a quelque temps, le module ngRoute était trop basique pour un routage plus sophistiqué.

Un nouveau NgRouter est en route, mais les auteurs le considèrent encore trop tôt pour la production. Au moment où j'écris ceci, la version stable d'Angular est 1.3.15 et ui-router bascule.

Les raisons principales :

  • superbe état de nidification
  • abstraction d'itinéraire
  • paramètres facultatifs et obligatoires

Ici, je couvrirai l'imbrication d'états pour éviter les erreurs AngularJS.

Considérez cela comme un cas d'utilisation complexe mais standard. Il existe une application, qui a une vue de page d'accueil et une vue de produit. La vue du produit comporte trois sections distinctes : l'intro, le widget et le contenu. Nous voulons que le widget persiste et ne se recharge pas lors du changement d'état. Mais le contenu devrait se recharger.

Considérez la structure de page d'index de produit HTML suivante :

 <body> <header> <!-- SOME STATIC HEADER CONTENT --> </header> <section class="main"> <div class="page-content"> <div class="row"> <div class="col-xs-12"> <section class="intro"> <h2>SOME PRODUCT SPECIFIC INTRO</h2> </section> </div> </div> <div class="row"> <div class="col-xs-3"> <section class="widget"> <!-- some widget, which should never reload --> </section> </div> <div class="col-xs-9"> <section class="content"> <div class="product-content"> <h2>Product title</h2> <span>Context-specific content</span> </div> </section> </div> </div> </div> </section> <footer> <!-- SOME STATIC HEADER CONTENT --> </footer> </body>

C'est quelque chose que nous pourrions obtenir du codeur HTML, et nous devons maintenant le séparer en fichiers et en états. Je pars généralement avec la convention selon laquelle il existe un état MAIN abstrait, qui conserve les données globales si nécessaire. Utilisez-le au lieu de $rootScope. L'état principal conservera également le code HTML statique requis sur chaque page. Je garde index.html propre.

 <!— index.html —> <body> <div ui-view></div> </body>
 <!— main.html —> <header> <!-- SOME STATIC HEADER CONTENT --> </header> <section class="main"> <div ui-view></div> </section> <footer> <!-- SOME STATIC HEADER CONTENT --> </footer>

Voyons ensuite la page d'index des produits :

 <div class="page-content"> <div class="row"> <div class="col-xs-12"> <section class="intro"> <div ui-view="intro"></div> </section> </div> </div> <div class="row"> <div class="col-xs-3"> <section class="widget"> <div ui-view="widget"></div> </section> </div> <div class="col-xs-9"> <section class="content"> <div ui-view="content"></div> </section> </div> </div> </div>

Comme vous pouvez le constater, la page d'index des produits comporte trois vues nommées. Un pour l'intro, un pour le widget et un pour le produit. Nous respectons les spécifications! Alors maintenant, configurons le routage :

 function config($stateProvider) { $stateProvider // MAIN ABSTRACT STATE, ALWAYS ON .state('main', { abstract: true, url: '/', controller: 'MainController as MC', templateUrl: '/routing-demo/main.html' }) // A SIMPLE HOMEPAGE .state('main.homepage', { url: '', controller: 'HomepageController as HC', templateUrl: '/routing-demo/homepage.html' }) // THE ABOVE IS ALL GOOD, HERE IS TROUBLE // A COMPLEX PRODUCT PAGE .state('main.product', { abstract: true, url: ':id', controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html', }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'widget': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' }, 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }) // PRODUCT DETAILS SUBSTATE .state('main.product.details', { url: '/details', views: { 'widget': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }); } angular.module('articleApp', [ 'ui.router' ]) .config(config);

Ce serait la première approche. Maintenant, que se passe-t-il lors du basculement entre main.product.index et main.product.details ? Le contenu et le widget sont rechargés, mais nous voulons uniquement recharger le contenu. C'était problématique et les développeurs ont en fait créé des routeurs qui ne prendraient en charge que cette fonctionnalité. L'un des noms pour cela était les vues collantes . Heureusement, ui-router prend cela en charge avec un ciblage de vue nommé absolu .

 // A COMPLEX PRODUCT PAGE // WITH NO MORE TROUBLE .state('main.product', { abstract: true, url: ':id', views: { // TARGETING THE UNNAMED VIEW IN MAIN.HTML '@main': { controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html' }, // TARGETING THE WIDGET VIEW IN PRODUCT.HTML // BY DEFINING A CHILD VIEW ALREADY HERE, WE ENSURE IT DOES NOT RELOAD ON CHILD STATE CHANGE '[email protected]': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' } } }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }) // PRODUCT DETAILS SUBSTATE .state('main.product.details', { url: '/details', views: { 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } });

En déplaçant la définition d'état vers la vue parent, qui est également abstraite, nous pouvons empêcher la vue enfant de se recharger lors du changement d'URL, ce qui affecte normalement les frères et sœurs de cet enfant. Bien sûr, le widget pourrait être une simple directive. Mais le fait est qu'il pourrait également s'agir d'un autre état imbriqué complexe.

Il existe une autre façon de le faire en utilisant $urlRouterProvider.deferIntercept() , mais je pense que l'utilisation de la configuration d'état est en fait meilleure. Si vous êtes intéressé par l'interception des routes, j'ai écrit un petit tutoriel sur StackOverflow.

Erreur courante n° 6 : Tout déclarer dans le monde angulaire à l'aide de fonctions anonymes

Cette erreur est d'un calibre plus léger et est plus une question de style que d'éviter les messages d'erreur AngularJS. Vous avez peut-être déjà remarqué que je passe rarement des fonctions anonymes aux déclarations d'angular internal. En général, je définis d'abord une fonction, puis je la transmets.

Cela concerne plus que les fonctions. J'ai eu cette approche en lisant des guides de style, en particulier ceux d'Airbnb et de Todd Motto. Je crois qu'il y a plusieurs avantages et presque aucun inconvénient.

Tout d'abord, vous pouvez manipuler et faire muter vos fonctions et objets beaucoup plus facilement s'ils sont assignés à une variable. Deuxièmement, le code est plus propre et peut être facilement divisé en fichiers. Cela signifie la maintenabilité. Si vous ne voulez pas polluer l'espace de noms global, enveloppez chaque fichier dans des IIFE. La troisième raison est la testabilité. Considérez cet exemple :

 'use strict'; function yoda() { var privateMethod = function () { // this function is not exposed }; var publicMethod1 = function () { // this function is exposed, but it's internals are not exposed // some logic... }; var publicMethod2 = function (arg) { // THE BELOW CALL CANNOT BE SPIED ON WITH JASMINE publicMethod1('someArgument'); }; // IF THE LITERAL IS RETURNED THIS WAY, IT CAN'T BE REFERRED TO FROM INSIDE return { publicMethod1: function () { return publicMethod1(); }, publicMethod2: function (arg) { return publicMethod2(arg); } }; } angular.module('app', []) .factory('yoda', yoda);

Alors maintenant, nous pourrions nous moquer de publicMethod1 , mais pourquoi devrions-nous faire cela puisqu'il est exposé ? Ne serait-il pas plus simple d'espionner la méthode existante ? Cependant, la méthode est en fait une autre fonction - une enveloppe mince. Jetez un oeil à cette approche:

 function yoda() { var privateMethod = function () { // this function is not exposed }; var publicMethod1 = function () { // this function is exposed, but it's internals are not exposed // some logic... }; var publicMethod2 = function (arg) { // the below call cannot be spied on publicMethod1('someArgument'); // BUT THIS ONE CAN! hostObject.publicMethod1('aBetterArgument'); }; var hostObject = { publicMethod1: function () { return publicMethod1(); }, publicMethod2: function (arg) { return publicMethod2(arg); } }; return hostObject; }

Ce n'est pas seulement une question de style, car en effet le code est plus réutilisable et idiomatique. Le développeur obtient plus de puissance expressive. Le fractionnement de tout le code en blocs autonomes facilite simplement les choses.

Erreur courante n ° 7 : effectuer des traitements lourds dans Angular AKA en utilisant des travailleurs

Dans certains scénarios, il peut être nécessaire de traiter un large éventail d'objets complexes en les faisant passer par un ensemble de filtres, de décorateurs et enfin d'un algorithme de tri. Un cas d'utilisation est lorsque l'application doit fonctionner hors ligne ou lorsque les performances d'affichage des données sont essentielles. Et puisque JavaScript est monothread, il est relativement facile de geler le navigateur.

Il est également facile de l'éviter avec les web workers. Il ne semble pas y avoir de bibliothèques populaires qui gèrent cela spécifiquement pour AngularJS. C'est peut-être pour le mieux, car la mise en œuvre est facile.

Tout d'abord, configurons le service :

 function scoringService($q) { var scoreItems = function (items, weights) { var deferred = $q.defer(); var worker = new Worker('/worker-demo/scoring.worker.js'); var orders = { items: items, weights: weights }; worker.postMessage(orders); worker.onmessage = function (e) { if (e.data && e.data.ready) { deferred.resolve(e.data.items); } }; return deferred.promise; }; var hostObject = { scoreItems: function (items, weights) { return scoreItems(items, weights); } }; return hostObject; } angular.module('app.worker') .factory('scoringService', scoringService);

Maintenant, l'ouvrier :

 'use strict'; function scoringFunction(items, weights) { var itemsArray = []; for (var i = 0; i < items.length; i++) { // some heavy processing // itemsArray is populated, etc. } itemsArray.sort(function (a, b) { if (a.sum > b.sum) { return -1; } else if (a.sum < b.sum) { return 1; } else { return 0; } }); return itemsArray; } self.addEventListener('message', function (e) { var reply = { ready: true }; if (e.data && e.data.items && e.data.items.length) { reply.items = scoringFunction(e.data.items, e.data.weights); } self.postMessage(reply); }, false);

Maintenant, injectez le service comme d'habitude et traitez scoringService.scoreItems() comme vous le feriez pour n'importe quelle méthode de service qui renvoie une promesse. Le traitement lourd sera effectué sur un thread séparé et aucun dommage ne sera causé à l'UX.

A quoi faire attention :

  • il ne semble pas y avoir de règle générale quant au nombre d'ouvriers à engendrer. Certains développeurs affirment que 8 est un bon nombre, mais utilisez une calculatrice en ligne et adaptez-vous
  • vérifier la compatibilité avec les anciens navigateurs
  • Je rencontre un problème lors du passage du numéro 0 du service au travailleur. J'ai appliqué .toString() sur la propriété passée, et cela a fonctionné correctement.

Erreur courante n° 8 : L'utilisation excessive et les malentendus résolvent

Les résolutions ajoutent du temps supplémentaire au chargement de la vue. Je crois que la haute performance de l'application frontale est notre objectif principal. Le rendu de certaines parties de la vue ne devrait pas poser de problème pendant que l'application attend les données de l'API.

Considérez cette configuration :

 function resolve(index, timeout) { return { data: function($q, $timeout) { var deferred = $q.defer(); $timeout(function () { deferred.resolve(console.log('Data resolve called ' + index)); }, timeout); return deferred.promise; } }; } function configResolves($stateProvide) { $stateProvider // MAIN ABSTRACT STATE, ALWAYS ON .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/routing-demo/main.html', resolve: resolve(1, 1597) }) // A COMPLEX PRODUCT PAGE .state('main.product', { url: ':id', controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html', resolve: resolve(2, 2584) }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } }, resolve: resolve(3, 987) }); }

La sortie de la console sera :

 Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed

Ce qui signifie en gros que :

  • Les résolutions sont exécutées de manière asynchrone
  • Nous ne pouvons pas nous fier à un ordre d'exécution (ou du moins, nous devons fléchir un peu)
  • Tous les états sont bloqués jusqu'à ce que toutes les résolutions fassent leur travail, même si elles ne sont pas abstraites.

Cela signifie qu'avant que l'utilisateur ne voie une sortie, il doit attendre toutes les dépendances. Nous avons besoin de ces données, bien sûr, d'accord. S'il est absolument nécessaire de l'avoir avant la vue, mettez-le dans un bloc .run() . Sinon, appelez simplement le service à partir du contrôleur et gérez l'état à moitié chargé avec élégance. Voir le travail en cours - et le contrôleur est déjà exécuté, il s'agit donc en fait d'une progression - est préférable à l'arrêt de l'application.

Erreur courante #9 : ne pas optimiser l'application - trois exemples

a) Causer trop de boucles de résumé, comme attacher des curseurs aux modèles

Il s'agit d'un problème général qui peut entraîner des erreurs AngularJS, mais je vais en discuter à l'exemple des curseurs. J'utilisais cette bibliothèque de curseurs, curseur de plage angulaire, car j'avais besoin de fonctionnalités étendues. Cette directive a cette syntaxe dans la version minimale :

 <body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.price" > </div> </body>

Considérez le code suivant dans le contrôleur :

 this.maxPrice = '100'; this.price = '55'; $scope.$watch('MC.price', function (newVal) { if (newVal || newVal === 0) { for (var i = 0; i < 987; i++) { console.log('ALL YOUR BASE ARE BELONG TO US'); } } });

Donc ça marche lentement. La solution simple serait de définir un délai d'attente sur l'entrée. Mais ce n'est pas toujours pratique, et parfois nous ne voulons pas vraiment retarder le changement de modèle réel dans tous les cas.

Nous allons donc ajouter un modèle temporaire destiné à changer le modèle de travail à l'expiration du délai :

 <body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.priceTemporary" > </div> </body>

et dans le contrôleur :

 this.maxPrice = '100'; this.price = '55'; this.priceTemporary = '55'; $scope.$watch('MC.price', function (newVal) { if (!isNaN(newVal)) { for (var i = 0; i < 987; i++) { console.log('ALL YOUR BASE ARE BELONG TO US'); } } }); var timeoutInstance; $scope.$watch('MC.priceTemporary', function (newVal) { if (!isNaN(newVal)) { if (timeoutInstance) { $timeout.cancel(timeoutInstance); } timeoutInstance = $timeout(function () { $scope.MC.price = newVal; }, 144); } });

b) Ne pas utiliser $applyAsync

AngularJS n'a pas de mécanisme d'interrogation pour appeler $digest() . Il n'est exécuté que parce que nous utilisons les directives (par exemple ng-click , input ), les services ( $timeout , $http ) et les méthodes ( $watch ) qui évaluent notre code et appellent ensuite un résumé.

Ce que .$applyAsync() fait, c'est qu'il retarde la résolution des expressions jusqu'au prochain cycle $digest() , qui est déclenché après un délai d'attente de 0, qui est en fait d'environ 10 ms.

Il existe deux façons d'utiliser applyAsync maintenant. Une méthode automatisée pour les requêtes $http et une méthode manuelle pour le reste.

Pour que toutes les requêtes http qui reviennent à peu près au même moment soient résolues en un seul résumé, procédez comme suit :

 mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });

La méthode manuelle montre comment cela fonctionne réellement. Considérez une fonction qui s'exécute sur le rappel d'un écouteur d'événement JS vanilla ou d'un jQuery .click() , ou d'une autre bibliothèque externe. Après avoir exécuté et modifié les modèles, si vous ne l'avez pas déjà enveloppé dans un $apply() , vous devez appeler $scope.$root.$digest() ( $rootScope.$digest() ), ou au moins $scope.$digest() . Sinon, vous ne verrez aucun changement.

Si vous faites cela plusieurs fois dans un flux, il peut commencer à fonctionner lentement. Pensez à appeler $scope.$applyAsync() sur les expressions à la place. Il ne définira qu'un seul cycle de résumé d'appel pour chacun d'eux.

c) Faire un traitement lourd des images

Si vous rencontrez de mauvaises performances, vous pouvez rechercher la raison en utilisant la chronologie des outils de développement Chrome. J'écrirai plus sur cet outil dans l'erreur #17. Si votre graphique chronologique est dominé par la couleur verte après l'enregistrement, vos problèmes de performances peuvent être liés au traitement des images. Ceci n'est pas strictement lié à AngularJS, mais peut survenir en plus des problèmes de performances d'AngularJS (qui seraient principalement jaunes sur le graphique). En tant qu'ingénieurs front-end, nous devons penser au projet final complet.

Prenez un moment pour évaluer :

  • Utilisez-vous la parallaxe ?
  • Avez-vous plusieurs couches de contenu qui se chevauchent ?
  • Déplacez-vous vos images ?
  • Redimensionnez-vous les images (par exemple avec la taille d'arrière-plan) ?
  • Redimensionnez-vous les images en boucles et provoquez-vous peut-être des boucles de résumé lors du redimensionnement ?

Si vous avez répondu "oui" à au moins trois des questions ci-dessus, envisagez de l'assouplir. Vous pouvez peut-être servir différentes tailles d'image et ne pas redimensionner du tout. Peut-être pourriez-vous ajouter le hack de traitement GPU "transform: translateZ (0)". Ou utilisez requestAnimationFrame pour les gestionnaires.

Erreur courante #10 : jQuerying It - Arbre DOM détaché

Plusieurs fois, vous entendez probablement qu'il n'est pas recommandé d'utiliser jQuery avec AngularJS, et qu'il faut l'éviter. Il est impératif de comprendre la raison derrière ces déclarations. Il y a au moins trois raisons, pour autant que je sache, mais aucune d'entre elles n'est un véritable bloqueur.

Raison 1 : lorsque vous exécutez du code jQuery, vous devez appeler vous-même $digest() . Dans de nombreux cas, il existe une solution AngularJS adaptée à AngularJS et pouvant être mieux utilisée dans Angular que jQuery (par exemple, ng-click ou le système d'événements).

Raison 2 : La méthode de réflexion sur la création de l'application. Si vous avez ajouté JavaScript à des sites Web, qui se rechargent lors de la navigation, vous n'avez pas trop à vous soucier de la consommation de mémoire. Avec les applications d'une seule page, vous devez vous inquiéter. Si vous ne nettoyez pas, les utilisateurs qui passent plus de quelques minutes sur votre application peuvent rencontrer des problèmes de performances croissants.

Raison 3 : Le nettoyage n'est en fait pas la chose la plus facile à faire et à analyser. Il n'y a aucun moyen d'appeler un ramasse-miettes à partir du script (dans le navigateur). Vous pouvez vous retrouver avec des arbres DOM détachés. J'ai créé un exemple (jQuery est chargé dans index.html):

 <section> <test-for-toptal></test-for-toptal> <button ng-click="MC.removeDirective()">remove directive</button> </section>
 function MainController($rootScope, $scope) { this.removeDirective = function () { $rootScope.$emit('destroyDirective'); }; } function testForToptal($rootScope, $timeout) { return { link: function (scope, element, attributes) { var destroyListener = $rootScope.$on('destroyDirective', function () { scope.$destroy(); }); // adding a timeout for the DOM to get ready $timeout(function () { scope.toBeDetached = element.find('p'); }); scope.$on('$destroy', function () { destroyListener(); element.remove(); }); }, template: '<div><p>I AM DIRECTIVE</p></div>' }; } angular.module('app', []) .controller('MainController', MainController) .directive('testForToptal', testForToptal);

Il s'agit d'une simple directive qui affiche du texte. Il y a un bouton en dessous, qui détruira simplement la directive manuellement.

Ainsi, lorsque la directive est supprimée, il reste une référence à l'arborescence DOM dans scope.toBeDetached. Dans les outils de développement Chrome, si vous accédez à l'onglet "profils" puis "prendre un instantané du tas", vous verrez dans la sortie :

Vous pouvez vivre avec quelques-uns, mais c'est mauvais si vous en avez une tonne. Surtout si pour une raison quelconque, comme dans l'exemple, vous le stockez sur l'oscilloscope. L'ensemble du DOM sera évalué à chaque résumé. L'arbre DOM détaché problématique est celui avec 4 nœuds. Alors, comment cela peut-il être résolu?

 scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });

L'arbre DOM détaché avec 4 entrées est supprimé !

Dans cet exemple, la directive utilise la même portée et stocke l'élément DOM sur la portée. C'était plus facile pour moi de le démontrer ainsi. Cela ne devient pas toujours si mauvais, car vous pouvez le stocker dans une variable. Cependant, cela occuperait toujours de la mémoire si une fermeture qui avait référencé cette variable ou toute autre de la même portée de fonction vivait.

Erreur courante n° 11 : surutiliser la portée isolée

Chaque fois que vous avez besoin d'une directive dont vous savez qu'elle sera utilisée à un seul endroit, ou dont vous ne vous attendez pas à entrer en conflit avec l'environnement dans lequel elle est utilisée, il n'est pas nécessaire d'utiliser une portée isolée. Dernièrement, il y a une tendance à créer des composants réutilisables, mais saviez-vous que les directives angulaires de base n'utilisent pas du tout de portée isolée ?

Il y a deux raisons principales : vous ne pouvez pas appliquer deux directives de portée isolées à un élément et vous pouvez rencontrer des problèmes d'imbrication/d'héritage/de traitement des événements. Surtout en ce qui concerne la transclusion - les effets peuvent ne pas être ceux que vous attendez.

Donc, cela échouerait:

 <p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"></p>

Et même si vous n'utilisez qu'une seule directive, vous remarquerez que ni les modèles de portée isolés ni les événements diffusés dans isolatedScopeDirective ne seront disponibles pour AnotherController. Cela étant triste, vous pouvez flex et utiliser la magie de transclusion pour le faire fonctionner - mais pour la plupart des cas d'utilisation, il n'est pas nécessaire d'isoler.

 <p isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"> <div ng-controller="AnotherController"> … the isolated scope is not available here, look: {{ isolatedModel }} </div> </p>

Alors, deux questions maintenant :

  1. Comment pouvez-vous traiter les modèles de portée parent dans une directive de même portée ?
  2. Comment pouvez-vous instancier de nouvelles valeurs de modèle ?

Il y a deux façons, dans les deux cas, vous transmettez des valeurs aux attributs. Considérez ce MainController :

 function MainController($interval) { this.foo = { bar: 1 }; this.baz = 1; var that = this; $interval(function () { that.foo.bar++; }, 144); $interval(function () { that.baz++; }, 144); this.quux = [1,2,3]; }

Qui contrôle cette vue :

 <body ng-controller="MainController as MC"> <div class="cyan-surface"> <h1>Attributes test</h1> <test-directive watch-attribute="MC.foo" observe-attribute="current index: {{ MC.baz }}"></test-directive> </div> </body>

Notez que "watch-attribute" n'est pas interpolé. Tout fonctionne, grâce à la magie JS. Voici la définition de la directive :

 function testDirective() { var postLink = function (scope, element, attrs) { scope.$watch(attrs.watchAttribute, function (newVal) { if (newVal) { // take a look in the console // we can't use the attribute directly console.log(attrs.watchAttribute); // the newVal is evaluated, and it can be used scope.modifiedFooBar = newVal.bar * 10; } }, true); attrs.$observe('observeAttribute', function (newVal) { scope.observed = newVal; }); }; return { link: postLink, templateUrl: '/attributes-demo/test-directive.html' }; }

Notez que attrs.watchAttribute est passé dans scope.$watch() sans les guillemets ! Cela signifie que ce qui a été réellement passé à $watch était la chaîne MC.foo ! Cela fonctionne, cependant, car toute chaîne passée dans $watch() est évaluée par rapport à la portée et MC.foo est disponible sur la portée. C'est également la façon la plus courante de surveiller les attributs dans les directives principales d'AngularJS.

Voir le code sur github pour le modèle, et regardez dans $parse et $eval pour encore plus de génialité.

Erreur courante n° 12 : ne pas nettoyer après vous-même : observateurs, intervalles, délais d'attente et variables

AngularJS fait une partie du travail en votre nom, mais pas tout. Les éléments suivants doivent être nettoyés manuellement :

  • Tous les observateurs qui ne sont pas liés à la portée actuelle (par exemple, liés à $rootScope)
  • Intervalles
  • Délais d'attente
  • Variables référençant DOM dans les directives
  • Plugins jQuery douteux, par exemple ceux qui n'ont pas de gestionnaires réagissant à l'événement JavaScript $destroy

Si vous ne le faites pas manuellement, vous rencontrerez un comportement inattendu et des fuites de mémoire. Pire encore - ceux-ci ne seront pas visibles instantanément, mais ils finiront par remonter. La loi de Murphy.

Étonnamment, AngularJS fournit des moyens pratiques de gérer tout cela :

 function cleanMeUp($interval, $rootScope, $timeout) { var postLink = function (scope, element, attrs) { var rootModelListener = $rootScope.$watch('someModel', function () { // do something }); var myInterval = $interval(function () { // do something in intervals }, 2584); var myTimeout = $timeout(function () { // defer some action here }, 1597); scope.domElement = element; $timeout(function () { // calling $destroy manually for testing purposes scope.$destroy(); }, 987); // here is where the cleanup happens scope.$on('$destroy', function () { // disable the listener rootModelListener(); // cancel the interval and timeout $interval.cancel(myInterval); $timeout.cancel(myTimeout); // nullify the DOM-bound model scope.domElement = null; }); element.on('$destroy', function () { // this is a jQuery event // clean up all vanilla JavaScript / jQuery artifacts here // respectful jQuery plugins have $destroy handlers, // that is the reason why this event is emitted... // follow the standards. }); };

Remarquez l'événement jQuery $destroy . Il s'appelle comme celui d'AngularJS, mais il est géré séparément. Scope $watchers ne réagira pas à l'événement jQuery.

Erreur courante n° 13 : conserver trop d'observateurs

Cela devrait être assez simple maintenant. Il y a une chose à comprendre ici : $digest() . Pour chaque liaison {{ model }} , AngularJS crée un observateur. À chaque phase de résumé, chacune de ces liaisons est évaluée et comparée à la valeur précédente. C'est ce qu'on appelle le sale-checking, et c'est ce que fait $digest. Si la valeur a changé depuis la dernière vérification, le rappel de l'observateur est déclenché. If that watcher callback modifies a model ($scope variable), a new $digest cycle is fired (up to a maximum of 10) when an exception is thrown.

Browsers don't have problems even with thousands of bindings, unless the expressions are complex. The common answer for “how many watchers are ok to have” is 2000.

So, how can we limit the number of watchers? By not watching scope models when we don't expect them to change. It is fairly easy onwards from AngularJS 1.3, since one-time bindings are in core now.

 <li ng-repeat="item in ::vastArray">{{ ::item.velocity }}</li>

After vastArray and item.velocity are evaluated once, they will never change again. You can still apply filters to the array, they will work just fine. It is just that the array itself will not be evaluated. In many cases, that is a win.

Common Mistake #14: Misunderstanding The Digest

This AngularJS error was already partly covered in mistakes 9.b and in 13. This is a more thorough explanation. AngularJS updates DOM as a result of callback functions to watchers. Every binding, that is the directive {{ someModel }} sets up watchers, but watchers are also set for many other directives like ng-if and ng-repeat . Just take a look at the source code, it is very readable. Watchers can also be set manually, and you have probably done that at least a few times yourself.

$watch() ers are bound to scopes. $Watchers can take strings, which are evaluated against the scope that the $watch() was bound to. They can also evaluate functions. And they also take callbacks. So, when $rootScope.$digest() is called, all the registered models (that is $scope variables) are evaluated and compared against their previous values. If the values don't match, the callback to the $watch() is executed.

It is important to understand that even though a model's value was changed, the callback does not fire until the next digest phase. It is called a “phase” for a reason - it can consist of several digest cycles. If only a watcher changes a scope model, another digest cycle is executed.

But $digest() is not polled for . It is called from core directives, services, methods, etc. If you change a model from a custom function that does not call .$apply , .$applyAsync , .$evalAsync , or anything else that eventually calls $digest() , the bindings will not be updated.

By the way, the source code for $digest() is actually quite complex. It is nevertheless worth reading, as the hilarious warnings make up for it.

Common Mistake #15: Not Relying On Automation, Or Relying On It Too Much

If you follow the trends within front end development and are a bit lazy - like me - then you probably try to not do everything by hand. Keeping track of all your dependencies, processing sets of files in different ways, reloading the browser after every file save - there is a lot more to developing than just coding.

So you may be using bower, and maybe npm depending on how you serve your app. There is a chance that you may be using grunt, gulp, or brunch. Or bash, which also is cool. In fact, you may have started your latest project with some Yeoman generator!

This leads to the question: do you understand the whole process of what your infrastructure really does? Do you need what you have, especially if you just spent hours trying to fix your connect webserver livereload functionality?

Take a second to assess what you need. All those tools are only here to aid you, there is no other reward for using them. The more experienced developers I talk to tend to simplify things.

Common Mistake #16: Not Running The Unit Tests In TDD Mode

Tests will not make your code free of AngularJS error messages. What they will do is assure that your team doesn't run into regression issues all the time.

I am writing specifically about unit tests here, not because I feel they are more important than e2e tests, but because they execute much faster. I must admit that the process I am about to describe is a very pleasurable one.

Test Driven Development as an implementation for eg gulp-karma runner, basically runs all your unit tests on every file save. My favorite way to write tests is, I just write empty assurances first:

 describe('some module', function () { it('should call the name-it service…', function () { // leave this empty for now }); ... });

After that, I write or refactor the actual code, then I come back to the tests and fill in the assurances with actual test code.

Having a TDD task running in a terminal speeds up the process by about 100%. Unit tests execute in a matter of a few seconds, even if you have a lot of them. Just save the test file and the runner will pick it up, evaluate your tests, and provide feedback instantly.

With e2e tests, the process is much slower. My advice - split e2e tests up into test suites and just run one at a time. Protractor has support for them, and below is the code I use for my test tasks (I like gulp).

 'use strict'; var gulp = require('gulp'); var args = require('yargs').argv; var browserSync = require('browser-sync'); var karma = require('gulp-karma'); var protractor = require('gulp-protractor').protractor; var webdriverUpdate = require('gulp-protractor').webdriver_update; function test() { // Be sure to return the stream // NOTE: Using the fake './foobar' so as to run the files // listed in karma.conf.js INSTEAD of what was passed to // gulp.src ! return gulp.src('./foobar') .pipe(karma({ configFile: 'test/karma.conf.js', action: 'run' })) .on('error', function(err) { // Make sure failed tests cause gulp to exit non-zero // console.log(err); this.emit('end'); //instead of erroring the stream, end it }); } function tdd() { return gulp.src('./foobar') .pipe(karma({ configFile: 'test/karma.conf.js', action: 'start' })) .on('error', function(err) { // Make sure failed tests cause gulp to exit non-zero // console.log(err); // this.emit('end'); // not ending the stream here }); } function runProtractor () { var argument = args.suite || 'all'; // NOTE: Using the fake './foobar' so as to run the files // listed in protractor.conf.js, instead of what was passed to // gulp.src return gulp.src('./foobar') .pipe(protractor({ configFile: 'test/protractor.conf.js', args: ['--suite', argument] })) .on('error', function (err) { // Make sure failed tests cause gulp to exit non-zero throw err; }) .on('end', function () { // Close browser sync server browserSync.exit(); }); } gulp.task('tdd', tdd); gulp.task('test', test); gulp.task('test-e2e', ['webdriver-update'], runProtractor); gulp.task('webdriver-update', webdriverUpdate);

Common Mistake #17: Not Using The Available Tools

A - chrome breakpoints

Chrome dev tools allow you to point at a specific place in any of the files loaded into the browser, pause code execution at that point, and let you interact with all the variables available from that point. That is a lot! That functionality does not require you to add any code at all, everything happens in the dev tools.

Not only you get access to all the variables, you also see the call stack, print stack traces, and more. You can even configure it to work with minified files. Read about it here.

There are other ways you can get similar run-time access, eg by adding console.log() calls. But breakpoints are more sophisticated.

AngularJS also allows you to access scope through DOM elements (as long as debugInfo is enabled), and inject available services through the console. Consider the following in the console:

 $(document.body).scope().$root

or point at an element in the inspector, and then:

 $($0).scope()

Even if debugInfo is not enabled, you can do:

 angular.reloadWithDebugInfo()

And have it available after reload:

To inject and interact with a service from the console, try:

 var injector = $(document.body).injector(); var someService = injector.get('someService');

B - chrome timeline

Another great tool that comes with dev tools is the timeline. That will allow you to record and analyse your app's live performance as you are using it. The output shows, among others, memory usage, frame rate, and the dissection of the different processes that occupy the CPU: loading, scripting, rendering, and painting.

If you experience that your app's performance degrades, you will most likely be able to find the cause for that through the timeline tab. Just record your actions which led to performance issues and see what happens. Too many watchers? You will see yellow bars taking a lot of space. Memory leaks? You can see how much memory was consumed over time on a graph.

A detailed description: https://developer.chrome.com/devtools/docs/timeline

C - inspecting apps remotely on iOS and Android

If you are developing a hybrid app or a responsive web app, you can access your device's console, DOM tree, and all other tools available either through Chrome or Safari dev tools. That includes the WebView and UIWebView.

Tout d'abord, démarrez votre serveur Web sur l'hôte 0.0.0.0 afin qu'il soit accessible depuis votre réseau local. Activer l'inspecteur Web dans les paramètres. Connectez ensuite votre appareil à votre bureau et accédez à votre page de développement local, en utilisant l'adresse IP de votre machine au lieu du "localhost" habituel. C'est tout ce qu'il faut, votre appareil devrait maintenant être disponible depuis le navigateur de votre bureau.

Voici les instructions détaillées pour Android Et pour iOS, des guides non officiels se trouvent facilement via google.

J'ai récemment eu une expérience intéressante avec browserSync. Cela fonctionne de la même manière que livereload, mais il synchronise également tous les navigateurs qui affichent la même page via browserSync. Cela inclut l'interaction de l'utilisateur comme le défilement, le clic sur les boutons, etc. Je regardais la sortie du journal de l'application iOS tout en contrôlant la page sur l'iPad depuis mon bureau. Cela a bien fonctionné !

Erreur courante #18 : ne pas lire le code source sur l'exemple NG-INIT

Ng-init , d'après le son, devrait être similaire à ng-if et ng-repeat , n'est-ce pas ? Vous êtes-vous déjà demandé pourquoi il y a un commentaire dans la documentation indiquant qu'il ne devrait pas être utilisé ? IMHO c'était surprenant! Je m'attendrais à ce que la directive initialise un modèle. C'est aussi ce qu'il fait, mais… il est implémenté d'une manière différente, c'est-à-dire qu'il ne surveille pas la valeur de l'attribut. Vous n'avez pas besoin de parcourir le code source d'AngularJS - laissez-moi vous l'apporter :

 var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });

Moins que ce à quoi vous vous attendiez ? Assez lisible, en plus de la syntaxe de directive maladroite, n'est-ce pas ? La sixième ligne est ce dont il s'agit.

Comparez-le à ng-show :

 var ngShowDirective = ['$animate', function($animate) { return { restrict: 'A', multiElement: true, link: function(scope, element, attr) { scope.$watch(attr.ngShow, function ngShowWatchAction(value) { // we're adding a temporary, animation-specific class for ng-hide since this way // we can control when the element is actually displayed on screen without having // to have a global/greedy CSS selector that breaks when other animations are run. // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { tempClasses: NG_HIDE_IN_PROGRESS_CLASS }); }); } }; }];

Encore une fois, la sixième ligne. Il y a une $watch là-bas, c'est ce qui rend cette directive dynamique. Dans le code source AngularJS, une grande partie de tout le code sont des commentaires qui décrivent le code qui était principalement lisible depuis le début. Je pense que c'est un excellent moyen d'en savoir plus sur AngularJS.

Conclusion

Ce guide couvrant les erreurs AngularJS les plus courantes est presque deux fois plus long que les autres guides. Cela s'est avéré ainsi naturellement. La demande d'ingénieurs front-end JavaScript de haute qualité est très élevée. AngularJS est si chaud en ce moment , et il occupe une position stable parmi les outils de développement les plus populaires depuis quelques années. Avec AngularJS 2.0 en route, il dominera probablement pour les années à venir.

Ce qui est génial avec le développement front-end, c'est qu'il est très gratifiant. Notre travail est visible instantanément et les gens interagissent directement avec les produits que nous livrons. Le temps passé à apprendre JavaScript, et je pense que nous devrions nous concentrer sur le langage JavaScript, est un très bon investissement. C'est le langage d'Internet. La concurrence est super forte ! Il y a un objectif pour nous - l'expérience utilisateur. Pour réussir, nous devons tout couvrir.

Le code source utilisé dans ces exemples peut être téléchargé depuis GitHub. N'hésitez pas à le télécharger et à vous l'approprier.

Je voulais donner des crédits aux quatre développeurs d'édition qui m'ont le plus inspiré :

  • Ben Nadel
  • Devise de Todd
  • Pascal Précht
  • Panda des sables

Je voulais également remercier toutes les personnes formidables sur les chaînes FreeNode #angularjs et #javascript pour de nombreuses excellentes conversations et un soutien continu.

Et enfin, souvenez-vous toujours :

 // when in doubt, comment it out! :)