Os 18 erros mais comuns que os desenvolvedores do AngularJS cometem

Publicados: 2022-03-11

Os aplicativos de página única exigem que os desenvolvedores front-end se tornem melhores engenheiros de software. CSS e HTML não são mais a maior preocupação, na verdade, não existe mais uma única preocupação. O desenvolvedor front-end precisa lidar com XHRs, lógica do aplicativo (modelos, visualizações, controladores), desempenho, animações, estilos, estrutura, SEO e integração com serviços externos. O resultado que emerge de todos esses combinados é a Experiência do Usuário (UX) que deve ser sempre priorizada.

AngularJS é um framework muito poderoso. É o terceiro repositório mais estrelado no GitHub. Não é difícil começar a usar, mas os objetivos que se pretende atingir exigem compreensão. Os desenvolvedores do AngularJS não podem mais ignorar o consumo de memória, porque ele não será mais redefinido na navegação. Esta é a vanguarda do desenvolvimento web. Vamos abraçá-lo!

Erros comuns do AngularJS

Erro comum nº 1: acessando o escopo por meio do DOM

Existem alguns ajustes de otimização recomendados para produção. Um deles é desabilitar as informações de depuração.

DebugInfoEnabled é uma configuração cujo padrão é true e permite acesso ao escopo por meio de nós DOM. Se você quiser tentar isso por meio do console JavaScript, selecione um elemento DOM e acesse seu escopo com:

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

Pode ser útil mesmo quando não estiver usando jQuery com seu CSS, mas não deve ser usado fora do console. A razão é que quando $compileProvider.debugInfoEnabled é definido como false, chamar .scope() em um nó DOM retornará undefined .

Essa é uma das poucas opções recomendadas para produção.

Observe que você ainda pode acessar o escopo por meio do console, mesmo quando estiver em produção. Chame angular.reloadWithDebugInfo() do console e o aplicativo fará exatamente isso.

Erro comum nº 2: não ter um ponto lá

Você provavelmente já leu que se não tivesse um ponto em seu ng-model , estaria fazendo errado. Quando se trata de herança, essa afirmação geralmente é verdadeira. Os escopos têm um modelo prototípico de herança, típico do JavaScript, e os escopos aninhados são comuns ao AngularJS. Muitas diretivas criam escopos filho como ngRepeat , ngIf e ngController . Ao resolver um modelo, a pesquisa começa no escopo atual e passa por todos os escopos pai, até $rootScope .

Mas, ao definir um novo valor, o que acontece depende de que tipo de modelo (variável) queremos alterar. Se o modelo for primitivo, o escopo filho apenas criará um novo modelo. Mas se a alteração for em uma propriedade de um objeto de modelo, a pesquisa nos escopos pai localizará o objeto referenciado e alterará sua propriedade real. Um novo modelo não seria definido no escopo atual, portanto, nenhum mascaramento ocorreria:

 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>

Clicar no botão “Definir primitivo” definirá foo no escopo interno para 2, mas não alterará foo no escopo externo.

Clicar no botão “Alterar objeto” mudará a propriedade da barra do escopo pai. Como não há variável no escopo interno, nenhum sombreamento ocorrerá e o valor visível para bar será 3 em ambos os escopos.

Outra maneira de fazer isso é aproveitar o fato de que os escopos pai e o escopo raiz são referenciados em todos os escopos. Os objetos $parent e $root podem ser usados ​​para acessar o escopo pai e $rootScope , respectivamente, diretamente da exibição. Pode ser uma maneira poderosa, mas não sou fã dela devido ao problema de direcionar um escopo específico no fluxo. Há outra maneira de definir e acessar propriedades específicas de um escopo - usando a sintaxe controllerAs .

Erro comum nº 3: não usar a sintaxe controllerAs

A maneira alternativa e mais eficiente de atribuir modelos para usar um objeto controlador em vez do $scope injetado. Em vez de injetar escopo, podemos definir modelos como este:

 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>

Isso é muito menos confuso. Especialmente quando há muitos escopos aninhados, como pode ser o caso de estados aninhados.

