18 самых распространенных ошибок, которые допускают разработчики AngularJS

Опубликовано: 2022-03-11

Одностраничные приложения требуют, чтобы разработчики интерфейса стали лучшими инженерами-программистами. CSS и HTML больше не являются самой большой проблемой, на самом деле, больше нет какой-то одной проблемы. Front-end разработчик должен обрабатывать XHR, логику приложения (модели, представления, контроллеры), производительность, анимацию, стили, структуру, SEO и интеграцию с внешними сервисами. Результатом, который получается из всех этих объединений, является пользовательский опыт (UX), которому всегда следует уделять приоритетное внимание.

AngularJS — очень мощный фреймворк. Это третий по количеству звезд репозиторий на GitHub. Его несложно начать использовать, но цели, для достижения которых он предназначен, требуют понимания. Разработчики AngularJS больше не могут игнорировать потребление памяти, потому что оно больше не будет сбрасываться при навигации. Это авангард веб-разработки. Давайте обнимем это!

Распространенные ошибки AngularJS

Распространенная ошибка №1: доступ к области видимости через DOM

Есть несколько настроек оптимизации, рекомендуемых для производства. Один из них — отключение отладочной информации.

DebugInfoEnabled — это параметр, который по умолчанию имеет значение true и разрешает доступ к области через узлы DOM. Если вы хотите попробовать это через консоль JavaScript, выберите элемент DOM и получите доступ к его области действия с помощью:

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

Это может быть полезно, даже если jQuery не используется с его CSS, но его не следует использовать вне консоли. Причина в том, что когда $compileProvider.debugInfoEnabled установлено значение false, вызов .scope() на узле DOM вернет undefined .

Это один из немногих рекомендуемых вариантов для производства.

Обратите внимание, что вы по-прежнему можете получить доступ к прицелу через консоль даже во время производства. Вызовите angular.reloadWithDebugInfo() из консоли, и приложение сделает именно это.

Распространенная ошибка № 2: Отсутствие точки внутри

Вы, наверное, читали, что если у вас не было точки в вашей ng-модели , вы делали это неправильно. Когда дело касается наследования, это утверждение часто верно. Области имеют прототипную модель наследования, типичную для JavaScript, а вложенные области являются общими для AngularJS. Многие директивы создают дочерние области, такие как ngRepeat , ngIf и ngController . При разрешении модели поиск начинается с текущей области и проходит через каждую родительскую область вплоть до $rootScope .

Но что происходит при установке нового значения, зависит от того, какую модель (переменную) мы хотим изменить. Если модель является примитивной, дочерняя область просто создаст новую модель. Но если изменение относится к свойству объекта модели, поиск в родительских областях найдет указанный объект и изменит его фактическое свойство. Новая модель не будет установлена ​​в текущей области, поэтому маскирование не произойдет:

 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>

Щелчок по кнопке с надписью «Установить примитив» установит для foo во внутренней области значение 2, но не изменит foo во внешней области.

Нажатие кнопки с надписью «Изменить объект» изменит свойство панели из родительской области. Поскольку во внутренней области видимости нет переменной, затенение не произойдет, а видимое значение для bar будет равно 3 в обеих областях.

Другой способ сделать это — использовать тот факт, что на родительские области и корневую область ссылаются из каждой области. Объекты $parent и $root можно использовать для доступа к родительской области и $rootScope соответственно непосредственно из представления. Это может быть мощный способ, но я не фанат его из-за проблемы с нацеливанием на конкретную область выше по течению. Существует еще один способ установить и получить доступ к свойствам, специфичным для области, — с помощью синтаксиса controllerAs .

Распространенная ошибка № 3: не использовать синтаксис controllerAs

Альтернативный и наиболее эффективный способ назначения моделей для использования объекта контроллера вместо внедренной $scope. Вместо того, чтобы вводить область действия, мы можем определить такие модели:

 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>

Это гораздо менее запутанно. Особенно при наличии множества вложенных областей, как это может быть в случае с вложенными состояниями.

В синтаксисе контроллера есть еще кое-что.

