I 18 errori più comuni commessi dagli sviluppatori di AngularJS

Pubblicato: 2022-03-11

Le app a pagina singola richiedono agli sviluppatori front-end di diventare migliori ingegneri del software. CSS e HTML non sono più la preoccupazione maggiore, infatti, non c'è più una sola preoccupazione. Lo sviluppatore front-end deve gestire XHR, logica dell'applicazione (modelli, viste, controller), prestazioni, animazioni, stili, struttura, SEO e integrazione con servizi esterni. Il risultato che emerge da tutti quelli combinati è la User Experience (UX) che dovrebbe essere sempre prioritaria.

AngularJS è un framework molto potente. È il terzo repository più stellato su GitHub. Non è difficile iniziare a utilizzare, ma gli obiettivi che si intende raggiungere richiedono comprensione. Gli sviluppatori AngularJS non possono più ignorare il consumo di memoria, perché non verrà più ripristinato durante la navigazione. Questa è l'avanguardia dello sviluppo web. Abbracciamolo!

Errori comuni di AngularJS

Errore comune n. 1: accedere all'ambito tramite il DOM

Ci sono alcune modifiche di ottimizzazione consigliate per la produzione. Uno di questi è disabilitare le informazioni di debug.

DebugInfoEnabled è un'impostazione che per impostazione predefinita è true e consente l'accesso all'ambito tramite i nodi DOM. Se vuoi provarlo tramite la console JavaScript, seleziona un elemento DOM e accedi al suo ambito con:

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

Può essere utile anche quando non si utilizza jQuery con i suoi CSS, ma non dovrebbe essere utilizzato al di fuori della console. Il motivo è che quando $compileProvider.debugInfoEnabled è impostato su false, la chiamata .scope() su un nodo DOM restituirà undefined .

Questa è una delle poche opzioni consigliate per la produzione.

Tieni presente che puoi comunque accedere all'oscilloscopio tramite la console, anche quando è in produzione. Chiama angular.reloadWithDebugInfo() dalla console e l'app farà proprio questo.

Errore comune n. 2: non avere un punto lì dentro

Probabilmente hai letto che se non avevi un punto nel tuo ng-model , lo stavi sbagliando. Quando si tratta di eredità, tale affermazione è spesso vera. Gli ambiti hanno un modello di ereditarietà prototipo, tipico di JavaScript, e gli ambiti nidificati sono comuni ad AngularJS. Molte direttive creano ambiti figlio come ngRepeat , ngIf e ngController . Quando si risolve un modello, la ricerca inizia nell'ambito corrente e passa attraverso ogni ambito padre, fino a $rootScope .

Ma, quando si imposta un nuovo valore, ciò che accade dipende dal tipo di modello (variabile) che si desidera modificare. Se il modello è una primitiva, l'ambito figlio creerà semplicemente un nuovo modello. Ma se la modifica riguarda una proprietà di un oggetto del modello, la ricerca negli ambiti padre troverà l'oggetto di riferimento e ne cambierà la proprietà effettiva. Non verrebbe impostato un nuovo modello nell'ambito attuale, quindi non si verificherebbe alcun mascheramento:

 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>

Facendo clic sul pulsante "Imposta primitiva" imposterà foo nell'ambito interno su 2, ma non cambierà foo nell'ambito esterno.

Facendo clic sul pulsante "Cambia oggetto" cambierà la proprietà della barra dall'ambito padre. Poiché non ci sono variabili nell'ambito interno, non si verificherà alcuna ombreggiatura e il valore visibile per la barra sarà 3 in entrambi gli ambiti.

Un altro modo per farlo è sfruttare il fatto che gli ambiti padre e l'ambito radice sono referenziati da ogni ambito. Gli oggetti $parent e $root possono essere utilizzati per accedere rispettivamente all'ambito padre e $rootScope direttamente dalla vista. Potrebbe essere un modo potente, ma non ne sono un fan a causa del problema con il targeting di un particolare ambito a monte del flusso. Esiste un altro modo per impostare e accedere alle proprietà specifiche di un ambito, utilizzando la sintassi controllerAs .

Errore comune n. 3: non utilizzare la sintassi controllerAs

Il modo alternativo e più efficiente per assegnare modelli per utilizzare un oggetto controller invece dell'ambito $ iniettato. Invece di iniettare scope, possiamo definire modelli come questo:

 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>