Há mais na sintaxe controllerAs.

Erro comum nº 4: não utilizar totalmente a sintaxe controllerAs

Existem algumas advertências sobre como o objeto do controlador é exposto. É basicamente um objeto definido no escopo do controlador, assim como um modelo normal.

Se você precisar observar uma propriedade do objeto controlador, poderá observar uma função, mas isso não é obrigatório. Aqui está um exemplo:

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

É mais fácil fazer apenas:

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

O que significa que também na cadeia de escopo, você pode acessar o MC de um controlador filho:

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

No entanto, para poder fazer isso, você precisa ser consistente com o acrônimo usado para controllerAs. Existem pelo menos três maneiras de configurá-lo. Você já viu o primeiro:

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

No entanto, se você usar ui-router , especificar um controlador dessa maneira estará sujeito a erros. Para estados, os controladores devem ser especificados na configuração de estado:

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

Há outra maneira de anotar:

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

Você pode fazer o mesmo em diretivas:

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

A outra maneira de anotar também é válida, embora menos concisa:

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

Erro comum nº 5: não usar visualizações nomeadas com UI-ROUTER para energia”

A solução de roteamento de fato para AngularJS tem sido, até agora, o ui-router . Removido do núcleo há algum tempo, o módulo ngRoute, era muito básico para roteamento mais sofisticado.

Há um novo NgRouter a caminho, mas os autores ainda o consideram muito cedo para produção. Quando estou escrevendo isso, o Angular estável é 1.3.15, e ui-router arrasa.

Os principais motivos:

  • aninhamento de estado incrível
  • abstração de rota
  • parâmetros opcionais e obrigatórios

Aqui abordarei o aninhamento de estado para evitar erros do AngularJS.

Pense nisso como um caso de uso complexo, mas padrão. Existe um aplicativo, que tem uma visualização da página inicial e uma visualização do produto. A visualização do produto tem três seções separadas: a introdução, o widget e o conteúdo. Queremos que o widget persista e não recarregue ao alternar entre o estado. Mas o conteúdo deve recarregar.

Considere a seguinte estrutura de página de índice de produto 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>

Isso é algo que podemos obter do codificador HTML e agora precisamos separá-lo em arquivos e estados. Eu geralmente sigo a convenção de que existe um estado MAIN abstrato, que mantém os dados globais, se necessário. Use isso em vez de $rootScope. O estado Main também manterá o HTML estático necessário em todas as páginas. Eu mantenho index.html limpo.

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

Então vamos ver a página de índice do produto:

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

Como você pode ver, a página de índice do produto tem três visualizações nomeadas. Um para a introdução, um para o widget e um para o produto. Atendemos as especificações! Agora vamos configurar o roteamento:

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

Essa seria a primeira abordagem. Agora, o que acontece ao alternar entre main.product.index e main.product.details ? O conteúdo e o widget são recarregados, mas queremos apenas recarregar o conteúdo. Isso era problemático, e os desenvolvedores realmente criaram roteadores que dariam suporte apenas a essa funcionalidade. Um dos nomes para isso era visualizações fixas . Felizmente, ui-router oferece suporte pronto para uso com o direcionamento de exibição nomeado absoluto .

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

Ao mover a definição de estado para a visão pai, que também é abstrata, podemos evitar que a visão filha seja recarregada ao alternar urls que normalmente afetam os irmãos dessa criança. Claro, o widget pode ser uma diretiva simples. Mas o ponto é que também pode ser outro estado aninhado complexo.

Existe outra maneira de fazer isso usando $urlRouterProvider.deferIntercept() , mas acho que usar a configuração de estado é realmente melhor. Se você estiver interessado em interceptar rotas, escrevi um pequeno tutorial sobre StackOverflow.

Erro comum nº 6: declarar tudo no mundo angular usando funções anônimas

Esse erro é de calibre mais leve e é mais uma questão de estilo do que evitar mensagens de erro do AngularJS. Você deve ter notado anteriormente que raramente passo funções anônimas para declarações internas do angular. Eu geralmente apenas defino uma função primeiro e depois a passo.