Распространенная ошибка № 4: неполное использование синтаксиса контроллера

Есть несколько предостережений относительно того, как выставляется объект контроллера. По сути, это объект, установленный в области действия контроллера, как и обычная модель.

Если вам нужно наблюдать за свойством объекта контроллера, вы можете наблюдать за функцией, но не обязаны. Вот пример:

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

Легче просто сделать:

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

Это означает, что также вниз по цепочке областей действия вы можете получить доступ к MC из дочернего контроллера:

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

Однако, чтобы иметь возможность сделать это, вы должны соответствовать аббревиатуре, которую вы используете для controllerAs. Есть как минимум три способа его установки. Вы уже видели первый:

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

Однако, если вы используете ui-router , указание контроллера таким образом может привести к ошибке. Для состояний контроллеры должны быть указаны в конфигурации состояния:

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

Есть еще один способ комментировать:

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

Вы можете сделать то же самое в директивах:

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

Другой способ аннотирования тоже действителен, хотя и менее лаконичен:

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

Распространенная ошибка № 5: не использовать именованные представления с UI-ROUTER для повышения мощности»

Решением маршрутизации де-факто для AngularJS до сих пор был ui-router . Некоторое время назад удаленный из ядра модуль ngRoute был слишком простым для более сложной маршрутизации.

На подходе новый NgRouter , но авторы все еще считают его слишком ранним для производства. Когда я пишу это, стабильный Angular — 1.3.15, а ui-router — крут.

Основные причины:

  • удивительная государственная вложенность
  • абстракция маршрута
  • необязательные и обязательные параметры

Здесь я расскажу о вложении состояний, чтобы избежать ошибок AngularJS.

Думайте об этом как о сложном, но стандартном варианте использования. Существует приложение, которое имеет вид домашней страницы и вид продукта. Представление продукта состоит из трех отдельных разделов: введение, виджет и контент. Мы хотим, чтобы виджет сохранялся и не перезагружался при переключении между состояниями. Но содержимое должно перезагрузиться.

Рассмотрим следующую структуру индексной страницы продукта в формате 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>

Это то, что мы могли бы получить от кодировщика HTML, и теперь нам нужно разделить его на файлы и состояния. Обычно я придерживаюсь соглашения о том, что существует абстрактное состояние MAIN, которое при необходимости сохраняет глобальные данные. Используйте это вместо $rootScope. В состоянии Main также будет храниться статический HTML-код, необходимый на каждой странице. Я держу index.html в чистоте.

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

Затем давайте посмотрим на индексную страницу продукта:

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

Как видите, на индексной странице продукта есть три именованных представления. Один для вступления, один для виджета и один для продукта. Соответствуем спецификациям! Итак, теперь давайте настроим маршрутизацию:

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

Это будет первый подход. Что происходит при переключении между main.product.index и main.product.details ? Контент и виджет перезагружаются, но мы хотим перезагрузить только контент. Это было проблематично, и разработчики фактически создали маршрутизаторы, которые поддерживали бы именно эту функциональность. Одним из названий этого было липкое представление . К счастью, ui-router поддерживает это из коробки с таргетингом на абсолютное именованное представление .

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

Переместив определение состояния в родительское представление, которое также является абстрактным, мы можем защитить дочернее представление от перезагрузки при переключении URL-адресов, что обычно влияет на дочерние элементы. Конечно, виджет может быть простой директивой. Но дело в том, что это может быть и другое сложное вложенное состояние.

Есть еще один способ сделать это с помощью $urlRouterProvider.deferIntercept() , но я думаю, что использование конфигурации состояния на самом деле лучше. Если вас интересует перехват маршрутов, я написал небольшой туториал по StackOverflow.

Распространенная ошибка № 6: Объявление всего в мире Angular с помощью анонимных функций

Эта ошибка более легкого калибра и больше связана со стилем, чем с тем, чтобы избежать сообщений об ошибках AngularJS. Возможно, вы уже заметили, что я редко передаю анонимные функции во внутренние объявления angular. Обычно я просто сначала определяю функцию, а затем передаю ее.