Questo è molto meno confuso. Soprattutto quando sono presenti molti ambiti nidificati, come può essere il caso degli stati nidificati.

C'è di più nella sintassi del controller.

Errore comune n. 4: non utilizzare completamente la sintassi controllerAs

Ci sono alcuni avvertimenti su come viene esposto l'oggetto controller. È fondamentalmente un oggetto impostato nell'ambito del controller, proprio come un modello normale.

Se è necessario guardare una proprietà dell'oggetto controller, è possibile guardare una funzione ma non è necessario. Ecco un esempio:

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

È più facile fare solo:

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

Ciò significa che anche lungo la catena dell'ambito, puoi accedere a MC da un controller figlio:

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

Tuttavia, per poterlo fare devi essere coerente con l'acronimo che usi per controllerA. Ci sono almeno tre modi per impostarlo. Hai già visto il primo:

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

Tuttavia, se usi ui-router , specificare un controller in questo modo è soggetto a errori. Per gli stati, i controller devono essere specificati nella configurazione dello stato:

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

C'è un altro modo per annotare:

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

Puoi fare lo stesso nelle direttive:

 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);

Anche l'altro modo di annotare è valido, sebbene meno conciso:

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

Errore comune n. 5: non utilizzare viste con nome con UI-ROUTER per la potenza"

La soluzione di routing de facto per AngularJS è stata, fino ad ora, ui-router . Rimosso dal core qualche tempo fa, il modulo ngRoute era troppo semplice per un routing più sofisticato.

C'è un nuovo NgRouter in arrivo, ma gli autori lo considerano ancora troppo presto per la produzione. Quando scrivo questo, Angular stabile è 1.3.15 e ui-router oscilla.

I motivi principali:

  • nidificazione di stato impressionante
  • astrazione del percorso
  • parametri opzionali e obbligatori

Qui tratterò la nidificazione degli stati per evitare errori di AngularJS.

Pensa a questo come a un caso d'uso complesso ma standard. C'è un'app, che ha una visualizzazione della home page e una visualizzazione del prodotto. La visualizzazione del prodotto ha tre sezioni separate: l'introduzione, il widget e il contenuto. Vogliamo che il widget persista e non si ricarichi quando si passa da uno stato all'altro. Ma il contenuto dovrebbe essere ricaricato.

Considera la seguente struttura della pagina dell'indice del prodotto HTML:

 <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>

Questo è qualcosa che potremmo ottenere dal codificatore HTML e ora dobbiamo separarlo in file e stati. In genere vado con la convenzione che esiste uno stato MAIN astratto, che mantiene i dati globali se necessario. Usalo invece di $rootScope. Lo stato Main manterrà anche l'HTML statico richiesto in ogni pagina. Mantengo index.html pulito.

 <!— 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>

Quindi vediamo la pagina dell'indice del prodotto:

 <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>

Come puoi vedere, la pagina dell'indice del prodotto ha tre viste con nome. Uno per l'introduzione, uno per il widget e uno per il prodotto. Incontriamo le specifiche! Quindi ora impostiamo il routing:

 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);

Questo sarebbe il primo approccio. Ora, cosa succede quando si passa da main.product.index main.product.details ? Il contenuto e il widget vengono ricaricati, ma vogliamo solo ricaricare il contenuto. Questo era problematico e gli sviluppatori hanno effettivamente creato router che avrebbero supportato proprio quella funzionalità. Uno dei nomi per questo era visualizzazioni appiccicose . Fortunatamente, ui-router lo supporta immediatamente con il targeting per visualizzazione con nome assoluto .

 // 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' } } });

Spostando la definizione dello stato nella vista genitore, anch'essa astratta, possiamo evitare che la vista figlio venga ricaricata quando si cambiano gli URL che normalmente interessano i fratelli di quel bambino. Naturalmente, il widget potrebbe essere una semplice direttiva. Ma il punto è che potrebbe anche essere un altro stato nidificato complesso.

C'è un altro modo per farlo attraverso l'uso di $urlRouterProvider.deferIntercept() , ma penso che usare la configurazione dello stato sia effettivamente migliore. Se sei interessato ad intercettare le rotte, ho scritto un piccolo tutorial su StackOverflow.

Errore comune n. 6: dichiarare tutto nel mondo angolare utilizzando funzioni anonime