Isso diz respeito a mais do que apenas funções. Peguei essa abordagem lendo guias de estilo, especialmente do Airbnb e do Todd Motto. Acredito que há várias vantagens e quase nenhuma desvantagem.

Em primeiro lugar, você pode manipular e alterar suas funções e objetos com muito mais facilidade se eles forem atribuídos a uma variável. Em segundo lugar, o código é mais limpo e pode ser facilmente dividido em arquivos. Isso significa manutenibilidade. Se você não quiser poluir o namespace global, envolva todos os arquivos em IIFEs. A terceira razão é a testabilidade. Considere este exemplo:

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

Então agora podemos zombar do publicMethod1 , mas por que devemos fazer isso já que ele está exposto? Não seria mais fácil apenas espionar o método existente? No entanto, o método é na verdade outra função - um invólucro fino. Dê uma olhada nesta abordagem:

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

Não se trata apenas de estilo, pois na verdade o código é mais reutilizável e idiomático. O desenvolvedor ganha poder mais expressivo. Dividir todo o código em blocos autocontidos torna tudo mais fácil.

Erro comum nº 7: fazendo processamento pesado em Angular AKA usando workers

Em alguns cenários, pode ser necessário processar uma grande variedade de objetos complexos passando-os por um conjunto de filtros, decoradores e, finalmente, um algoritmo de classificação. Um caso de uso é quando o aplicativo deve funcionar offline ou onde o desempenho da exibição de dados é fundamental. E como o JavaScript é single-thread, é relativamente fácil congelar o navegador.

Também é fácil evitá-lo com os trabalhadores da web. Não parece haver nenhuma biblioteca popular que lide com isso especificamente para o AngularJS. Pode ser o melhor, porém, já que a implementação é fácil.

Primeiro, vamos configurar o serviço:

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

Agora, o trabalhador:

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

Agora, injete o serviço como de costume e trate scoringService.scoreItems() como faria com qualquer método de serviço que retorne uma promessa. O processamento pesado será realizado em um thread separado e nenhum dano será causado ao UX.

O que observar:

  • não parece haver uma regra geral para quantos trabalhadores gerar. Alguns desenvolvedores afirmam que 8 é um bom número, mas use uma calculadora online e faça a sua escolha
  • verifique a compatibilidade com navegadores mais antigos
  • Eu me deparo com um problema ao passar o número 0 do serviço para o trabalhador. Apliquei .toString() na propriedade passada e funcionou corretamente.

Erro comum nº 8: soluções de uso excessivo e mal-entendido

As resoluções adicionam tempo extra ao carregamento da vista. Acredito que o alto desempenho do aplicativo front-end seja nosso principal objetivo. Não deve ser um problema renderizar algumas partes da visualização enquanto o aplicativo aguarda os dados da API.

Considere esta configuração:

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

A saída do console será:

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

O que basicamente significa que:

  • As resoluções são executadas de forma assíncrona
  • Não podemos confiar em uma ordem de execução (ou pelo menos precisamos flexibilizar um pouco)
  • Todos os estados são bloqueados até que todas as resoluções funcionem, mesmo que não sejam abstratas.

Isso significa que antes que o usuário veja qualquer saída, ele deve esperar por todas as dependências. Precisamos ter esses dados, claro, ok. Se for absolutamente necessário tê-lo antes da visualização, coloque-o em um bloco .run() . Caso contrário, basta fazer a chamada para o serviço do controlador e lidar com o estado semi-carregado normalmente. Ver o trabalho em andamento - e o controlador já está executado, então na verdade está em andamento - é melhor do que ter o aplicativo travado.

Erro comum nº 9: não otimizar o aplicativo - três exemplos

a) Causando muitos loops de resumo, como anexar controles deslizantes a modelos

Este é um problema geral que pode resultar em erros do AngularJS, mas vou discuti-lo no exemplo dos sliders. Eu estava usando essa biblioteca de controle deslizante, controle deslizante de alcance angular, porque precisava da funcionalidade estendida. Essa diretiva tem esta sintaxe na versão mínima:

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

