18 najczęstszych błędów popełnianych przez programistów AngularJS
Opublikowany: 2022-03-11Aplikacje jednostronicowe wymagają od programistów front-end, aby stali się lepszymi inżynierami oprogramowania. CSS i HTML nie są już największym problemem, w rzeczywistości nie ma już jednego problemu. Programista front-end musi obsługiwać XHR, logikę aplikacji (modele, widoki, kontrolery), wydajność, animacje, style, strukturę, SEO oraz integrację z usługami zewnętrznymi. Rezultatem, który wyłania się z tych wszystkich kombinacji, jest doświadczenie użytkownika (UX), które zawsze powinno być traktowane priorytetowo.
AngularJS to bardzo potężny framework. Jest to trzecie pod względem popularności repozytorium na GitHub. Nie jest trudno zacząć używać, ale cele, które ma osiągnąć, wymagają zrozumienia. Deweloperzy AngularJS nie mogą już ignorować zużycia pamięci, ponieważ nie będzie się już resetować podczas nawigacji. To jest awangarda tworzenia stron internetowych. Przyjmijmy to!
Powszechny błąd nr 1: Dostęp do zakresu przez DOM
W przypadku produkcji zalecanych jest kilka poprawek optymalizacji. Jednym z nich jest wyłączenie informacji debugowania.
DebugInfoEnabled
to ustawienie, które domyślnie ma wartość true i umożliwia dostęp do zakresu przez węzły DOM. Jeśli chcesz spróbować tego za pomocą konsoli JavaScript, wybierz element DOM i uzyskaj dostęp do jego zakresu za pomocą:
angular.element(document.body).scope()
Może być przydatny nawet wtedy, gdy nie używasz jQuery z jego CSS, ale nie powinien być używany poza konsolą. Powodem jest to, że gdy $compileProvider.debugInfoEnabled
ma wartość false, wywołanie .scope()
w węźle DOM zwróci undefined
.
To jedna z niewielu zalecanych opcji produkcji.
Pamiętaj, że nadal możesz uzyskać dostęp do lunety za pośrednictwem konsoli, nawet podczas produkcji. Wywołaj angular.reloadWithDebugInfo()
z konsoli, a aplikacja to zrobi.
Powszechny błąd nr 2: brak kropki w środku
Prawdopodobnie czytałeś, że jeśli nie miałeś kropki w swoim modelu ng , robiłeś to źle. Jeśli chodzi o dziedziczenie, to stwierdzenie często jest prawdziwe. Zakresy mają prototypowy model dziedziczenia, typowy dla JavaScript, a zakresy zagnieżdżone są wspólne dla AngularJS. Wiele dyrektyw tworzy zakresy podrzędne, takie jak ngRepeat
, ngIf
i ngController
. Podczas rozwiązywania modelu wyszukiwanie rozpoczyna się od bieżącego zakresu i przechodzi przez każdy zakres nadrzędny, aż do $rootScope
.
Ale przy ustalaniu nowej wartości to, co się stanie, zależy od tego, jaki model (zmienną) chcemy zmienić. Jeśli model jest prymitywem, zakres podrzędny po prostu utworzy nowy model. Ale jeśli zmiana dotyczy właściwości obiektu modelowego, wyszukiwanie w zakresach nadrzędnych znajdzie obiekt, do którego się odwołuje, i zmieni jego rzeczywistą właściwość. Nowy model nie zostałby ustawiony na bieżącym zakresie, więc nie wystąpiłoby maskowanie:
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>
Kliknięcie przycisku oznaczonego „Ustaw prymityw” ustawi foo w zakresie wewnętrznym na 2, ale nie zmieni foo w zakresie zewnętrznym.
Kliknięcie przycisku oznaczonego „Zmień obiekt” spowoduje zmianę właściwości paska z zakresu nadrzędnego. Ponieważ nie ma zmiennej w zakresie wewnętrznym, cieniowanie nie wystąpi, a widoczna wartość słupka będzie wynosić 3 w obu zakresach.
Innym sposobem na to jest wykorzystanie faktu, że zakresy nadrzędne i zakres główny są przywoływane z każdego zakresu. Obiekty $parent
i $root
mogą być użyte do uzyskania dostępu odpowiednio do zakresu nadrzędnego i $rootScope
bezpośrednio z widoku. Może to potężny sposób, ale nie jestem jego fanem ze względu na problem z celowaniem w konkretny zakres w górę strumienia. Istnieje inny sposób ustawiania i uzyskiwania dostępu do właściwości specyficznych dla zakresu — przy użyciu składni controllerAs
.
Powszechny błąd nr 3: nie używanie kontrolera jako składni
Alternatywny i najbardziej wydajny sposób przypisywania modeli do używania obiektu kontrolera zamiast wstrzykniętego $scope. Zamiast wstrzykiwać zakres, możemy zdefiniować modele w następujący sposób:
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>
To znacznie mniej mylące. Zwłaszcza, gdy istnieje wiele zakresów zagnieżdżonych, co może mieć miejsce w przypadku stanów zagnieżdżonych.
W składni controllerAs jest więcej.
Częsty błąd nr 4: Nie w pełni wykorzystując składnię kontrolera
Istnieje kilka zastrzeżeń dotyczących sposobu ujawniania obiektu kontrolera. Jest to w zasadzie obiekt ustawiony w zasięgu kontrolera, tak jak normalny model.
Jeśli potrzebujesz obserwować właściwość obiektu kontrolera, możesz obserwować funkcję, ale nie jest to wymagane. Oto przykład:
function MainController($scope) { this.title = 'Some title'; $scope.$watch(angular.bind(this, function () { return this.title; }), function (newVal, oldVal) { // handle changes }); }
Łatwiej jest po prostu zrobić:
function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }
Oznacza to, że również w dół łańcucha zasięgu można uzyskać dostęp do MC z kontrolera podrzędnego:
function NestedController($scope) { if ($scope.MC && $scope.MC.title === 'Some title') { $scope.MC.title = 'New title'; } }
Jednak, aby móc to zrobić, musisz być spójny z akronimem używanym dla controllerAs. Są co najmniej trzy sposoby, aby to ustawić. Widziałeś już pierwszy:
<div ng-controller="MainController as MC"> … </div>
Jeśli jednak używasz ui-router
, określenie kontrolera w ten sposób jest podatne na błędy. Dla stanów kontrolery należy określić w konfiguracji stanów:
angular.module('myApp', []) .config(function ($stateProvider) { $stateProvider .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/path/to/template.html' }) }). controller('MainController', function () { … });
Jest inny sposób na adnotację:
(…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })
To samo możesz zrobić w dyrektywach:
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);
Inny sposób adnotacji jest również poprawny, choć mniej zwięzły:
function testForToptal() { return { controller: 'AnotherController', controllerAs: 'AC', template: '<p>{{ AC.text }}</p>' }; }
Powszechny błąd nr 5: nie używanie nazwanych widoków z UI-ROUTER do zasilania”
De facto rozwiązaniem routingu dla AngularJS był do tej pory ui-router
. Usunięty z jądra jakiś czas temu moduł ngRoute był zbyt prosty dla bardziej wyrafinowanego routingu.
Jest w drodze nowy NgRouter
, ale autorzy wciąż uważają, że jest za wcześnie na produkcję. Kiedy to piszę, stabilny Angular to 1.3.15, a ui-router
się kołysze.
Główne powody:
- niesamowite zagnieżdżanie stanu
- abstrakcja trasy
- parametry opcjonalne i wymagane
Tutaj omówię zagnieżdżanie stanów, aby uniknąć błędów AngularJS.
Pomyśl o tym jako o złożonym, ale standardowym przypadku użycia. Istnieje aplikacja, która ma widok strony głównej i widok produktu. Widok produktu składa się z trzech oddzielnych sekcji: wstępu, widżetu i treści. Chcemy, aby widżet utrzymywał się i nie ładował się ponownie podczas przełączania między stanami. Ale zawartość powinna się ponownie załadować.
Rozważ następującą strukturę strony indeksu produktów 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>
To jest coś, co moglibyśmy uzyskać z kodera HTML, a teraz musimy podzielić go na pliki i stany. Generalnie zgadzam się z konwencją, że istnieje abstrakcyjny stan GŁÓWNY, który w razie potrzeby przechowuje dane globalne. Użyj tego zamiast $rootScope. Stan Main zachowa również statyczny kod HTML, który jest wymagany na każdej stronie. Utrzymuję index.html w czystości.
<!— 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>
Następnie zobaczmy stronę indeksu produktów:
<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>
Jak widać, strona indeksu produktów ma trzy nazwane widoki. Jeden na wprowadzenie, jeden na widżet i jeden na produkt. Spełniamy specyfikacje! A teraz skonfigurujmy 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);
To byłoby pierwsze podejście. A teraz, co się dzieje, gdy przełączasz się między main.product.index
a main.product.details
? Treść i widżet zostaną ponownie załadowane, ale chcemy tylko ponownie załadować zawartość. Było to problematyczne, a programiści faktycznie stworzyli routery, które obsługiwały właśnie tę funkcjonalność. Jedną z nazw były lepkie widoki . Na szczęście ui-router
obsługuje to po wyjęciu z pudełka z bezwzględnym kierowaniem na nazwany widok .
// 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' } } });
Przenosząc definicję stanu do widoku rodzica, który również jest abstrakcyjny, możemy uchronić widok dziecka przed ponownym ładowaniem podczas przełączania adresów URL, co normalnie wpływa na rodzeństwo tego dziecka. Oczywiście widżet może być prostą dyrektywą. Ale chodzi o to, że może to być również inny złożony stan zagnieżdżony.
Jest inny sposób na zrobienie tego poprzez użycie $urlRouterProvider.deferIntercept()
, ale myślę, że użycie konfiguracji stanu jest faktycznie lepsze. Jeśli interesuje Cię przechwytywanie tras, napisałem mały samouczek na StackOverflow.
Powszechny błąd nr 6: deklarowanie wszystkiego w świecie Angular za pomocą funkcji anonimowych
Ten błąd jest lżejszego kalibru i jest bardziej kwestią stylu niż unikania komunikatów o błędach AngularJS. Być może zauważyłeś wcześniej, że rzadko przekazuję anonimowe funkcje do deklaracji Angular Internal. Zwykle najpierw definiuję funkcję, a potem ją przekazuję.
Dotyczy to nie tylko funkcji. Takie podejście zaczerpnąłem z poradników stylistycznych, zwłaszcza Airbnb i Todd Motto. Uważam, że ma to kilka zalet i prawie żadnych wad.
Przede wszystkim możesz znacznie łatwiej manipulować i mutować swoje funkcje i obiekty, jeśli są one przypisane do zmiennej. Po drugie, kod jest bardziej przejrzysty i można go łatwo podzielić na pliki. Oznacza to łatwość konserwacji. Jeśli nie chcesz zanieczyszczać globalnej przestrzeni nazw, opakuj każdy plik w IIFE. Trzeci powód to testowalność. Rozważ ten przykład:
'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);
Więc teraz moglibyśmy publicMethod1
, ale dlaczego mielibyśmy to robić, skoro jest ujawniona? Czy nie byłoby łatwiej po prostu szpiegować istniejącą metodę? Jednak metoda ta jest w rzeczywistości inną funkcją - cienkim opakowaniem. Spójrz na to podejście:
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; }
Nie chodzi tylko o styl, ponieważ w efekcie kod jest bardziej wielokrotnego użytku i idiomatyczny. Deweloper zyskuje większą moc wyrazu. Dzielenie całego kodu na samodzielne bloki tylko to ułatwia.
Powszechny błąd nr 7: Wykonywanie ciężkiego przetwarzania w Angular AKA przy użyciu pracowników
W niektórych scenariuszach może być wymagane przetworzenie dużej liczby złożonych obiektów poprzez przepuszczenie ich przez zestaw filtrów, dekoratorów i wreszcie algorytm sortowania. Jednym z przypadków użycia jest sytuacja, w której aplikacja powinna działać w trybie offline lub gdy wydajność wyświetlania danych jest kluczowa. A ponieważ JavaScript jest jednowątkowy, stosunkowo łatwo można zamrozić przeglądarkę.
Łatwo też tego uniknąć w przypadku pracowników internetowych. Wydaje się, że nie ma żadnych popularnych bibliotek, które obsługują to specjalnie dla AngularJS. To może być najlepsze, ponieważ implementacja jest łatwa.
Najpierw skonfigurujmy usługę:
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);
Teraz pracownik:
'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);
Teraz wstrzyknij usługę jak zwykle i potraktuj scoringService.scoreItems()
jak każdą metodę usługi, która zwraca obietnicę. Ciężkie przetwarzanie zostanie przeprowadzone w osobnym wątku i nie zostanie wyrządzona żadna szkoda UX.
Na co zwrócić uwagę:
- nie wydaje się, aby istniała ogólna reguła określająca, ilu robotników ma się odradzać. Niektórzy programiści twierdzą, że 8 to dobra liczba, ale skorzystaj z kalkulatora online i dopasuj się do siebie
- sprawdź kompatybilność ze starszymi przeglądarkami
- Mam problem przy przekazywaniu numeru 0 z serwisu do pracownika. Zastosowałem
.toString()
na przekazanej właściwości i zadziałało poprawnie.
Powszechny błąd nr 8: Nadużywanie i niezrozumienie rozwiązań
Rozstrzygnięcia dodają dodatkowy czas na wczytanie widoku. Uważam, że naszym głównym celem jest wysoka wydajność aplikacji frontendowej. Nie powinno być problemu z renderowaniem niektórych części widoku, gdy aplikacja czeka na dane z API.
Rozważ tę konfigurację:
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) }); }
Dane wyjściowe konsoli będą następujące:
Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed
Co w zasadzie oznacza, że:
- Rozwiązania są wykonywane asynchronicznie
- Nie możemy polegać na kolejności wykonania (a przynajmniej musimy trochę się wyginać)
- Wszystkie stany są blokowane, dopóki wszystkie postanowienia nie wykonają swoich zadań, nawet jeśli nie są abstrakcyjne.
Oznacza to, że zanim użytkownik zobaczy jakiekolwiek dane wyjściowe, musi poczekać na wszystkie zależności. Musimy mieć te dane, jasne, w porządku. Jeśli jest to absolutnie konieczne, aby mieć go przed widokiem, umieść go w bloku .run()
. W przeciwnym razie po prostu wykonaj wywołanie usługi z kontrolera i sprawnie obsłuż stan w połowie załadowany. Widzenie pracy w toku – a kontroler jest już wykonany, więc w rzeczywistości jest to postęp – jest lepsze niż zatrzymanie aplikacji.
Powszechny błąd nr 9: brak optymalizacji aplikacji — trzy przykłady
a) Wywoływanie zbyt wielu pętli skrótu, takich jak dołączanie suwaków do modeli
Jest to ogólny problem, który może skutkować błędami AngularJS, ale omówię go na przykładzie sliderów. Korzystałem z tej biblioteki suwaków, suwaka zakresu kątowego, ponieważ potrzebowałem rozszerzonej funkcjonalności. Ta dyrektywa ma następującą składnię w wersji minimalnej:
<body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.price" > </div> </body>
Rozważ następujący kod w kontrolerze:
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'); } } });
Więc to działa powoli. Zwykłym rozwiązaniem byłoby ustawienie limitu czasu na wejściu. Ale nie zawsze jest to przydatne i czasami tak naprawdę nie chcemy opóźniać faktycznej zmiany modelu we wszystkich przypadkach.
Dodamy więc tymczasowy model, który zmieni działający model po przekroczeniu limitu czasu:
<body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.priceTemporary" > </div> </body>
a w kontrolerze:
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) Nie używam $applyAsync
AngularJS nie ma mechanizmu odpytywania do wywołania $digest()
. Jest wykonywany tylko dlatego, że używamy dyrektyw (np ng-click
, input
), usług ( $timeout
, $http
) i metod ( $watch
), które oceniają nasz kod i następnie wywołują skrót.