Questo errore è di calibro più leggero ed è più una questione di stile che evitare i messaggi di errore di AngularJS. Potresti aver notato in precedenza che raramente passo funzioni anonime alle dichiarazioni di angular internal. Di solito definisco prima una funzione e poi la passo.

Questo non riguarda solo le funzioni. Ho avuto questo approccio leggendo le guide di stile, in particolare di Airbnb e Todd Motto. Credo che ci siano diversi vantaggi e quasi nessun inconveniente.

Prima di tutto, puoi manipolare e mutare le tue funzioni e oggetti molto più facilmente se sono assegnati a una variabile. In secondo luogo, il codice è più pulito e può essere facilmente suddiviso in file. Ciò significa manutenibilità. Se non vuoi inquinare lo spazio dei nomi globale, avvolgi ogni file in IIFE. Il terzo motivo è la testabilità. Considera questo esempio:

 '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);

Quindi ora potremmo prendere in giro publicMethod1 , ma perché dovremmo farlo poiché è esposto? Non sarebbe più semplice spiare il metodo esistente? Tuttavia, il metodo è in realtà un'altra funzione: un involucro sottile. Dai un'occhiata a questo approccio:

 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; }

Non si tratta solo di stile, poiché in effetti il ​​codice è più riutilizzabile e idiomatico. Lo sviluppatore ottiene più potere espressivo. Dividere tutto il codice in blocchi autonomi rende tutto più semplice.

Errore comune n. 7: eseguire elaborazioni pesanti in AKA angolare utilizzando i lavoratori

In alcuni scenari, potrebbe essere necessario elaborare una vasta gamma di oggetti complessi facendoli passare attraverso una serie di filtri, decoratori e infine un algoritmo di ordinamento. Un caso d'uso è quando l'app dovrebbe funzionare offline o dove le prestazioni di visualizzazione dei dati sono fondamentali. E poiché JavaScript è a thread singolo, è relativamente facile bloccare il browser.

È anche facile evitarlo con i web worker. Non sembrano esserci librerie popolari che lo gestiscono specificamente per AngularJS. Potrebbe essere per il meglio però, dal momento che l'implementazione è facile.

Per prima cosa, configuriamo il servizio:

 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);

Ora il lavoratore:

 '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);

Ora, inietta il servizio come al solito e tratta scoringService.scoreItems() come faresti con qualsiasi metodo di servizio che restituisce una promessa. L'elaborazione pesante verrà eseguita su un thread separato e non verrà fatto alcun danno all'UX.

A cosa prestare attenzione:

  • non sembra esserci una regola generale per quanti lavoratori deporre le uova. Alcuni sviluppatori affermano che 8 è un buon numero, ma usa un calcolatore online e adatta a te
  • verifica la compatibilità con i browser meno recenti
  • Ho riscontrato un problema durante il passaggio del numero 0 dal servizio al lavoratore. Ho applicato .toString() sulla proprietà passata e ha funzionato correttamente.

Errore comune n. 8: l'uso eccessivo e l'incomprensione si risolve

Le risoluzioni aggiungono tempo extra al caricamento della vista. Credo che le prestazioni elevate dell'app front-end siano il nostro obiettivo principale. Non dovrebbe essere un problema eseguire il rendering di alcune parti della vista mentre l'app attende i dati dall'API.

Considera questa configurazione:

 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) }); }

L'output della console sarà:

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

Il che sostanzialmente significa che:

  • Le risoluzioni vengono eseguite in modo asincrono
  • Non possiamo fare affidamento su un ordine di esecuzione (o almeno dobbiamo flettere un po')
  • Tutti gli stati sono bloccati finché tutte le risoluzioni non fanno il loro dovere, anche se non sono astratte.

Ciò significa che prima che l'utente veda qualsiasi output, deve attendere tutte le dipendenze. Dobbiamo avere quei dati, certo, ok. Se è assolutamente necessario averlo prima della visualizzazione, inserirlo in un blocco .run() . In caso contrario, è sufficiente effettuare la chiamata al servizio dal controller e gestire lo stato di caricamento parziale con grazia. Vedere il lavoro in corso - e il controller è già eseguito, quindi in realtà è in corso - è meglio che avere l'app in stallo.

Errore comune n. 9: non ottimizzare l'app: tre esempi

a) Causare troppi loop digest, ad esempio attaccare i cursori ai modelli