Considere o seguinte código no controlador:

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

Então isso funciona devagar. A solução casual seria definir um tempo limite na entrada. Mas isso nem sempre é útil e, às vezes, não queremos atrasar a mudança real do modelo em todos os casos.

Portanto, adicionaremos um modelo temporário vinculado a alterar o modelo de trabalho no tempo limite:

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

e no controlador:

 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) Não usando $applyAsync

AngularJS não possui um mecanismo de pesquisa para chamar $digest() . Ele só é executado porque usamos as diretivas (por exemplo ng-click , input ), services ( $timeout , $http ) e métodos ( $watch ) que avaliam nosso código e chamam um resumo depois.

O que .$applyAsync() faz é atrasar a resolução das expressões até o próximo ciclo $digest() , que é acionado após um tempo limite 0, que na verdade é ~10ms.

Existem duas maneiras de usar applyAsync agora. Uma maneira automatizada para solicitações $http e uma maneira manual para o resto.

Para fazer com que todas as solicitações http que retornam ao mesmo tempo sejam resolvidas em um resumo, faça:

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

A forma manual mostra como ele realmente funciona. Considere alguma função que é executada no retorno de chamada para um ouvinte de evento vanilla JS ou um jQuery .click() , ou alguma outra biblioteca externa. Depois de executar e alterar os modelos, se você ainda não o envolveu em um $apply() , você precisa chamar $scope.$root.$digest() ( $rootScope.$digest() ), ou pelo menos $scope.$digest() . Caso contrário, você não verá nenhuma mudança.

Se você fizer isso várias vezes em um fluxo, ele poderá começar a ficar lento. Considere chamar $scope.$applyAsync() nas expressões. Ele definirá apenas um ciclo de resumo para todos eles.

c) Fazendo processamento pesado de imagens

Se você tiver um desempenho ruim, poderá investigar o motivo usando a Linha do tempo das Ferramentas do desenvolvedor do Chrome. Vou escrever mais sobre essa ferramenta no erro #17. Se o gráfico de linha do tempo estiver dominado pela cor verde após a gravação, seus problemas de desempenho podem estar relacionados ao processamento de imagens. Isso não está estritamente relacionado ao AngularJS, mas pode acontecer em cima dos problemas de desempenho do AngularJS (que seriam principalmente amarelos no gráfico). Como engenheiros de front-end, precisamos pensar no projeto final completo.

Reserve um momento para avaliar:

  • Você usa paralaxe?
  • Você tem várias camadas de conteúdo sobrepostas umas às outras?
  • Você move suas imagens?
  • Você dimensiona imagens (por exemplo, com tamanho de fundo)?
  • Você redimensiona imagens em loops e talvez cause loops de resumo ao redimensionar?

Se você respondeu “sim” a pelo menos três das perguntas acima, considere facilitar. Talvez você possa veicular vários tamanhos de imagem e não redimensionar. Talvez você possa adicionar o “transform: translateZ(0)” force o hack de processamento da GPU. Ou use requestAnimationFrame para manipuladores.

Erro comum nº 10: jQuerying - Árvore DOM desanexada

Muitas vezes você provavelmente ouve que não é recomendado usar jQuery com AngularJS, e que deve ser evitado. É imperativo entender a razão por trás dessas declarações. Existem pelo menos três razões, até onde posso ver, mas nenhuma delas são bloqueadoras reais.

Razão 1: Quando você executa o código jQuery, você mesmo precisa chamar $digest() . Para muitos casos, existe uma solução AngularJS que é feita sob medida para AngularJS e pode ser melhor usada dentro de Angular do que jQuery (por exemplo, ng-click ou o sistema de eventos).

Razão 2: O método de pensamento sobre a construção do aplicativo. Se você adiciona JavaScript a sites, que recarregam ao navegar, não precisa se preocupar muito com o consumo de memória. Com aplicativos de página única, você precisa se preocupar. Se você não limpar, os usuários que passarem mais do que alguns minutos no seu aplicativo poderão enfrentar problemas de desempenho crescentes.

