Top 18 cele mai frecvente greșeli pe care le fac dezvoltatorii AngularJS
Publicat: 2022-03-11Aplicațiile cu o singură pagină cer dezvoltatorilor front-end să devină ingineri software mai buni. CSS și HTML nu mai sunt cea mai mare preocupare, de fapt, nu mai există o singură preocupare. Dezvoltatorul front-end trebuie să se ocupe de XHR, logica aplicației (modele, vizualizări, controlere), performanță, animații, stiluri, structură, SEO și integrarea cu servicii externe. Rezultatul care reiese din toate cele combinate este Experiența utilizatorului (UX) care ar trebui întotdeauna prioritizată.
AngularJS este un cadru foarte puternic. Este al treilea cel mai marcat depozit de pe GitHub. Nu este dificil să începeți să utilizați, dar obiectivele pe care este destinat să le atingă necesită înțelegere. Dezvoltatorii AngularJS nu mai pot ignora consumul de memorie, deoarece nu se va mai reseta la navigare. Aceasta este avangarda dezvoltării web. Să-l îmbrățișăm!
Greșeala comună #1: Accesarea domeniului de aplicare prin DOM
Există câteva ajustări de optimizare recomandate pentru producție. Una dintre ele este dezactivarea informațiilor de depanare.
DebugInfoEnabled
este o setare care este implicit la true și permite accesul la sfera prin nodurile DOM. Dacă doriți să încercați acest lucru prin consola JavaScript, selectați un element DOM și accesați domeniul său cu:
angular.element(document.body).scope()
Poate fi util chiar și atunci când nu utilizați jQuery cu CSS-ul său, dar nu ar trebui să fie folosit în afara consolei. Motivul este că atunci când $compileProvider.debugInfoEnabled
este setat la false, apelarea .scope()
pe un nod DOM va returna undefined
.
Aceasta este una dintre puținele opțiuni recomandate pentru producție.
Vă rugăm să rețineți că puteți accesa în continuare domeniul prin consolă, chiar și atunci când sunteți în producție. Apelați angular.reloadWithDebugInfo()
din consolă și aplicația va face exact asta.
Greșeala comună nr. 2: Nu aveți un punct acolo
Probabil ați citit că, dacă nu aveați un punct în modelul dvs. ng , ați procedat greșit. Când se referă la moștenire, această afirmație este adesea adevărată. Scopurile au un model prototip de moștenire, tipic pentru JavaScript, iar domeniile imbricate sunt comune pentru AngularJS. Multe directive creează domenii copil, cum ar fi ngRepeat
, ngIf
și ngController
. La rezolvarea unui model, căutarea începe pe domeniul curent și trece prin fiecare domeniu părinte, până la $rootScope
.
Dar, la stabilirea unei noi valori, ceea ce se întâmplă depinde de ce fel de model (variabilă) vrem să schimbăm. Dacă modelul este un primitiv, domeniul copil va crea doar un nou model. Dar dacă modificarea se referă la o proprietate a unui obiect model, căutarea în domeniile părinte va găsi obiectul la care se face referire și va modifica proprietatea actuală. Un nou model nu ar fi setat pe domeniul actual, deci nu ar avea loc nicio mascare:
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>
Făcând clic pe butonul etichetat „Set primitive” va seta foo în domeniul interior la 2, dar nu va schimba foo în domeniul exterior.
Făcând clic pe butonul etichetat „Schimbare obiect” se va schimba proprietatea barei din domeniul părinte. Deoarece nu există nicio variabilă în domeniul interior, nu se va produce umbrire, iar valoarea vizibilă pentru bar va fi 3 în ambele domenii.
O altă modalitate de a face acest lucru este să valorificați faptul că domeniile părinte și domeniul rădăcină sunt referite din fiecare domeniu. Obiectele $parent
și $root
pot fi folosite pentru a accesa domeniul părinte și, respectiv, $rootScope
, direct din vizualizare. Poate fi o modalitate puternică, dar nu sunt un fan al acesteia din cauza problemei cu vizarea unui anumit domeniu de aplicare în flux. Există o altă modalitate de a seta și de a accesa proprietăți specifice unui domeniu - folosind sintaxa controllerAs
.
Greșeala comună #3: Nu se utilizează sintaxa controllerAs
Modul alternativ și cel mai eficient de a atribui modele pentru a utiliza un obiect controler în loc de $scope injectat. În loc să injectăm domeniul de aplicare, putem defini modele ca acesta:
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>
Acest lucru este mult mai puțin confuz. Mai ales când există multe domenii imbricate, așa cum poate fi cazul stărilor imbricate.
Există mai mult la sintaxa controllerAs.
Greșeala comună #4: Nu se utilizează pe deplin sintaxa controller-ului
Există câteva avertismente cu privire la modul în care este expus obiectul controlerului. Este practic un obiect setat pe domeniul de aplicare al controlerului, la fel ca un model normal.
Dacă trebuie să urmăriți o proprietate a obiectului controler, puteți urmări o funcție, dar nu vi se cere. Iată un exemplu:
function MainController($scope) { this.title = 'Some title'; $scope.$watch(angular.bind(this, function () { return this.title; }), function (newVal, oldVal) { // handle changes }); }
Este mai ușor să faci doar:
function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }
Înseamnă că, de asemenea, în lanțul domeniului de aplicare, puteți accesa MC de la un controler copil:
function NestedController($scope) { if ($scope.MC && $scope.MC.title === 'Some title') { $scope.MC.title = 'New title'; } }
Cu toate acestea, pentru a putea face acest lucru, trebuie să fiți în concordanță cu acronimul pe care îl utilizați pentru controllerAs. Există cel puțin trei moduri de a-l seta. L-ai vazut deja pe primul:
<div ng-controller="MainController as MC"> … </div>
Cu toate acestea, dacă utilizați ui-router
, specificarea unui controler în acest fel este predispusă la erori. Pentru state, controlerele trebuie specificate în configurația stării:
angular.module('myApp', []) .config(function ($stateProvider) { $stateProvider .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/path/to/template.html' }) }). controller('MainController', function () { … });
Există o altă modalitate de adnotare:
(…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })
Puteți face același lucru în directive:
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);
Celălalt mod de adnotare este și el valid, deși mai puțin concis:
function testForToptal() { return { controller: 'AnotherController', controllerAs: 'AC', template: '<p>{{ AC.text }}</p>' }; }
Greșeala comună #5: Nu folosiți vizualizările numite cu UI-ROUTER pentru putere”
Soluția de rutare de facto pentru AngularJS a fost, până acum, ui-router
. Eliminat din nucleu cu ceva timp în urmă, modulul ngRoute, era prea de bază pentru o rutare mai sofisticată.
Există un nou NgRouter
pe drum, dar autorii încă îl consideră prea devreme pentru producție. Când scriu asta, unghiul stabil este 1.3.15, iar ui-router
este rupt.
Principalele motive:
- cuibărit de stat minunat
- abstracția traseului
- parametrii opționali și necesari
Aici voi acoperi imbricarea stărilor pentru a evita erorile AngularJS.
Gândiți-vă la acesta ca la un caz de utilizare complex, dar standard. Există o aplicație, care are o vizualizare a paginii de pornire și o vizualizare a produsului. Vizualizarea produsului are trei secțiuni separate: introducerea, widgetul și conținutul. Dorim ca widgetul să persiste și să nu se reîncarce atunci când comutăm între stări. Dar conținutul ar trebui să se reîncarce.
Luați în considerare următoarea structură a paginii de index al produsului 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>
Acesta este ceva ce am putea obține de la codificatorul HTML și acum trebuie să-l separăm în fișiere și stări. În general, merg cu convenția că există o stare MAIN abstractă, care păstrează datele globale dacă este necesar. Folosiți asta în loc de $rootScope. Starea principală va păstra, de asemenea, HTML static care este necesar pe fiecare pagină. Păstrez index.html curat.
<!— 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>
Apoi, să vedem pagina de index al produsului:
<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>
După cum puteți vedea, pagina de index al produsului are trei vizualizări denumite. Unul pentru introducere, unul pentru widget și unul pentru produs. Ne îndeplinim specificațiile! Deci, acum să setăm rutarea:
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);
Asta ar fi prima abordare. Acum, ce se întâmplă când comutați între main.product.index
și main.product.details
? Conținutul și widgetul sunt reîncărcate, dar vrem doar să reîncărcăm conținutul. Acest lucru a fost problematic, iar dezvoltatorii au creat de fapt routere care să suporte tocmai această funcționalitate. Unul dintre denumirile pentru acest lucru a fost sticky views . Din fericire, ui-router
acceptă acest lucru imediat, cu direcționarea absolută a vizualizării cu nume .
// 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' } } });
Mutând definiția stării în vizualizarea părinte, care este, de asemenea, abstractă, putem păstra vizualizarea copil de la reîncărcare atunci când comutăm adresele URL care afectează în mod normal frații acelui copil. Desigur, widget-ul ar putea fi o directivă simplă. Dar ideea este că ar putea fi și o altă stare complexă imbricată.
Există o altă modalitate de a face acest lucru prin utilizarea $urlRouterProvider.deferIntercept()
, dar cred că utilizarea configurației stării este de fapt mai bună. Dacă ești interesat de interceptarea rutelor, am scris un mic tutorial despre StackOverflow.
Greșeala comună #6: Declararea totul în lumea unghiulară folosind funcții anonime
Această greșeală este de un calibru mai ușor și este mai mult o chestiune de stil decât evitarea mesajelor de eroare AngularJS. Poate ați observat anterior că rareori trec funcții anonime declarațiilor unghiulare interne. De obicei, definesc mai întâi o funcție și apoi o transmit.
Aceasta se referă mai mult decât la funcții. Am obținut această abordare citind ghiduri de stil, în special Airbnb și Todd Motto. Cred că există mai multe avantaje și aproape niciun dezavantaj.
În primul rând, vă puteți manipula și modifica funcțiile și obiectele mult mai ușor dacă sunt alocate unei variabile. În al doilea rând, codul este mai curat și poate fi împărțit cu ușurință în fișiere. Asta înseamnă mentenanță. Dacă nu doriți să poluați spațiul de nume global, împachetați fiecare fișier în IIFE-uri. Al treilea motiv este testabilitatea. Luați în considerare acest exemplu:
'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);
Deci acum am putea bate joc de publicMethod1
, dar de ce ar trebui să facem asta, deoarece este expus? Nu ar fi mai ușor să spionezi metoda existentă? Cu toate acestea, metoda este de fapt o altă funcție - un înveliș subțire. Aruncă o privire la această abordare:
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; }
Nu este vorba doar despre stil, deoarece, de fapt, codul este mai reutilizabil și idiomatic. Dezvoltatorul capătă mai multă putere de expresie. Împărțirea întregului cod în blocuri autonome ușurează.
Greșeala comună #7: Efectuarea procesării grele în Angular AKA Folosind lucrători
În unele scenarii, poate fi necesară procesarea unei game largi de obiecte complexe, trecându-le printr-un set de filtre, decoratori și, în final, un algoritm de sortare. Un caz de utilizare este atunci când aplicația ar trebui să funcționeze offline sau în care performanța de afișare a datelor este esențială. Și deoarece JavaScript are un singur thread, este relativ ușor să înghețați browserul.
Este, de asemenea, ușor de evitat cu lucrătorii web. Nu pare să existe biblioteci populare care să se ocupe de asta special pentru AngularJS. Ar putea fi totuși cel mai bine, deoarece implementarea este ușoară.
Mai întâi, să setăm serviciul:
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);
Acum, muncitorul:
'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);
Acum, injectați serviciul ca de obicei și tratați scoringService.scoreItems()
ca orice metodă de serviciu care returnează o promisiune. Prelucrarea grea va fi efectuată pe un fir separat și nu va fi adus niciun rău UX.
La ce să fii atent:
- nu pare să existe o regulă generală pentru câți lucrători să depună icre. Unii dezvoltatori susțin că 8 este un număr bun, dar folosește un calculator online și potriviți-vă
- verificați compatibilitatea cu browserele mai vechi
- Am o problemă când trec numărul 0 de la serviciu către muncitor. Am aplicat
.toString()
pe proprietatea transmisă și a funcționat corect.
Greșeala obișnuită #8: Rezolvarea utilizării excesive și a neînțelegerii
Rezolvă adăugarea de timp suplimentar la încărcarea vizualizării. Cred că performanța ridicată a aplicației front-end este scopul nostru principal. Nu ar trebui să fie o problemă să redați unele părți ale vizualizării în timp ce aplicația așteaptă datele din API.
Luați în considerare această configurație:
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) }); }
Ieșirea consolei va fi:
Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed
Ceea ce înseamnă practic că:
- Rezolvările sunt executate asincron
- Nu ne putem baza pe un ordin de execuție (sau cel puțin trebuie să ne flexăm destul de mult)
- Toate statele sunt blocate până când toate rezoluțiile își fac treaba, chiar dacă nu sunt abstracte.
Aceasta înseamnă că înainte ca utilizatorul să vadă orice ieșire, el/ea trebuie să aștepte toate dependențele. Trebuie să avem acele date, sigur, bine. Dacă este absolut necesar să îl aveți înainte de vizualizare, puneți-l într-un bloc .run()
. În caz contrar, apelați serviciul de la controler și gestionați cu grație starea pe jumătate încărcată. A vedea lucrul în desfășurare - și controlerul este deja executat, deci este de fapt un progres - este mai bine decât să blochezi aplicația.
Greșeala comună #9: Nu optimizarea aplicației - Trei exemple
a) Cauză prea multe bucle de digerare, cum ar fi atașarea glisoarelor la modele
Aceasta este o problemă generală care poate duce la erori AngularJS, dar o voi discuta la exemplul glisoarelor. Folosisem această bibliotecă de glisoare, slider unghiular, pentru că aveam nevoie de funcționalitatea extinsă. Directiva respectivă are această sintaxă în versiunea minimă:
<body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.price" > </div> </body>
Luați în considerare următorul cod în controler:
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'); } } });
Deci funcționează lent. Soluția obișnuită ar fi să setați un timeout pentru intrare. Dar acest lucru nu este întotdeauna la îndemână și, uneori, nu vrem cu adevărat să amânăm schimbarea efectivă a modelului în toate cazurile.
Deci vom adăuga un model temporar care va schimba modelul de lucru la expirarea timpului:
<body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.priceTemporary" > </div> </body>
și în controler:
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) Nu se utilizează $applyAsync
AngularJS nu are un mecanism de sondare pentru a apela $digest()
. Este executat doar pentru că folosim directivele (de exemplu ng-click
, input
), serviciile ( $timeout
, $http
) și metodele ( $watch
) care ne evaluează codul și apoi apelează un digest.