Это касается не только функций. Я научился этому подходу, читая руководства по стилю, особенно Airbnb и Тодда Мотто. Я считаю, что у него есть несколько преимуществ и почти нет недостатков.

Прежде всего, вы можете намного проще манипулировать и видоизменять свои функции и объекты, если они назначены переменной. Во-вторых, код чище и легко разбивается на файлы. Это означает ремонтопригодность. Если вы не хотите загрязнять глобальное пространство имен, оберните каждый файл в IIFE. Третья причина — тестируемость. Рассмотрим этот пример:

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

Итак, теперь мы можем издеваться над publicMethod1 , но зачем нам это делать, если он открыт? Не проще ли просто подсмотреть существующий метод? Однако на самом деле метод представляет собой другую функцию — тонкую обертку. Взгляните на этот подход:

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

Это касается не только стиля, так как на самом деле код более многоразовый и идиоматический. Разработчик получает больше выразительной силы. Разделение всего кода на автономные блоки упрощает задачу.

Распространенная ошибка № 7: Выполнение тяжелой обработки в Angular AKA с использованием воркеров

В некоторых сценариях может потребоваться обработать большой массив сложных объектов, пропустив их через набор фильтров, декораторов и, наконец, алгоритм сортировки. Один из вариантов использования — когда приложение должно работать в автономном режиме или когда производительность отображения данных является ключевой. А поскольку JavaScript является однопоточным, браузер относительно легко заморозить.

Этого также легко избежать с помощью веб-воркеров. Кажется, нет никаких популярных библиотек, которые бы обрабатывали это специально для AngularJS. Хотя это может быть и к лучшему, так как реализация проста.

Сначала настроим сервис:

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

Итак, рабочий:

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

Теперь внедрите службу как обычно и обработайте scoringService.scoreItems() так же, как любой метод службы, который возвращает обещание. Тяжелая обработка будет выполняться в отдельном потоке, и UX не пострадает.

На что обращать внимание:

  • похоже, не существует общего правила, сколько рабочих должно создаваться. Некоторые разработчики утверждают, что 8 — хорошее число, но воспользуйтесь онлайн-калькулятором и подберите себе
  • проверьте совместимость со старыми браузерами
  • Я столкнулся с проблемой при передаче числа 0 от службы к работнику. Я применил .toString() к переданному свойству, и все заработало правильно.

Распространенная ошибка № 8: Решается чрезмерное использование и непонимание

Решения добавляют дополнительное время к загрузке представления. Я считаю, что высокая производительность внешнего интерфейса — наша главная цель. Не должно быть проблем с визуализацией некоторых частей представления, пока приложение ожидает данные от API.

Рассмотрим эту настройку:

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

Вывод консоли будет:

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

Что в основном означает, что:

  • Решения выполняются асинхронно
  • Мы не можем полагаться на порядок выполнения (или, по крайней мере, нам нужно немного сгибаться)
  • Все состояния блокируются до тех пор, пока все разрешения не сделают свое дело, даже если они не являются абстрактными.

Это означает, что прежде чем пользователь увидит какие-либо выходные данные, он должен дождаться всех зависимостей. Нам нужны эти данные, конечно, хорошо. Если абсолютно необходимо иметь его перед представлением, поместите его в блок .run() . В противном случае просто вызовите службу из контроллера и изящно обработайте полузагруженное состояние. Видеть работу в процессе — а контроллер уже выполняется, так что на самом деле это прогресс — лучше, чем зависание приложения.

Распространенная ошибка № 9: не оптимизировать приложение — три примера

а) Вызывает слишком много циклов дайджеста, например, прикрепление ползунков к моделям

Это общая проблема, которая может привести к ошибкам AngularJS, но я расскажу о ней на примере слайдеров. Я использовал эту библиотеку слайдеров, слайдер углового диапазона, потому что мне нужна была расширенная функциональность. Эта директива имеет такой синтаксис в минимальной версии:

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

Рассмотрим следующий код в контроллере:

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