Razão 3: A limpeza não é realmente a coisa mais fácil de fazer e analisar. Não há como chamar um coletor de lixo do script (no navegador). Você pode acabar com árvores DOM desanexadas. Eu criei um exemplo (jQuery é carregado em 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);

Esta é uma diretiva simples que gera algum texto. Há um botão abaixo dele, que apenas destruirá a diretiva manualmente.

Assim, quando a diretiva é removida, permanece uma referência à árvore DOM em scope.toBeDetached. Nas ferramentas de desenvolvimento do chrome, se você acessar a aba “perfis” e depois “tirar instantâneo de heap”, você verá na saída:

Você pode viver com alguns, mas é ruim se você tiver uma tonelada. Especialmente se por algum motivo, como no exemplo, você armazená-lo no escopo. Todo o DOM será avaliado em cada resumo. A árvore DOM desanexada problemática é aquela com 4 nós. Então, como isso pode ser resolvido?

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

A árvore DOM separada com 4 entradas foi removida!

Neste exemplo, a diretiva usa o mesmo escopo e armazena o elemento DOM no escopo. Foi mais fácil para mim demonstrar dessa forma. Nem sempre fica tão ruim, pois você pode armazená-lo em uma variável. No entanto, ainda ocuparia memória se qualquer encerramento que tivesse referenciado essa variável ou qualquer outra do mesmo escopo de função permanecesse.

Erro comum nº 11: uso excessivo do escopo isolado

Sempre que você precisar de uma diretiva que você sabe que será usada em um único lugar, ou que você não espera entrar em conflito com qualquer ambiente em que ela seja usada, não há necessidade de usar escopo isolado. Ultimamente, há uma tendência de criar componentes reutilizáveis, mas você sabia que as diretivas angulares principais não usam escopo isolado?

Existem duas razões principais: você não pode aplicar duas diretivas de escopo isoladas a um elemento e pode encontrar problemas com aninhamento/herança/processamento de eventos. Especialmente em relação à transclusão - os efeitos podem não ser os esperados.

Então isso falharia:

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

E mesmo se você usar apenas uma diretiva, você notará que nem os modelos de escopo isolados nem os eventos transmitidos em isolatedScopeDirective não estarão disponíveis para AnotherController. Isso sendo triste, você pode flexionar e usar a magia de transclusão para fazê-lo funcionar - mas para a maioria dos casos de uso, não há necessidade de isolar.

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

Então, duas perguntas agora:

  1. Como você pode processar modelos de escopo pai em uma diretiva de mesmo escopo?
  2. Como você pode instanciar novos valores de modelo?

Existem duas maneiras, em ambas você passa valores para atributos. Considere este 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]; }

Que controla esta visão:

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

Observe que “watch-attribute” não é interpolado. Tudo funciona, devido à magia JS. Aqui está a definição da diretiva:

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

Observe que attrs.watchAttribute é passado para scope.$watch() sem as aspas! Isso significa que o que foi realmente passado para $watch foi a string MC.foo ! Ele funciona, no entanto, porque qualquer string passada para $watch() é avaliada em relação ao escopo e MC.foo está disponível no escopo. Essa também é a maneira mais comum de os atributos serem observados nas diretivas principais do AngularJS.

Veja o código no github para o modelo e veja $parse e $eval para ainda mais incrível.

Erro comum nº 12: não limpar depois de si mesmo - observadores, intervalos, tempos limite e variáveis

AngularJS faz algum trabalho em seu nome, mas não tudo. O seguinte precisa ser limpo manualmente:

  • Quaisquer observadores que não estejam vinculados ao escopo atual (por exemplo, vinculados a $rootScope)
  • Intervalos
  • Tempos limite
  • Variáveis ​​que fazem referência ao DOM em diretivas
  • Plugins jQuery desonestos, por exemplo, aqueles que não têm manipuladores reagindo ao evento JavaScript $destroy

Se você não fizer isso manualmente, encontrará um comportamento inesperado e vazamentos de memória. Pior ainda - eles não serão instantaneamente visíveis, mas eventualmente aparecerão. Lei de Murphy.