Questo è un problema generale che può causare errori di AngularJS, ma ne parlerò nell'esempio degli slider. Stavo usando questa libreria di slider, slider di intervallo angolare, perché avevo bisogno della funzionalità estesa. Quella direttiva ha questa sintassi nella versione minima:

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

Considera il seguente codice nel controller:

 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'); } } });

Quindi funziona lentamente. La soluzione casuale sarebbe quella di impostare un timeout sull'input. Ma ciò non è sempre utile e, a volte, non vogliamo davvero ritardare l'effettiva modifica del modello in tutti i casi.

Quindi aggiungeremo un modello temporaneo destinato a modificare il modello di lavoro in timeout:

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

e nel controller:

 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) Non si utilizza $applyAsync

AngularJS non ha un meccanismo di polling per chiamare $digest() . Viene eseguito solo perché utilizziamo le direttive (ad esempio ng-click , input ), services ( $timeout , $http ) e metodi ( $watch ) che valutano il nostro codice e in seguito chiamano un digest.

Quello che fa .$applyAsync() è ritardare la risoluzione delle espressioni fino al prossimo ciclo $digest() , che viene attivato dopo un timeout 0, che in realtà è ~10ms.

Ci sono due modi per usare adesso applyAsync . Un modo automatizzato per le richieste $http e un modo manuale per il resto.

Per fare in modo che tutte le richieste http che ritornano nello stesso momento vengano risolte in un digest, procedi nel seguente modo:

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

Il modo manuale mostra come funziona effettivamente. Considera alcune funzioni che vengono eseguite sul callback a un listener di eventi JS vanilla o a jQuery .click() o qualche altra libreria esterna. Dopo aver eseguito e modificato i modelli, se non l'hai già inserito in $apply() devi chiamare $scope.$root.$digest() ( $rootScope.$digest() ), o almeno $scope.$digest() . In caso contrario, non vedrai alcun cambiamento.

Se lo fai più volte in un flusso, potrebbe iniziare a funzionare lentamente. Considera invece di chiamare $scope.$applyAsync() sulle espressioni. Imposterà solo un ciclo di digest per tutti loro.

c) Elaborazione pesante delle immagini

Se riscontri prestazioni scadenti, puoi indagare sul motivo utilizzando la sequenza temporale degli Strumenti per sviluppatori di Chrome. Scriverò di più su questo strumento nell'errore n. 17. Se il grafico della sequenza temporale è dominato dal colore verde dopo la registrazione, i problemi di prestazioni potrebbero essere correlati all'elaborazione delle immagini. Questo non è strettamente correlato ad AngularJS, ma può verificarsi in aggiunta a problemi di prestazioni di AngularJS (che sarebbero per lo più gialli sul grafico). Come ingegneri front-end, dobbiamo pensare al progetto finale completo.

Prenditi un momento per valutare:

  • Usi il parallasse?
  • Hai diversi livelli di contenuto sovrapposti?
  • Muovi le tue immagini?
  • Ridimensioni le immagini (ad es. con le dimensioni dello sfondo)?
  • Ridimensioni le immagini in loop e forse causi loop di digest durante il ridimensionamento?

Se hai risposto "sì" ad almeno tre dei precedenti, considera di allentarlo. Forse puoi servire immagini di varie dimensioni e non ridimensionarle affatto. Forse potresti aggiungere l'hacking dell'elaborazione della GPU "transform: translateZ(0)". Oppure usa requestAnimationFrame per i gestori.

Errore comune n. 10: jQuerying It - Detached DOM Tree

Molte volte probabilmente sentirai che non è consigliabile utilizzare jQuery con AngularJS e che dovrebbe essere evitato. È imperativo capire il motivo di queste affermazioni. Ci sono almeno tre ragioni, per quanto posso vedere, ma nessuna di esse è un vero e proprio bloccante.

Motivo 1: quando esegui il codice jQuery, devi chiamare tu stesso $digest() . Per molti casi, esiste una soluzione AngularJS che è su misura per AngularJS e può essere di uso migliore all'interno di Angular rispetto a jQuery (ad esempio ng-click o il sistema di eventi).

Motivo 2: il metodo di pensiero sulla creazione dell'app. Se hai aggiunto JavaScript ai siti Web, che si ricaricano durante la navigazione, non devi preoccuparti troppo del consumo di memoria. Con le app a pagina singola, devi preoccuparti. Se non pulisci, gli utenti che trascorrono più di pochi minuti sulla tua app potrebbero riscontrare crescenti problemi di prestazioni.