Так что работает медленно. Случайным решением было бы установить тайм-аут на входе. Но это не всегда удобно, и иногда мы действительно не хотим откладывать фактическое изменение модели во всех случаях.

Поэтому мы добавим временную модель, привязанную к изменению рабочей модели по тайм-ауту:

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

и в контроллере:

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

б) Не использовать $applyAsync

AngularJS не имеет механизма опроса для вызова $digest() . Он выполняется только потому, что мы используем директивы (например, ng-click , input ), сервисы ( $timeout , $http ) и методы ( $watch ), которые оценивают наш код и впоследствии вызывают дайджест.

Что .$applyAsync() , так это задерживает разрешение выражений до следующего цикла $digest() , который запускается после тайм-аута 0, что на самом деле составляет ~ 10 мс.

Сейчас есть два способа использования applyAsync . Автоматический способ для запросов $http и ручной способ для остальных.

Чтобы все HTTP-запросы, которые возвращаются примерно в одно и то же время, разрешались в одном дайджесте, выполните:

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

Ручной способ показывает, как это работает на самом деле. Рассмотрим некоторую функцию, которая запускается при обратном вызове прослушивателя событий vanilla JS или jQuery .click() , или какой-либо другой внешней библиотеки. После того, как он выполнит и изменит модели, если вы еще не обернули его в $apply() , вам нужно вызвать $scope.$root.$digest() ( $rootScope.$digest() ) или, по крайней мере, $scope.$digest() . В противном случае вы не увидите никаких изменений.

Если вы сделаете это несколько раз в одном потоке, он может начать работать медленно. Вместо этого рассмотрите возможность вызова $scope.$applyAsync() для выражений. Он установит только один цикл дайджеста для всех из них.

c) Выполнение тяжелой обработки изображений

Если у вас плохая производительность, вы можете выяснить причину, используя временную шкалу из инструментов разработчика Chrome. Подробнее об этом инструменте я напишу в ошибке №17. Если после записи на вашей временной шкале преобладает зеленый цвет, проблемы с производительностью могут быть связаны с обработкой изображений. Это не имеет прямого отношения к AngularJS, но может произойти помимо проблем с производительностью AngularJS (которые в основном будут желтыми на графике). Как фронтенд-инженеры, мы должны думать о завершении конечного проекта.

Найдите минутку, чтобы оценить:

  • Вы используете параллакс?
  • У вас есть несколько слоев контента, перекрывающих друг друга?
  • Вы перемещаете изображения?
  • Вы масштабируете изображения (например, с размером фона)?
  • Изменяете ли вы размер изображений в циклах и, возможно, вызываете циклы дайджеста при изменении размера?

Если вы ответили «да» хотя бы на три вопроса из вышеперечисленного, рассмотрите возможность его смягчения. Возможно, вы можете обслуживать изображения разных размеров и вообще не изменять их размер. Может быть, вы могли бы добавить «transform: translateZ (0)» для принудительной обработки GPU. Или используйте requestAnimationFrame для обработчиков.

Распространенная ошибка № 10: jQuerying It — отдельное дерево DOM

Много раз вы, вероятно, слышали, что не рекомендуется использовать jQuery с AngularJS и что этого следует избегать. Крайне важно понять причину этих заявлений. Насколько я вижу, есть по крайней мере три причины, но ни одна из них не является блокирующей.

Причина 1: когда вы выполняете код jQuery, вам нужно самостоятельно вызывать $digest() . Во многих случаях существует решение AngularJS, адаптированное для AngularJS, которое может лучше использоваться внутри Angular, чем jQuery (например, ng-click или система событий).

Причина 2: метод создания приложения. Если вы добавляли JavaScript на веб-сайты, которые перезагружаются при навигации, вам не нужно было слишком беспокоиться о потреблении памяти. С одностраничными приложениями вам действительно нужно беспокоиться. Если вы не выполните очистку, у пользователей, которые проводят в вашем приложении более нескольких минут, могут возникнуть проблемы с производительностью.