Surpreendentemente, o AngularJS fornece maneiras práticas de lidar com todos eles:

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

Observe o evento $destroy do jQuery. É chamado como o AngularJS, mas é tratado separadamente. Scope $watchers não reagirão ao evento jQuery.

Erro comum nº 13: manter muitos observadores

Isso deve ser bem simples agora. Há uma coisa para entender aqui: $digest() . Para cada ligação {{ model }} , o AngularJS cria um watcher. Em cada fase de digestão, cada uma dessas ligações é avaliada e comparada com o valor anterior. Isso é chamado de verificação suja, e é isso que o $digest faz. Se o valor mudou desde a última verificação, o retorno de chamada do inspetor é acionado. 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.

Primeiro, inicie seu servidor web no host 0.0.0.0 para que seja acessível a partir de sua rede local. Ative o inspetor da web nas configurações. Em seguida, conecte seu dispositivo ao seu desktop e acesse sua página de desenvolvimento local, usando o ip da sua máquina em vez do “localhost” normal. Isso é tudo o que precisa, seu dispositivo agora deve estar disponível para você no navegador da sua área de trabalho.

Aqui estão as instruções detalhadas para Android E para iOS, guias não oficiais podem ser encontrados facilmente através do google.

Recentemente, tive uma experiência legal com o browserSync. Ele funciona de maneira semelhante ao livereload, mas também sincroniza todos os navegadores que estão visualizando a mesma página por meio do browserSync. Isso inclui a interação do usuário, como rolar, clicar em botões, etc. Eu estava olhando para a saída de log do aplicativo iOS enquanto controlava a página no iPad da minha área de trabalho. Funcionou bem!

Erro comum nº 18: não ler o código-fonte no exemplo NG-INIT

Ng-init , pelo som dele, deve ser semelhante a ng-if e ng-repeat , certo? Você já se perguntou por que há um comentário nos documentos que não deve ser usado? IMHO que foi surpreendente! Eu esperaria que a diretiva inicializasse um modelo. Também é isso que ele faz, mas... ele é implementado de uma forma diferente, ou seja, não observa o valor do atributo. Você não precisa navegar pelo código-fonte do AngularJS - deixe-me trazê-lo para você:

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

Menos do que você esperaria? Bastante legível, além da sintaxe de diretiva estranha, não é? A sexta linha é o que está em causa.

Compare com 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 }); }); } }; }];

Novamente, a sexta linha. Há um $watch lá, é isso que torna essa diretiva dinâmica. No código-fonte do AngularJS, uma grande parte de todo o código são comentários que descrevem o código que era mais legível desde o início. Eu acredito que é uma ótima maneira de aprender sobre AngularJS.

Conclusão

Este guia que cobre os erros mais comuns do AngularJS é quase o dobro dos outros guias. Ficou assim naturalmente. A demanda por engenheiros de front-end JavaScript de alta qualidade é muito alta. O AngularJS está em alta no momento e mantém uma posição estável entre as ferramentas de desenvolvimento mais populares há alguns anos. Com o AngularJS 2.0 a caminho, ele provavelmente dominará nos próximos anos.

O que é ótimo no desenvolvimento front-end é que ele é muito gratificante. Nosso trabalho é visível instantaneamente e as pessoas interagem diretamente com os produtos que entregamos. O tempo gasto aprendendo JavaScript, e acredito que devemos focar na linguagem JavaScript, é um investimento muito bom. É a linguagem da Internet. A concorrência é super forte! Há um foco para nós - a experiência do usuário. Para ter sucesso, precisamos cobrir tudo.

O código-fonte usado nesses exemplos pode ser baixado do GitHub. Sinta-se à vontade para baixá-lo e torná-lo seu.

Eu queria dar créditos a quatro desenvolvedores de publicações que mais me inspiraram:

  • Ben Nadel
  • Lema de Todd
  • Pascal Precht
  • Panda Sandeep

Eu também queria agradecer a todas as ótimas pessoas nos canais #angularjs e #javascript do FreeNode por muitas conversas excelentes e suporte contínuo.

E por fim, lembre-se sempre:

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