To, co .$applyAsync()
, to opóźnianie rozwiązywania wyrażeń do następnego cyklu $digest()
, który jest wyzwalany po przekroczeniu limitu czasu 0, który w rzeczywistości wynosi ~10ms.
Istnieją dwa sposoby użycia applyAsync
teraz. Zautomatyzowany sposób na żądania $http
i ręczny sposób na resztę.
Aby wszystkie żądania http, które wracają mniej więcej w tym samym czasie, zostały rozwiązane w jednym podsumowaniu, wykonaj następujące czynności:
mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });
Sposób ręczny pokazuje, jak to faktycznie działa. Rozważmy jakąś funkcję, która działa w wywołaniu zwrotnym do waniliowego detektora zdarzeń JS lub jQuery .click()
, lub innej zewnętrznej biblioteki. Po wykonaniu i zmianie modeli, jeśli jeszcze nie opakowałeś go w $apply()
, musisz wywołać $scope.$root.$digest()
( $rootScope.$digest()
) lub przynajmniej $scope.$digest()
. W przeciwnym razie nie zobaczysz żadnych zmian.
Jeśli zrobisz to wiele razy w jednym przepływie, może zacząć działać wolno. Zamiast tego rozważ wywołanie $scope.$applyAsync()
na wyrażeniach. Ustawi tylko jeden cykl podsumowania dla nich wszystkich.
c) Ciężkie przetwarzanie obrazów
Jeśli zauważysz niską wydajność, możesz zbadać przyczynę, korzystając z osi czasu w Narzędziach dla programistów Chrome. Więcej o tym narzędziu napiszę w błędzie #17. Jeśli po nagraniu na wykresie osi czasu dominuje kolor zielony, problemy z wydajnością mogą być związane z przetwarzaniem obrazów. Nie jest to ściśle związane z AngularJS, ale może się zdarzyć na szczycie problemów z wydajnością AngularJS (które byłyby głównie żółte na wykresie). Jako inżynierowie front-endu musimy myśleć o całościowym projekcie końcowym.
Poświęć chwilę na ocenę:
- Używasz paralaksy?
- Czy masz kilka warstw treści nakładających się na siebie?
- Czy przesuwasz swoje obrazy?
- Czy skalujecie obrazy (np. z rozmiarem tła)?
- Czy zmieniasz rozmiar obrazów w pętlach i może powodujesz pętle skrótu przy zmianie rozmiaru?
Jeśli odpowiedziałeś „tak” na co najmniej trzy z powyższych, rozważ złagodzenie tego. Być może możesz obsługiwać różne rozmiary obrazów i wcale ich nie zmieniać. Może mógłbyś dodać „transform: translateZ(0)” wymusza hackowanie przetwarzania GPU. Lub użyj requestAnimationFrame dla obsługi.
Powszechny błąd nr 10: jQuerying It - Oddzielone drzewo DOM
Wiele razy prawdopodobnie słyszysz, że nie zaleca się używania jQuery z AngularJS i że należy tego unikać. Konieczne jest zrozumienie przyczyny tych stwierdzeń. O ile wiem, istnieją co najmniej trzy powody, ale żaden z nich nie jest faktycznym blokerem.
Powód 1: Kiedy wykonujesz kod jQuery, musisz sam wywołać $digest()
. W wielu przypadkach istnieje rozwiązanie AngularJS, które jest dostosowane do AngularJS i może być lepiej użyte wewnątrz Angulara niż jQuery (np. ng-click lub system zdarzeń).
Powód 2: Sposób myślenia o budowaniu aplikacji. Jeśli dodawałeś JavaScript do stron internetowych, które ładują się ponownie podczas nawigacji, nie musiałeś się zbytnio martwić o zużycie pamięci. W przypadku aplikacji jednostronicowych musisz się martwić. Jeśli nie posprzątasz, użytkownicy, którzy spędzą więcej niż kilka minut w Twojej aplikacji, mogą mieć coraz większe problemy z wydajnością.
Powód 3: Sprzątanie nie jest w rzeczywistości najłatwiejszą rzeczą do zrobienia i przeanalizowania. Nie ma możliwości wywołania garbage collectora ze skryptu (w przeglądarce). Możesz skończyć z oderwanymi drzewami DOM. Stworzyłem przykład (jQuery jest ładowane w 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);
To jest prosta dyrektywa, która wyświetla jakiś tekst. Poniżej znajduje się przycisk, który po prostu ręcznie zniszczy dyrektywę.
Tak więc po usunięciu dyrektywy pozostaje odniesienie do drzewa DOM w scope.toBeDetached. W narzędziach programistycznych chrome, jeśli uzyskasz dostęp do zakładki „profile”, a następnie „zrób migawkę sterty”, zobaczysz w danych wyjściowych:
Możesz żyć z kilkoma, ale źle, jeśli masz tonę. Zwłaszcza jeśli z jakiegoś powodu, jak w przykładzie, przechowujesz go na lunecie. Cały DOM będzie oceniany na każdym podsumowaniu. Problematyczne odłączone drzewo DOM to drzewo z 4 węzłami. Jak więc można to rozwiązać?
scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });
Odłączone drzewo DOM z 4 wpisami zostało usunięte!
W tym przykładzie dyrektywa używa tego samego zakresu i przechowuje element DOM w tym zakresie. Łatwiej było mi to zademonstrować w ten sposób. Nie zawsze jest tak źle, ponieważ można to zapisać w zmiennej. Jednak nadal zajmowałby pamięć, gdyby jakiekolwiek zamknięcie, które odwoływało się do tej zmiennej lub jakiejkolwiek innej z tego samego zakresu funkcji, żyło dalej.
Powszechny błąd nr 11: nadużywanie izolowanego zakresu
Ilekroć potrzebujesz dyrektywy, o której wiesz, że będzie używana w jednym miejscu lub której nie spodziewasz się kolidować z jakimkolwiek środowiskiem, w którym jest używana, nie ma potrzeby używania zakresu izolowanego. Ostatnio pojawiła się tendencja do tworzenia komponentów wielokrotnego użytku, ale czy wiesz, że podstawowe dyrektywy kątowe w ogóle nie używają izolowanego zakresu?
Istnieją dwa główne powody: nie możesz zastosować dwóch dyrektyw zakresu izolowanego do elementu i możesz napotkać problemy z zagnieżdżaniem/dziedziczeniem/przetwarzaniem zdarzeń. Zwłaszcza jeśli chodzi o transkluzję – efekty mogą nie być takie, jakich oczekujesz.
Więc to by się nie udało:
<p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"></p>
Nawet jeśli użyjesz tylko jednej dyrektywy, zauważysz, że ani modele zakresu izolowanego, ani zdarzenia emitowane w isolatedScopeDirective nie będą dostępne dla AnotherController. To smutne, ale możesz nagiąć i użyć magii transkluzji, aby to zadziałało – ale w większości przypadków nie ma potrzeby izolowania.
<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>
A więc teraz dwa pytania:
- Jak można przetwarzać modele zakresu nadrzędnego w dyrektywie o tym samym zakresie?
- Jak utworzyć instancję nowych wartości modelu?
Są dwa sposoby, w obu przekazujesz wartości do atrybutów. Rozważ ten główny kontroler:
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]; }
To kontroluje ten widok:
<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>
Zauważ, że „atrybut obserwacji” nie jest interpolowany. To wszystko działa dzięki magii JS. Oto definicja dyrektywy:
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' }; }
Zauważ, że attrs.watchAttribute
jest przekazywany do scope.$watch()
bez cudzysłowów! Oznacza to, że do $watch został przekazany ciąg znaków MC.foo
! Działa jednak, ponieważ każdy ciąg znaków przekazany do $watch()
jest oceniany względem zakresu, a MC.foo
jest dostępny w zakresie. Jest to również najczęstszy sposób, w jaki atrybuty są obserwowane w podstawowych dyrektywach AngularJS.
Zobacz kod szablonu na github i zajrzyj do $parse
i $eval
, aby uzyskać jeszcze więcej niesamowitości.
Powszechny błąd nr 12: nie sprzątanie po sobie — obserwatorzy, interwały, limity czasu i zmienne
AngularJS wykonuje trochę pracy w Twoim imieniu, ale nie wszystkie. Następujące elementy muszą zostać ręcznie wyczyszczone:
- Każdy obserwator, który nie jest powiązany z bieżącym zakresem (np. powiązany z $rootScope)
- Interwały
- Limity czasu
- Zmienne odwołujące się do DOM w dyrektywach
- Sprytne wtyczki jQuery, np. takie, które nie mają handlerów reagujących na zdarzenie JavaScript
$destroy
Jeśli nie zrobisz tego ręcznie, napotkasz nieoczekiwane zachowanie i wycieki pamięci. Co gorsza - nie będą one od razu widoczne, ale w końcu się podkradną. Prawo Murphy'ego.
O dziwo, AngularJS zapewnia przydatne sposoby radzenia sobie z tymi wszystkimi:
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. }); };
Zwróć uwagę na zdarzenie jQuery $destroy
. Nazywa się jak AngularJS, ale jest obsługiwany osobno. Zakres $watchers nie zareaguje na zdarzenie jQuery.
Powszechny błąd nr 13: Trzymanie zbyt wielu obserwatorów
Teraz powinno to być całkiem proste. Jest tu jedna rzecz do zrozumienia: $digest()
. Dla każdego wiązania {{ model }}
AngularJS tworzy obserwatora. W każdej fazie trawienia każde takie wiązanie jest oceniane i porównywane z poprzednią wartością. Nazywa się to sprawdzaniem brudu i to właśnie robi $digest. Jeśli wartość zmieniła się od ostatniego sprawdzenia, wywołanie zwrotne obserwatora jest uruchamiane. 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.
Najpierw uruchom serwer WWW na hoście 0.0.0.0, aby był dostępny z sieci lokalnej. Włącz inspektora sieci w ustawieniach. Następnie podłącz swoje urządzenie do pulpitu i uzyskaj dostęp do lokalnej strony programistycznej, używając adresu IP komputera zamiast zwykłego „localhost”. To wszystko, czego potrzeba, Twoje urządzenie powinno być teraz dostępne w przeglądarce na komputerze.
Oto szczegółowe instrukcje dla Androida A dla iOS, nieoficjalne przewodniki można łatwo znaleźć w google.
Ostatnio miałem fajne doświadczenia z browserSync. Działa podobnie do livereload, ale w rzeczywistości synchronizuje również wszystkie przeglądarki wyświetlające tę samą stronę za pomocą browserSync. Obejmuje to interakcję użytkownika, taką jak przewijanie, klikanie przycisków itp. Patrzyłem na dane wyjściowe dziennika aplikacji na iOS podczas kontrolowania strony na iPadzie z mojego pulpitu. Działało ładnie!
Częsty błąd #18: nieczytanie kodu źródłowego na przykładzie NG-INIT
Ng-init
, z brzmienia tego, powinien być podobny do ng-if
i ng-repeat
, prawda? Czy kiedykolwiek zastanawiałeś się, dlaczego w dokumentach jest komentarz, że nie należy go używać? IMHO to było zaskakujące! Spodziewałbym się, że dyrektywa zainicjuje model. To też robi, ale… jest zaimplementowane w inny sposób, to znaczy nie pilnuje wartości atrybutu. Nie musisz przeglądać kodu źródłowego AngularJS - pozwól, że ci go przyniosę:
var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });
Mniej niż można by się spodziewać? Całkiem czytelne, poza niezręczną składnią dyrektyw, prawda? Szósty wiersz jest tym, o co chodzi.
Porównaj to z 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 }); }); } }; }];
Znowu szósta linia. Jest tam $watch
, to sprawia, że ta dyrektywa jest dynamiczna. W kodzie źródłowym AngularJS dużą część całego kodu stanowią komentarze opisujące kod, który był w większości czytelny od samego początku. Uważam, że to świetny sposób na poznanie AngularJS.
Wniosek
Ten przewodnik opisujący najczęstsze błędy AngularJS jest prawie dwa razy dłuższy niż inne przewodniki. Okazało się w ten sposób naturalnie. Zapotrzebowanie na wysokiej jakości inżynierów frontendowych JavaScript jest bardzo duże. AngularJS jest teraz tak gorący i od kilku lat utrzymuje stabilną pozycję wśród najpopularniejszych narzędzi programistycznych. Z AngularJS 2.0 w drodze, prawdopodobnie będzie dominował przez wiele lat.
To, co jest wspaniałe w rozwoju front-endu, to to, że jest bardzo satysfakcjonujące. Nasza praca jest widoczna natychmiast, a ludzie wchodzą w bezpośrednią interakcję z dostarczanymi przez nas produktami. Czas spędzony na nauce JavaScript i uważam, że powinniśmy skupić się na języku JavaScript, jest bardzo dobrą inwestycją. To język Internetu. Konkurencja jest super silna! Jest dla nas jeden cel - doświadczenie użytkownika. Aby odnieść sukces, musimy objąć wszystko.
Kod źródłowy użyty w tych przykładach można pobrać z serwisu GitHub. Zapraszam do pobrania i dostosowania go do własnych potrzeb.
Chciałem przyznać kredyty czterem twórcom wydawniczym, którzy najbardziej mnie zainspirowali:
- Ben Nadel
- Motto Todda
- Pascal Precht
- Sandeep Panda
Chciałem również podziękować wszystkim wspaniałym ludziom na kanałach FreeNode #angularjs i #javascript za wiele wspaniałych rozmów i ciągłe wsparcie.
I na koniec zawsze pamiętaj:
// when in doubt, comment it out! :)