Motivo 3: pulire non è in realtà la cosa più facile da fare e analizzare. Non c'è modo di chiamare un Garbage Collector dallo script (nel browser). Potresti finire con alberi DOM staccati. Ho creato un esempio (jQuery è caricato in 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);

Questa è una semplice direttiva che restituisce del testo. C'è un pulsante sotto di esso, che distruggerà semplicemente la direttiva manualmente.

Quindi, quando la direttiva viene rimossa, rimane un riferimento all'albero DOM in scope.toBeDetached. Negli strumenti di sviluppo di Chrome, se accedi alla scheda "profili" e quindi "scatta un'istantanea dell'heap", vedrai nell'output:

Puoi vivere con pochi, ma è brutto se ne hai un sacco. Soprattutto se per qualche motivo, come nell'esempio, lo memorizzi nell'oscilloscopio. L'intero DOM sarà valutato su ogni digest. L'albero DOM distaccato problematico è quello con 4 nodi. Quindi come può essere risolto?

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

L'albero DOM separato con 4 voci è stato rimosso!

In questo esempio, la direttiva utilizza lo stesso ambito e memorizza l'elemento DOM nell'ambito. È stato più facile per me dimostrarlo in quel modo. Non va sempre così male, poiché potresti memorizzarlo in una variabile. Tuttavia, occuperebbe comunque memoria se una chiusura che aveva fatto riferimento a quella variabile oa qualsiasi altra dallo stesso ambito di funzione sopravvivesse.

Errore comune n. 11: uso eccessivo dell'ambito isolato

Ogni volta che hai bisogno di una direttiva che sai verrà utilizzata in un unico posto o che non ti aspetti sia in conflitto con l'ambiente in cui viene utilizzata, non è necessario utilizzare l'ambito isolato. Ultimamente, c'è una tendenza a creare componenti riutilizzabili, ma sapevi che le direttive angolari di base non utilizzano affatto l'ambito isolato?

Esistono due ragioni principali: non è possibile applicare due direttive di ambito isolate a un elemento e potrebbero verificarsi problemi con nidificazione/ereditarietà/elaborazione degli eventi. Soprattutto per quanto riguarda la trasclusione: gli effetti potrebbero non essere quelli che ti aspetti.

Quindi questo fallirebbe:

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

E anche se si utilizza una sola direttiva, si noterà che né i modelli di ambito isolati né gli eventi trasmessi in isolatedScopeDirective non saranno disponibili per AnotherController. Purtroppo, puoi flettere e usare la magia di trasclusione per farlo funzionare, ma per la maggior parte dei casi d'uso non è necessario isolare.

 <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>

Quindi, due domande ora:

  1. In che modo è possibile elaborare i modelli di ambito padre in una direttiva con lo stesso ambito?
  2. Come puoi creare un'istanza di nuovi valori di modello?

Ci sono due modi, in entrambi si passano i valori agli attributi. Considera questo 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]; }

Che controlla questa vista:

 <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>

Si noti che "watch-attribute" non è interpolato. Funziona tutto, grazie alla magia di JS. Ecco la definizione della direttiva:

 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' }; }

Si noti che attrs.watchAttribute viene passato in scope.$watch() senza le virgolette! Ciò significa che ciò che è stato effettivamente passato a $watch era la stringa MC.foo ! Funziona, tuttavia, perché qualsiasi stringa passata in $watch() viene valutata rispetto all'ambito e MC.foo è disponibile nell'ambito. Questo è anche il modo più comune in cui gli attributi vengono osservati nelle direttive principali di AngularJS.

Guarda il codice su github per il modello e guarda in $parse e $eval per ancora più suggestione.

Errore comune n. 12: non ripulire da soli: osservatori, intervalli, timeout e variabili

AngularJS fa del lavoro per tuo conto, ma non solo. I seguenti elementi devono essere ripuliti manualmente:

  • Tutti i watcher che non sono legati allo scope corrente (ad es. legati a $rootScope)
  • Intervalli
  • Timeout
  • Variabili che fanno riferimento a DOM nelle direttive
  • Plugin jQuery ingannevoli, ad esempio quelli che non hanno gestori che reagiscono all'evento JavaScript $destroy

Se non lo fai manualmente, riscontrerai comportamenti imprevisti e perdite di memoria. Ancora peggio: questi non saranno immediatamente visibili, ma alla fine si insinueranno. Legge di Murphy.