Ceea ce .$applyAsync()
este că întârzie rezoluția expresiilor până la următorul ciclu $digest()
, care este declanșat după un timeout 0, care este de fapt ~10ms.
Există două moduri de a utiliza applyAsync
acum. O modalitate automată pentru cererile $http
și o modalitate manuală pentru restul.
Pentru ca toate solicitările http care revin în aproximativ același timp să se rezolve într-un singur rezumat, faceți:
mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });
Modul manual arată cum funcționează de fapt. Luați în considerare o funcție care rulează pe apel invers la un ascultător de evenimente vanilla JS sau un jQuery .click()
sau o altă bibliotecă externă. După ce se execută și modifică modelele, dacă nu l-ați împachetat deja într-un $apply()
, trebuie să apelați $scope.$root.$digest()
( $rootScope.$digest()
), sau cel puțin $scope.$digest()
. În caz contrar, nu veți vedea nicio schimbare.
Dacă faceți asta de mai multe ori într-un singur flux, ar putea începe să funcționeze lent. Luați în considerare apelarea $scope.$applyAsync()
pe expresii. Va seta un singur ciclu de apelare pentru toate.
c) Efectuarea procesării grele a imaginilor
Dacă întâmpinați performanțe proaste, puteți investiga motivul utilizând Cronologia din Chrome Developer Tools. Voi scrie mai multe despre acest instrument în greșeala #17. Dacă graficul cronologic este dominat de culoarea verde după înregistrare, problemele de performanță pot fi legate de procesarea imaginilor. Acest lucru nu este strict legat de AngularJS, dar se poate întâmpla pe lângă problemele de performanță AngularJS (care ar fi în mare parte galben pe grafic). Ca ingineri front-end, trebuie să ne gândim la proiectul final complet.
Luați un moment pentru a evalua:
- Folosești paralaxa?
- Aveți mai multe straturi de conținut care se suprapun unul pe altul?
- Îți muți imaginile?
- Scalați imaginile (de exemplu, cu dimensiunea fundalului)?
- Redimensionați imaginile în bucle și, probabil, provocați bucle de digerare la redimensionare?
Dacă ați răspuns „da” la cel puțin trei dintre cele de mai sus, luați în considerare reducerea acestuia. Poate că puteți servi imagini de diferite dimensiuni și nu redimensionați deloc. Poate ați putea adăuga hack-ul de procesare GPU „transform: translateZ(0)”. Sau utilizați requestAnimationFrame pentru handler.
Greșeala comună #10: jQuerying It - Arborele DOM detașat
De multe ori probabil auziți că nu este recomandat să utilizați jQuery cu AngularJS și că ar trebui evitat. Este imperativ să înțelegeți motivul din spatele acestor afirmații. Există cel puțin trei motive, din câte văd, dar niciunul dintre ele nu este blocatoare efective.
Motivul 1: Când executați codul jQuery, trebuie să apelați singur $digest()
. Pentru multe cazuri, există o soluție AngularJS care este adaptată pentru AngularJS și poate fi mai bună în interiorul Angular decât jQuery (de exemplu, ng-click sau sistemul de evenimente).
Motivul 2: Metoda de gândire despre construirea aplicației. Dacă ați adăugat JavaScript pe site-uri web, care se reîncarcă atunci când navigați, nu trebuie să vă faceți griji prea mult cu privire la consumul de memorie. Cu aplicațiile cu o singură pagină, trebuie să vă faceți griji. Dacă nu curățați, utilizatorii care petrec mai mult de câteva minute pe aplicația dvs. pot întâmpina probleme de performanță tot mai mari.
Motivul 3: Curățarea nu este de fapt cel mai ușor lucru de făcut și analizat. Nu există nicio modalitate de a apela un colector de gunoi din script (în browser). Este posibil să ajungeți cu arbori DOM detașați. Am creat un exemplu (jQuery este încărcat în 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);
Aceasta este o directivă simplă care scoate un text. Există un buton sub el, care va distruge direct directiva manual.
Deci, atunci când directiva este eliminată, rămâne o referință la arborele DOM în scope.toBeDetached. În instrumentele de dezvoltare Chrome, dacă accesați fila „profiluri” și apoi „faceți instantaneu heap”, veți vedea în rezultat:
Poți trăi cu câțiva, dar este rău dacă ai o tonă. Mai ales dacă dintr-un motiv oarecare, ca în exemplu, îl stocați pe lunetă. Întregul DOM va fi evaluat la fiecare rezumat. Arborele DOM detașat problematic este cel cu 4 noduri. Deci, cum poate fi rezolvat acest lucru?
scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });
Arborele DOM detașat cu 4 intrări este eliminat!
În acest exemplu, directiva folosește același domeniu de aplicare și stochează elementul DOM în domeniu. Mi-a fost mai ușor să demonstrez așa. Nu devine întotdeauna atât de rău, deoarece l-ați putea stoca într-o variabilă. Cu toate acestea, ar ocupa în continuare memorie dacă ar exista vreo închidere care a făcut referire la acea variabilă sau oricare alta din același domeniu de activitate.
Greșeala comună #11: Folosirea excesivă a domeniului izolat
Ori de câte ori aveți nevoie de o directivă despre care știți că va fi folosită într-un singur loc sau despre care nu vă așteptați să intre în conflict cu orice mediu în care este utilizată, nu este nevoie să utilizați un domeniu izolat. În ultimul timp, există o tendință de a crea componente reutilizabile, dar știați că directivele unghiulare de bază nu folosesc deloc un domeniu izolat?
Există două motive principale: nu puteți aplica două directive de domeniu izolate unui element și puteți întâmpina probleme cu procesarea imbricare/moștenire/eveniment. Mai ales în ceea ce privește transcluzia - efectele pot să nu fie cele așteptate.
Deci asta ar eșua:
<p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"></p>
Și chiar dacă utilizați o singură directivă, veți observa că nici modelele de domeniu izolat și nici evenimentele difuzate în isolatedScopeDirective nu vor fi disponibile pentru AnotherController. Acest lucru fiind trist, puteți să flexați și să utilizați magia transcluziei pentru a o face să funcționeze - dar pentru majoritatea cazurilor de utilizare, nu este nevoie să vă izolați.
<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>
Deci, acum două întrebări:
- Cum puteți procesa modele de domeniu părinte într-o directivă cu același domeniu de aplicare?
- Cum puteți instanția noi valori de model?
Există două moduri, în ambele transmiți valori la atribute. Luați în considerare acest 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]; }
Asta controlează această vizualizare:
<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>
Observați că „atributul ceas” nu este interpolat. Totul funcționează, datorită magiei JS. Iată definiția directivei:
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' }; }
Observați că attrs.watchAttribute
este trecut în scope.$watch()
fără ghilimele! Asta înseamnă că ceea ce a fost de fapt transmis către $watch a fost șirul MC.foo
! Funcționează, totuși, deoarece orice șir trecut în $watch()
este evaluat în raport cu domeniul de aplicare, iar MC.foo
este disponibil în domeniu. Acesta este, de asemenea, cel mai comun mod în care atributele sunt urmărite în directivele de bază AngularJS.
Vedeți codul de pe github pentru șablon și căutați în $parse
și $eval
pentru și mai mult.
Greșeala comună nr. 12: Nu faceți curățenie după dvs. - Observatori, intervale, intervale de timp și variabile
AngularJS lucrează în numele tău, dar nu pe toate. Următoarele trebuie curățate manual:
- Orice observatori care nu sunt legați la domeniul curent (de exemplu, legați la $rootScope)
- Intervale
- Timeouts
- Variabile care fac referire la DOM în directive
- Pluginuri jQuery îndoielnice, de exemplu cele care nu au handlere care reacţionează la evenimentul JavaScript
$destroy
Dacă nu faci asta manual, vei întâlni un comportament neașteptat și pierderi de memorie. Și mai rău - acestea nu vor fi vizibile instantaneu, dar se vor strecura în cele din urmă. Legea lui Murphy.
În mod uimitor, AngularJS oferă modalități la îndemână de a trata toate acestea:
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. }); };
Observați evenimentul jQuery $destroy
. Se numește ca AngularJS, dar este tratat separat. Scope $watchers nu va reacționa la evenimentul jQuery.
Greșeala comună #13: Păstrarea prea mulți observatori
Acest lucru ar trebui să fie destul de simplu acum. Există un lucru de înțeles aici: $digest()
. Pentru fiecare legare {{ model }}
, AngularJS creează un observator. În fiecare fază de digerare, fiecare astfel de legare este evaluată și comparată cu valoarea anterioară. Asta se numește verificare murdară și asta face $digest. Dacă valoarea s-a schimbat de la ultima verificare, apelul înapoi este declanșat. 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.
Mai întâi, porniți serverul dvs. web pe gazda 0.0.0.0, astfel încât să fie accesibil din rețeaua locală. Activați inspectorul web în setări. Apoi conectați-vă dispozitivul la desktop și accesați pagina de dezvoltare locală, folosind ip-ul mașinii în loc de „localhost” obișnuit. Asta este tot ce este nevoie, dispozitivul dvs. ar trebui să vă fie acum disponibil din browserul desktopului.
Iată instrucțiunile detaliate pentru Android, iar pentru iOS, ghidurile neoficiale pot fi găsite cu ușurință prin google.
Am avut recent o experiență grozavă cu browserSync. Funcționează într-un mod similar cu livereload, dar, de asemenea, sincronizează de fapt toate browserele care vizualizează aceeași pagină prin browserSync. Aceasta include interacțiunea cu utilizatorul, cum ar fi derularea, clicul pe butoane etc. Mă uitam la ieșirea jurnalului aplicației iOS în timp ce controlam pagina de pe iPad de pe desktop. A funcționat frumos!
Greșeala comună #18: Nu citiți codul sursă în exemplul NG-INIT
Ng-init
, după sunetul acestuia, ar trebui să fie similar cu ng-if
și ng-repeat
, nu? Te-ai întrebat vreodată de ce există un comentariu în documente că nu ar trebui să fie folosit? IMHO, a fost surprinzător! M-aș aștepta ca directiva să inițializeze un model. Și asta face, dar... este implementat într-un mod diferit, adică nu urmărește valoarea atributului. Nu trebuie să răsfoiți codul sursă AngularJS - permiteți-mi să vi-l aduc:
var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });
Mai puțin decât v-ați aștepta? Destul de lizibil, pe lângă sintaxa incomodă a directivei, nu-i așa? A șasea linie este despre ce este vorba.
Comparați-l cu 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 }); }); } }; }];
Din nou, a șasea linie. Există un $watch
acolo, acesta este ceea ce face această directivă dinamică. În codul sursă AngularJS, o mare parte a întregului cod sunt comentarii care descriu cod care a fost în mare parte lizibil de la început. Cred că este o modalitate excelentă de a învăța despre AngularJS.
Concluzie
Acest ghid care acoperă cele mai comune greșeli AngularJS este aproape de două ori mai lung decât celelalte ghiduri. Așa a ieșit firesc. Cererea de ingineri front-end JavaScript de înaltă calitate este foarte mare. AngularJS este atât de fierbinte acum și de câțiva ani deține o poziție stabilă printre cele mai populare instrumente de dezvoltare. Cu AngularJS 2.0 pe drum, probabil că va domina în anii următori.
Ceea ce este grozav la dezvoltarea front-end este că este foarte plină de satisfacții. Munca noastră este vizibilă instantaneu, iar oamenii interacționează direct cu produsele pe care le livrăm. Timpul petrecut învățând JavaScript, și cred că ar trebui să ne concentrăm pe limbajul JavaScript, este o investiție foarte bună. Este limbajul internetului. Competiția este super puternică! Există un singur accent pentru noi - experiența utilizatorului. Pentru a avea succes, trebuie să acoperim totul.
Codul sursă folosit în aceste exemple poate fi descărcat de pe GitHub. Simțiți-vă liber să îl descărcați și să îl faceți al dvs.
Am vrut să acord credite celor patru dezvoltatori de editori care m-au inspirat cel mai mult:
- Ben Nadel
- Todd Motto
- Pascal Precht
- Sandeep Panda
De asemenea, am vrut să mulțumesc tuturor oamenilor grozavi de pe canalele FreeNode #angularjs și #javascript pentru multe conversații excelente și pentru sprijinul continuu.
Și, în sfârșit, amintiți-vă întotdeauna:
// when in doubt, comment it out! :)