Причина 3: Очистка на самом деле не самая простая вещь для анализа. Нет возможности вызвать сборщик мусора из скрипта (в браузере). Вы можете получить отдельные деревья DOM. Я создал пример (jQuery загружается в 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);

Это простая директива, которая выводит некоторый текст. Под ним есть кнопка, которая просто уничтожит директиву вручную.

Поэтому, когда директива удаляется, остается ссылка на дерево DOM в scope.toBeDetached. В инструментах разработчика Chrome, если вы откроете вкладку «Профили», а затем «сделать моментальный снимок кучи», вы увидите в выводе:

Вы можете жить с несколькими, но плохо, если у вас есть тонна. Особенно, если по какой-то причине, как в примере, вы храните его в области видимости. Весь DOM будет оцениваться при каждом дайджесте. Проблемное отдельное дерево DOM — это дерево с 4 узлами. Итак, как это можно решить?

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

Отдельное DOM-дерево с 4 записями удалено!

В этом примере директива использует ту же область действия и сохраняет элемент DOM в этой области. Мне было проще это продемонстрировать. Это не всегда так плохо, так как вы можете сохранить его в переменной. Тем не менее, это все равно заняло бы память, если бы какое-либо замыкание, которое ссылалось на эту переменную или любую другую из той же области видимости функции, продолжало существовать.

Распространенная ошибка № 11: чрезмерное использование изолированной области видимости

Всякий раз, когда вам нужна директива, которая, как вы знаете, будет использоваться в одном месте или которая, как вы не ожидаете, будет конфликтовать с какой-либо средой, в которой она используется, нет необходимости использовать изолированную область. В последнее время наблюдается тенденция к созданию повторно используемых компонентов, но знаете ли вы, что основные директивы angular вообще не используют изолированную область видимости?

Есть две основные причины: вы не можете применить к элементу две изолированные директивы области видимости, и вы можете столкнуться с проблемами вложения/наследования/обработки событий. Особенно в отношении трансклюзии - эффекты могут быть не такими, как вы ожидаете.

Так что это не удастся:

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

И даже если вы используете только одну директиву, вы заметите, что ни модели изолированных областей действия, ни события, транслируемые в isolatedScopeDirective, не будут доступны для AnotherController. Это печально, но вы можете сгибать и использовать магию трансклюзии, чтобы заставить ее работать, но в большинстве случаев нет необходимости изолировать.

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

Итак, теперь два вопроса:

  1. Как вы можете обрабатывать модели родительской области в директиве той же области?
  2. Как вы можете инстанцировать новые значения модели?

Есть два способа, в обоих вы передаете значения атрибутам. Рассмотрим этот 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]; }

Это управляет этим представлением:

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

Обратите внимание, что «watch-attribute» не интерполируется. Все работает благодаря магии JS. Вот определение директивы:

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

Обратите внимание, что attrs.watchAttribute передается в scope.$watch() без кавычек! Это означает, что на самом деле в $watch была передана строка MC.foo ! Однако это работает, потому что любая строка, переданная в $watch() , оценивается в соответствии с областью действия, а MC.foo доступен в области видимости. Это также наиболее распространенный способ отслеживания атрибутов в основных директивах AngularJS.

См. код на github для шаблона и посмотрите на $parse и $eval для еще большей удивительности.

Распространенная ошибка № 12: Не убирать за собой — наблюдатели, интервалы, тайм-ауты и переменные

AngularJS выполняет некоторую работу от вашего имени, но не всю. Следующее должно быть очищено вручную:

  • Любые наблюдатели, не привязанные к текущей области (например, привязанные к $rootScope)
  • Интервалы
  • Тайм-ауты
  • Переменные, ссылающиеся на DOM в директивах
  • Хитрые плагины jQuery, например те, у которых нет обработчиков, реагирующих на событие JavaScript $destroy

Если вы не сделаете этого вручную, вы столкнетесь с непредвиденным поведением и утечками памяти. Хуже того, они не будут видны сразу, но в конце концов они появятся. Закон Мерфи.