Sorprendentemente, AngularJS offre modi pratici per gestire tutti questi:

 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. }); };

Nota l'evento jQuery $destroy . Si chiama come quello di AngularJS, ma viene gestito separatamente. Gli osservatori di ambito $ non reagiranno all'evento jQuery.

Errore comune n. 13: mantenere troppi osservatori

Questo dovrebbe essere abbastanza semplice ora. C'è una cosa da capire qui: $digest() . Per ogni associazione {{ model }} , AngularJS crea un watcher. In ogni fase di digestione, ciascuno di tali legami viene valutato e confrontato con il valore precedente. Questo si chiama controllo sporco, ed è quello che fa $digest. Se il valore è cambiato dall'ultimo controllo, viene attivata la richiamata del watcher. 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.

Innanzitutto, avvia il tuo server web sull'host 0.0.0.0 in modo che sia accessibile dalla tua rete locale. Abilita l'ispettore web nelle impostazioni. Quindi collega il tuo dispositivo al desktop e accedi alla tua pagina di sviluppo locale, utilizzando l'ip della tua macchina invece del normale "localhost". Questo è tutto ciò che serve, il tuo dispositivo ora dovrebbe essere disponibile per te dal browser del tuo desktop.

Ecco le istruzioni dettagliate per Android E per iOS le guide non ufficiali si trovano facilmente tramite google.

Di recente ho avuto una bella esperienza con browserSync. Funziona in modo simile al livereload, ma sincronizza anche tutti i browser che stanno visualizzando la stessa pagina tramite browserSync. Ciò include l'interazione dell'utente come lo scorrimento, il clic sui pulsanti, ecc. Stavo guardando l'output del registro dell'app iOS mentre controllavo la pagina sull'iPad dal mio desktop. Ha funzionato bene!

Errore comune n. 18: non leggere il codice sorgente nell'esempio NG-INIT

Ng-init , dal suono, dovrebbe essere simile a ng-if e ng-repeat , giusto? Ti sei mai chiesto perché c'è un commento nei documenti che non dovrebbe essere usato? IMHO è stato sorprendente! Mi aspetto che la direttiva inizializzi un modello. Questo è anche quello che fa, ma... è implementato in un modo diverso, cioè non controlla il valore dell'attributo. Non è necessario sfogliare il codice sorgente di AngularJS - lascia che te lo porti:

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

Meno di quanto ti aspetteresti? Abbastanza leggibile, a parte la scomoda sintassi della direttiva, vero? La sesta riga è di cosa si tratta.

Confrontalo con 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 }); }); } }; }];

Di nuovo, la sesta riga. C'è un $watch lì, questo è ciò che rende dinamica questa direttiva. Nel codice sorgente di AngularJS, gran parte di tutto il codice sono commenti che descrivono codice che era per lo più leggibile dall'inizio. Credo che sia un ottimo modo per conoscere AngularJS.

Conclusione

Questa guida che copre gli errori più comuni di AngularJS è lunga quasi il doppio delle altre guide. Si è rivelato così naturalmente. La richiesta di ingegneri front-end JavaScript di alta qualità è molto alta. AngularJS è così caldo in questo momento e da alcuni anni mantiene una posizione stabile tra gli strumenti di sviluppo più popolari. Con AngularJS 2.0 in arrivo, probabilmente dominerà per gli anni a venire.

La cosa grandiosa dello sviluppo front-end è che è molto gratificante. Il nostro lavoro è immediatamente visibile e le persone interagiscono direttamente con i prodotti che consegniamo. Il tempo dedicato all'apprendimento di JavaScript, e credo che dovremmo concentrarci sul linguaggio JavaScript, è un ottimo investimento. È la lingua di Internet. La concorrenza è super forte! C'è un obiettivo per noi: l'esperienza dell'utente. Per avere successo, dobbiamo coprire tutto.

Il codice sorgente utilizzato in questi esempi può essere scaricato da GitHub. Sentiti libero di scaricarlo e personalizzarlo.

Volevo dare crediti a quattro sviluppatori editoriali che mi hanno ispirato di più:

  • Ben Nadel
  • Motto Todd
  • Pascal Precht
  • Panda Sandeep

Volevo anche ringraziare tutte le persone fantastiche sui canali #angularjs e #javascript di FreeNode per le molte conversazioni eccellenti e il supporto continuo.

E infine, ricorda sempre:

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