Удивительно, но AngularJS предоставляет удобные способы справиться со всем этим:

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

Обратите внимание на событие jQuery $destroy . Он называется так же, как AngularJS, но обрабатывается отдельно. Scope $watchers не будет реагировать на событие jQuery.

Распространенная ошибка № 13: держать слишком много наблюдателей

Теперь это должно быть довольно просто. Здесь нужно понять одну вещь: $digest() . Для каждой привязки {{ model }} AngularJS создает наблюдателя. На каждой фазе дайджеста каждое такое связывание оценивается и сравнивается с предыдущим значением. Это называется грязной проверкой, и это то, что делает $digest. Если значение изменилось с момента последней проверки, запускается обратный вызов наблюдателя. 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.

Сначала запустите веб-сервер на хосте 0.0.0.0, чтобы он был доступен из вашей локальной сети. Включите веб-инспектор в настройках. Затем подключите свое устройство к рабочему столу и получите доступ к локальной странице разработки, используя IP-адрес вашего компьютера вместо обычного «localhost». Это все, что нужно, теперь ваше устройство должно быть доступно для вас из браузера вашего рабочего стола.

Вот подробные инструкции для Android. А для iOS неофициальные руководства легко найти через google.

Недавно у меня был классный опыт работы с browserSync. Он работает аналогично livereload, но фактически синхронизирует все браузеры, которые просматривают одну и ту же страницу, через browserSync. Это включает в себя взаимодействие с пользователем, такое как прокрутка, нажатие кнопок и т. д. Я просматривал вывод журнала приложения iOS, управляя страницей на iPad со своего рабочего стола. Это сработало хорошо!

Распространенная ошибка № 18: не читать исходный код на примере NG-INIT

Ng-init , судя по звуку, должен быть похож на ng-if и ng-repeat , верно? Вы когда-нибудь задумывались, почему в документах есть комментарий о том, что его нельзя использовать? ИМХО это было неожиданно! Я ожидаю, что директива инициализирует модель. Он тоже так делает, но… реализован по-другому, то есть не следит за значением атрибута. Вам не нужно просматривать исходный код AngularJS — позвольте мне представить его вам:

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

Меньше, чем вы ожидаете? Довольно читабельно, если не считать неудобного синтаксиса директив, не так ли? Шестая строка — это то, о чем идет речь.

Сравните это с 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 }); }); } }; }];

И снова шестая строка. Там есть $watch , вот что делает эту директиву динамической. В исходном коде AngularJS большую часть всего кода составляют комментарии, которые описывают код, который с самого начала был в основном читабелен. Я считаю, что это отличный способ узнать об AngularJS.

Заключение

Это руководство, описывающее наиболее распространенные ошибки AngularJS, почти в два раза длиннее других руководств. Так получилось естественно. Спрос на высококвалифицированных разработчиков интерфейса JavaScript очень высок. AngularJS сейчас на пике популярности , и вот уже несколько лет он занимает стабильное положение среди самых популярных инструментов разработки. С появлением AngularJS 2.0 он, вероятно, будет доминировать в ближайшие годы.

Чем хороша фронтенд-разработка, так это тем, что она очень полезна. Наша работа видна мгновенно, и люди напрямую взаимодействуют с продуктами, которые мы поставляем. Время, потраченное на изучение JavaScript, и я считаю, что мы должны сосредоточиться на языке JavaScript, является очень хорошей инвестицией. Это язык Интернета. Конкуренция супер сильная! У нас есть один фокус — пользовательский опыт. Чтобы добиться успеха, нам нужно охватить все.

Исходный код, используемый в этих примерах, можно загрузить с GitHub. Не стесняйтесь скачать его и сделать его своим.

Я хотел отдать должное четырем разработчикам публикаций, которые меня больше всего вдохновили:

  • Бен Надель
  • Тодд Девиз
  • Паскаль Прехт
  • Сандип Панда

Я также хотел поблагодарить всех замечательных людей на каналах FreeNode #angularjs и #javascript за множество отличных бесед и постоянную поддержку.

И, наконец, всегда помните:

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