Os aplicativos de página única exigem que os desenvolvedores de front-end se tornem melhores engenheiros de software. CSS e HTML não são mais a maior preocupação, na verdade, não há mais apenas uma preocupação. O desenvolvedor front-end precisa lidar com XHRs, lógica de aplicativo (modelos, visualizações, controladores), desempenho, animações, estilos, estrutura, SEO e integração com serviços externos. O resultado que emerge de todos aqueles combinados é a Experiência do Usuário (UX) que deve ser sempre priorizada.
AngularJS é uma estrutura muito poderosa. É o terceiro repositório mais marcado no GitHub. Não é difícil de começar a usar, mas os objetivos que se pretende alcançar 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 de desenvolvimento web . Vamos abraçar!
Existem alguns ajustes de otimização recomendados para produção. Um deles está desabilitando as informações de depuração.
DebugInfoEnabled
é uma configuração cujo padrão é verdadeiro 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 falso, chamando .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 em produção. Ligar para angular.reloadWithDebugInfo()
do console e o aplicativo fará exatamente isso.
Você provavelmente já leu isso se estivesse não ter um ponto no seu modelo ng , você estava 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 cada escopo 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 mudança for para uma propriedade de um objeto de modelo, a pesquisa nos escopos pai encontrará o objeto referenciado e mudará 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);
OUTER SCOPE:
{{ foo }}
{{ bar.innerProperty }}
INNER SCOPE
{{ foo }}
{{ bar.innerProperty }}
Set primitive Mutate object
Clicar no botão rotulado “Definir primitivo” definirá foo no escopo interno para 2, mas não mudará foo no escopo externo.
Clicar no botão denominado “Alterar objeto” mudará a propriedade da barra do escopo pai. Como não há variável no escopo interno, nenhum sombreamento acontecerá e o valor visível para a barra 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 cada escopo. O $parent
e $root
os objetos podem ser usados para acessar o escopo pai e $rootScope
, respectivamente, diretamente da visualização. Pode ser uma maneira poderosa, mas não sou fã dela devido ao problema de direcionar um determinado escopo rio acima. Há outra maneira de definir e acessar propriedades específicas de um escopo - usando o controllerAs
sintaxe.
A forma alternativa e mais eficiente de atribuir modelos para usar um objeto controlador em vez do escopo $ 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() }
OUTER SCOPE:
{{ MC.foo }}
{{ MC.bar.someProperty }}
INNER SCOPE
{{ MC.foo }}
{{ MC.bar.someProperty }}
Change MC.foo Change MC.bar.someProperty
Isso é muito menos confuso. Especialmente quando há muitos escopos aninhados, como pode ser o caso com estados aninhados.
Há mais coisas na sintaxe do controllerAs.
Existem algumas ressalvas sobre como o objeto 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 não será necessá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 apenas fazer:
function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }
O que significa que também abaixo da 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 fazer isso, você precisa ser consistente com a sigla que usa para controllerAs. Existem pelo menos três maneiras de configurá-lo. Você já viu o primeiro:
…
No entanto, se você usar ui-router
, especificar um controlador dessa forma poderá gerar 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 () { … });
Existe outra maneira de fazer anotações:
(…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })
Você pode fazer o mesmo nas diretivas:
function AnotherController() { this.text = 'abc'; } function testForApeeScape() { return { controller: 'AnotherController as AC', template: '{{ AC.text }}
' }; } angular.module('myApp', []) .controller('AnotherController', AnotherController) .directive('testForApeeScape', testForApeeScape);
A outra forma de fazer anotações também é válida, embora menos concisa:
function testForApeeScape() { return { controller: 'AnotherController', controllerAs: 'AC', template: '{{ AC.text }}
' }; }
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 um roteamento mais sofisticado.
Há um novo NgRouter
está a caminho, mas os autores ainda consideram que é muito cedo para a produção. Quando estou escrevendo isto, o Angular estável é 1.3.15 e ui-router
rochas.
Os principais motivos:
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 visão da página inicial e uma visão do produto. A visualização do produto possui 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 os estados. Mas o conteúdo deve ser recarregado.
Considere a seguinte estrutura de página de índice de produto HTML:
SOME PRODUCT SPECIFIC INTRO
Product title
Context-specific content
Isso é algo que poderíamos obter do codificador HTML e agora precisamos separá-lo em arquivos e estados. Geralmente, aceito 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 a Principal state também manterá o HTML estático exigido em todas as páginas. Eu mantenho index.html limpo.
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
Então, vamos ver a página de índice do produto:
main.product.details
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 às especificações! Então, agora vamos configurar o roteamento:
ui-router
Essa seria a primeira abordagem. Agora, o que acontece ao alternar entre // 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' } } });
e $urlRouterProvider.deferIntercept()
? O conteúdo e o widget são recarregados, mas queremos apenas recarregar o conteúdo. Isso era problemático, e os desenvolvedores criaram roteadores que suportariam apenas essa funcionalidade. Um dos nomes para isso era visões pegajosas . Felizmente, '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);
suporta isso fora da caixa com segmentação de visualização com nome absoluto .
publicMethod1
Ao mover a definição de estado para a visualização pai, que também é abstrata, podemos evitar que a visualização filho seja recarregada ao trocar de urls, o que normalmente afeta os irmãos daquela criança. Obviamente, o widget pode ser uma diretiva simples. Mas a questão é que também pode ser outro estado aninhado complexo.
Há outra maneira de fazer isso usando 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; }
, mas acho que usar a configuração de estado é realmente melhor. Se você estiver interessado em interceptar rotas, escrevi um pequeno tutorial sobre StackOverflow .
este erro é de um calibre mais leve e é mais uma questão de estilo do que evitar mensagens de erro do AngularJS. Você deve ter notado que raramente passo funções anônimas para declarações internas do angular. Normalmente, apenas defino uma função primeiro e depois a transmito.
Isso diz respeito a mais do que apenas funções. Eu peguei essa abordagem lendo guias de estilo, especialmente do Airbnb e do Todd Motto. Acredito que haja várias vantagens e quase nenhuma desvantagem.
Em primeiro lugar, você pode manipular e modificar 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 sustentabilidade. Se você não quiser poluir o namespace global, envolva todos os arquivos em IIFEs. A terceira razão é a testabilidade. Considere este exemplo:
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);
Portanto, agora podemos zombar do 'use strict'; function scoringFunction(items, weights) { var itemsArray = []; for (var i = 0; i b.sum) { return -1; } else if (a.sum
scoringService.scoreItems()
Não se trata apenas de estilo, pois, na verdade, o código é mais reutilizável e idiomático. O desenvolvedor ganha mais poder expressivo. Dividir todo o código em blocos autocontidos torna tudo mais fácil.
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 de exibição de dados é fundamental. E como o JavaScript é de thread único, é relativamente fácil congelar o navegador.
Também é fácil evitá-lo com web workers. Não parece haver nenhuma biblioteca popular que lida com isso especificamente para AngularJS. No entanto, pode ser o melhor, já que a implementação é fácil.
Primeiro, vamos configurar o serviço:
.toString()
Agora, o trabalhador:
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) }); }
Agora, injete o serviço normalmente e trate Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed
como faria com qualquer método de serviço que retorna uma promessa. O processamento pesado será realizado em uma thread separada e nenhum dano será causado à UX.
O que procurar:
.run()
na propriedade passada e funcionou corretamente.Resolve adicionar tempo extra ao carregamento da visualização. Acredito que o alto desempenho do aplicativo front-end é nosso objetivo principal. Não deve ser um problema renderizar algumas partes da visualização enquanto o aplicativo aguarda os dados da API.
Considere esta configuração:
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'); } } });
A saída do console será:
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); } });
O que basicamente significa que:
Isso significa que antes que o usuário veja qualquer saída, ele deve esperar por todas as dependências. Precisamos ter esses dados, certo, ok. Se for absolutamente necessário tê-lo antes da visualização, coloque-o em um $digest()
quadra. Caso contrário, basta fazer a chamada para o serviço a partir do controlador e lidar com o estado de meia carga normalmente. Ver o trabalho em andamento - e o controlador já executado, portanto, está em andamento - é melhor do que deixar o aplicativo travar.
a) Causando muitos loops de resumo, como anexar controles deslizantes aos modelos
Este é um problema geral que pode resultar em erros do AngularJS, mas vou discuti-lo no exemplo dos controles deslizantes. Eu estava usando esta biblioteca de controle deslizante, controle deslizante de intervalo angular, porque precisava da funcionalidade estendida. Essa diretiva tem esta sintaxe na versão mínima:
ng-click
Considere o seguinte código no controlador:
input
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 do modelo real em todos os casos.
Então, vamos adicionar um modelo temporário vinculado para alterar o modelo de trabalho no tempo limite:
$timeout
e no controlador:
$http
b) Não usando $ applyAsync
O AngularJS não possui um mecanismo de pesquisa para chamar $watch
. Ele só é executado porque usamos as diretivas (por exemplo, .$applyAsync()
, $digest()
), serviços (applyAsync
, $http
) e métodos (mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });
) que avaliam nosso código e chame um resumo depois.
O que .click()
faz é atrasar a resolução das expressões até o próximo $apply()
ciclo, que é acionado após um tempo limite 0, que na verdade é de aproximadamente 10 ms.
Existem duas maneiras de usar $scope.$root.$digest()
agora. Uma forma automatizada para $rootScope.$digest()
pedidos, e uma forma manual para o resto.
Para que todas as solicitações http que retornam ao mesmo tempo sejam resolvidas em um resumo, faça:
$scope.$digest()
A forma manual mostra como realmente funciona. Considere alguma função que é executada no retorno de chamada para um ouvinte de evento JS vanilla ou um jQuery I AM DIRECTIVE$scope.$applyAsync()
, ou alguma outra biblioteca externa. Depois de executar e alterar os modelos, se você ainda não o embrulhou em um $digest()
você precisa ligar para remove directive
(function MainController($rootScope, $scope) { this.removeDirective = function () { $rootScope.$emit('destroyDirective'); }; } function testForApeeScape($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: '
), ou pelo menos scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });
. Caso contrário, você não verá nenhuma mudança.
Se você fizer isso várias vezes em um fluxo, ele pode começar a ficar lento. Considere ligar para … the isolated scope is not available here, look: {{ isolatedModel }}
nas expressões em vez disso. Ele definirá apenas um ciclo de resumo para todos eles.
c) Fazendo processamento pesado de imagens
Se tiver um desempenho ruim, você pode investigar o motivo usando a linha do tempo das Ferramentas de desenvolvedor do Chrome. Escreverei mais sobre essa ferramenta no erro nº 17. Se o gráfico da linha do tempo for 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 além dos problemas de desempenho do AngularJS (que seriam principalmente amarelos no gráfico). Como engenheiros front-end, precisamos pensar sobre o projeto final completo.
Reserve um momento para avaliar:
Se você respondeu “sim” a pelo menos três das opções acima, considere facilitar. Talvez você possa servir a vários tamanhos de imagem e não redimensionar nada. Talvez você pudesse adicionar o hack de processamento de GPU de força “transform: translateZ (0)”. Ou use requestAnimationFrame para manipuladores.
Muitas vezes você provavelmente já ouviu que não é recomendado usar jQuery com AngularJS e que deve ser evitado. É imperativo entender o motivo por trás dessas declarações. Existem pelo menos três razões, pelo que posso ver, mas nenhuma delas é um bloqueador real.
Razão 1: Ao executar o código jQuery, você precisa chamar 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]; }
você mesmo. Para muitos casos, existe um Solução AngularJS que é adaptado para AngularJS e pode ser mais útil dentro do Angular do que no 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ê tem adicionado JavaScript a sites, que são recarregados durante a navegação, não precisa se preocupar com o consumo excessivo de memória. Com aplicativos de página única, você precisa se preocupar. Se você não limpar, os usuários que gastam mais do que alguns minutos em seu aplicativo podem enfrentar problemas crescentes de desempenho.
Razão 3: Limpar não é realmente a coisa mais fácil de fazer e analisar. Não há como chamar um coletor de lixo a partir do script (no navegador). Você pode acabar com árvores DOM separadas. Eu criei um exemplo (jQuery é carregado em index.html):
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' }; }
Esta é uma diretiva simples que produz algum texto. Há um botão abaixo dele, que destruirá a diretiva manualmente.
Portanto, quando a diretiva é removida, permanece uma referência à árvore DOM em scope.toBeDetached. No chrome dev tools, se você acessar a guia “profiles” e “take heap snapshot”, você verá na saída:
Você pode viver com alguns, mas é ruim se você tiver uma tonelada. Principalmente se por algum motivo, como no exemplo, você o armazenar no osciloscópio. Todo o DOM será avaliado em cada resumo. A árvore DOM problemática separada é aquela com 4 nós. Então, como isso pode ser resolvido?
attrs.watchAttribute
A árvore DOM desanexada 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, já que você poderia armazená-lo em uma variável. No entanto, ainda ocuparia memória se qualquer fechamento que fizesse referência a essa variável ou qualquer outro do mesmo escopo de função vivesse.
Sempre que você precisar de uma diretiva que sabe que será usada em um único lugar, ou que você não espera que entre em conflito com qualquer ambiente em que seja usada, não há necessidade de usar o escopo isolado. Ultimamente, há uma tendência de criar componentes reutilizáveis, mas você sabia que as diretivas angulares centrais não usam escopo isolado?
Há dois motivos principais: você não pode aplicar duas diretivas de escopo isoladas a um elemento e pode encontrar problemas com processamento de aninhamento / herança / evento. Especialmente em relação à transclusão - os efeitos podem não ser os esperados.
Portanto, isso iria falhar:
scope.$watch()
E mesmo se você usar apenas uma diretiva, notará que nem os modelos de escopo isolados nem os eventos transmitidos em isolatedScopeDirective não estarão disponíveis para AnotherController. Isso é triste, você pode flexibilizar e usar a magia de transclusão para fazê-la funcionar - mas para a maioria dos casos de uso, não há necessidade de isolar.
MC.foo
Então, duas perguntas agora:
Existem duas maneiras, em ambas você passa valores para atributos. Considere este MainController:
$watch()
Isso controla esta visualização:
MC.foo
Observe que “watch-attribute” não é interpolado. Tudo funciona, devido à magia do JS. Aqui está a definição da diretiva:
$parse
Observe que $eval
é passado para $destroy
sem as aspas! Isso significa que o que foi realmente passado para $ watch foi a string 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. }); };
! Funciona, entretanto, porque qualquer string passada para $destroy
é avaliado em relação ao escopo e $digest()
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 dê uma olhada em {{ model }}
e
para ainda mais incrível.
O AngularJS faz algum trabalho em seu nome, mas não todo. O seguinte deve ser limpo manualmente:
vastArray
eventoSe você não fizer isso manualmente, encontrará um comportamento inesperado e vazamentos de memória. Pior ainda - eles não ficarão visíveis imediatamente, mas irão aparecer eventualmente. Lei de Murphy.
Surpreendentemente, o AngularJS fornece maneiras úteis de lidar com todos esses:
item.velocity
Observe o jQuery {{ someModel }}
evento. É chamado como o AngularJS, mas é tratado separadamente. Os observadores do escopo $ não reagirão ao evento jQuery.
Isso deve ser bastante simples agora. Há uma coisa a entender aqui: ng-if
. Para cada ligação ng-repeat
, AngularJS cria um inspetor. Em cada fase de digestão, cada ligação é avaliada e comparada com o valor anterior. Isso é chamado de verificação suja, e é isso que $ digest faz. Se o valor mudou desde a última verificação, o callback do inspetor é acionado. Se esse retorno de chamada do inspetor modificar um modelo (variável $ scope), um novo ciclo de $ digest é disparado (até um máximo de 10) quando uma exceção é lançada.
Os navegadores não têm problemas mesmo com milhares de ligações, a menos que as expressões sejam complexas. A resposta comum para “quantos observadores podem ter” é 2.000.
Então, como podemos limitar o número de observadores? Por não observar os modelos de escopo quando não esperamos que eles mudem. É bastante fácil partir do AngularJS 1.3, uma vez que as ligações únicas estão no núcleo agora.
$watch()
Depois de $Watchers
e $watch()
são avaliados uma vez, eles nunca mudarão novamente. Você ainda pode aplicar filtros ao array, eles funcionarão muito bem. Acontece que o próprio array não será avaliado. Em muitos casos, é uma vitória.
Este erro do AngularJS já foi parcialmente coberto pelos erros 9.be em 13. Esta é uma explicação mais completa. O AngularJS atualiza o DOM como resultado de funções de retorno de chamada para observadores. Cada ligação, essa é a diretiva $rootScope.$digest()
configura observadores, mas os observadores também são configurados para muitas outras diretivas como $scope
e $watch()
. Basta dar uma olhada no código-fonte, é muito legível. Os observadores também podem ser configurados manualmente, e você provavelmente já fez isso pelo menos algumas vezes.
$digest()
ers estão vinculados a escopos. .$apply
pode receber cadeias de caracteres, que são avaliadas em relação ao escopo de .$applyAsync
foi obrigado a. Eles também podem avaliar funções. E eles também aceitam chamadas de retorno. Portanto, quando .$evalAsync
é chamado, todos os modelos registrados (ou seja, $digest()
variáveis) são avaliados e comparados com seus valores anteriores. Se os valores não corresponderem, o retorno de chamada para o $digest()
É executado.
É importante entender que, embora o valor de um modelo tenha sido alterado, o retorno de chamada não dispara até a próxima fase de resumo. É chamada de “fase” por uma razão - pode consistir em vários ciclos de digestão. Se apenas um observador altera um modelo de escopo, outro ciclo de resumo é executado.
Mas describe('some module', function () { it('should call the name-it service…', function () { // leave this empty for now }); ... });
não é pesquisado para . É chamado a partir de diretivas, serviços, métodos principais etc. Se você alterar um modelo de uma função personalizada que não chama '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);
, console.log()
, debugInfo
ou qualquer outra coisa que eventualmente chame $(document.body).scope().$root
, as ligações não serão atualizadas.
A propósito, o código-fonte de $(
18 erros mais comuns cometidos pelos desenvolvedores AngularJS
Os aplicativos de página única exigem que os desenvolvedores de front-end se tornem melhores engenheiros de software. CSS e HTML não são mais a maior preocupação, na verdade, não há mais apenas uma preocupação. O desenvolvedor front-end precisa lidar com XHRs, lógica de aplicativo (modelos, visualizações, controladores), desempenho, animações, estilos, estrutura, SEO e integração com serviços externos. O resultado que emerge de todos aqueles combinados é a Experiência do Usuário (UX) que deve ser sempre priorizada.
AngularJS é uma estrutura muito poderosa. É o terceiro repositório mais marcado no GitHub. Não é difícil de começar a usar, mas os objetivos que se pretende alcançar 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 de desenvolvimento web . Vamos abraçar!
Existem alguns ajustes de otimização recomendados para produção. Um deles está desabilitando as informações de depuração.
DebugInfoEnabled
é uma configuração cujo padrão é verdadeiro 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 falso, chamando .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 em produção. Ligar para angular.reloadWithDebugInfo()
do console e o aplicativo fará exatamente isso.
Você provavelmente já leu isso se estivesse não ter um ponto no seu modelo ng , você estava 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 cada escopo 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 mudança for para uma propriedade de um objeto de modelo, a pesquisa nos escopos pai encontrará o objeto referenciado e mudará 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);
OUTER SCOPE:
{{ foo }}
{{ bar.innerProperty }}
INNER SCOPE
{{ foo }}
{{ bar.innerProperty }}
Set primitive Mutate object
Clicar no botão rotulado “Definir primitivo” definirá foo no escopo interno para 2, mas não mudará foo no escopo externo.
Clicar no botão denominado “Alterar objeto” mudará a propriedade da barra do escopo pai. Como não há variável no escopo interno, nenhum sombreamento acontecerá e o valor visível para a barra 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 cada escopo. O $parent
e $root
os objetos podem ser usados para acessar o escopo pai e $rootScope
, respectivamente, diretamente da visualização. Pode ser uma maneira poderosa, mas não sou fã dela devido ao problema de direcionar um determinado escopo rio acima. Há outra maneira de definir e acessar propriedades específicas de um escopo - usando o controllerAs
sintaxe.
A forma alternativa e mais eficiente de atribuir modelos para usar um objeto controlador em vez do escopo $ 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() }
OUTER SCOPE:
{{ MC.foo }}
{{ MC.bar.someProperty }}
INNER SCOPE
{{ MC.foo }}
{{ MC.bar.someProperty }}
Change MC.foo Change MC.bar.someProperty
Isso é muito menos confuso. Especialmente quando há muitos escopos aninhados, como pode ser o caso com estados aninhados.
Há mais coisas na sintaxe do controllerAs.
Existem algumas ressalvas sobre como o objeto 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 não será necessá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 apenas fazer:
function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }
O que significa que também abaixo da 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 fazer isso, você precisa ser consistente com a sigla que usa para controllerAs. Existem pelo menos três maneiras de configurá-lo. Você já viu o primeiro:
…
No entanto, se você usar ui-router
, especificar um controlador dessa forma poderá gerar 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 () { … });
Existe outra maneira de fazer anotações:
(…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })
Você pode fazer o mesmo nas diretivas:
function AnotherController() { this.text = 'abc'; } function testForApeeScape() { return { controller: 'AnotherController as AC', template: '{{ AC.text }}
' }; } angular.module('myApp', []) .controller('AnotherController', AnotherController) .directive('testForApeeScape', testForApeeScape);
A outra forma de fazer anotações também é válida, embora menos concisa:
function testForApeeScape() { return { controller: 'AnotherController', controllerAs: 'AC', template: '{{ AC.text }}
' }; }
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 um roteamento mais sofisticado.
Há um novo NgRouter
está a caminho, mas os autores ainda consideram que é muito cedo para a produção. Quando estou escrevendo isto, o Angular estável é 1.3.15 e ui-router
rochas.
Os principais motivos:
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 visão da página inicial e uma visão do produto. A visualização do produto possui 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 os estados. Mas o conteúdo deve ser recarregado.
Considere a seguinte estrutura de página de índice de produto HTML:
SOME PRODUCT SPECIFIC INTRO
Product title
Context-specific content
Isso é algo que poderíamos obter do codificador HTML e agora precisamos separá-lo em arquivos e estados. Geralmente, aceito 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 a Principal state também manterá o HTML estático exigido em todas as páginas. Eu mantenho index.html limpo.
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
Então, vamos ver a página de índice do produto:
main.product.details
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 às especificações! Então, agora vamos configurar o roteamento:
ui-router
Essa seria a primeira abordagem. Agora, o que acontece ao alternar entre // 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' } } });
e $urlRouterProvider.deferIntercept()
? O conteúdo e o widget são recarregados, mas queremos apenas recarregar o conteúdo. Isso era problemático, e os desenvolvedores criaram roteadores que suportariam apenas essa funcionalidade. Um dos nomes para isso era visões pegajosas . Felizmente, '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);
suporta isso fora da caixa com segmentação de visualização com nome absoluto .
publicMethod1
Ao mover a definição de estado para a visualização pai, que também é abstrata, podemos evitar que a visualização filho seja recarregada ao trocar de urls, o que normalmente afeta os irmãos daquela criança. Obviamente, o widget pode ser uma diretiva simples. Mas a questão é que também pode ser outro estado aninhado complexo.
Há outra maneira de fazer isso usando 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; }
, mas acho que usar a configuração de estado é realmente melhor. Se você estiver interessado em interceptar rotas, escrevi um pequeno tutorial sobre StackOverflow .
este erro é de um calibre mais leve e é mais uma questão de estilo do que evitar mensagens de erro do AngularJS. Você deve ter notado que raramente passo funções anônimas para declarações internas do angular. Normalmente, apenas defino uma função primeiro e depois a transmito.
Isso diz respeito a mais do que apenas funções. Eu peguei essa abordagem lendo guias de estilo, especialmente do Airbnb e do Todd Motto. Acredito que haja várias vantagens e quase nenhuma desvantagem.
Em primeiro lugar, você pode manipular e modificar 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 sustentabilidade. Se você não quiser poluir o namespace global, envolva todos os arquivos em IIFEs. A terceira razão é a testabilidade. Considere este exemplo:
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);
Portanto, agora podemos zombar do 'use strict'; function scoringFunction(items, weights) { var itemsArray = []; for (var i = 0; i b.sum) { return -1; } else if (a.sum
scoringService.scoreItems()
Não se trata apenas de estilo, pois, na verdade, o código é mais reutilizável e idiomático. O desenvolvedor ganha mais poder expressivo. Dividir todo o código em blocos autocontidos torna tudo mais fácil.
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 de exibição de dados é fundamental. E como o JavaScript é de thread único, é relativamente fácil congelar o navegador.
Também é fácil evitá-lo com web workers. Não parece haver nenhuma biblioteca popular que lida com isso especificamente para AngularJS. No entanto, pode ser o melhor, já que a implementação é fácil.
Primeiro, vamos configurar o serviço:
.toString()
Agora, o trabalhador:
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) }); }
Agora, injete o serviço normalmente e trate Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed
como faria com qualquer método de serviço que retorna uma promessa. O processamento pesado será realizado em uma thread separada e nenhum dano será causado à UX.
O que procurar:
.run()
na propriedade passada e funcionou corretamente.Resolve adicionar tempo extra ao carregamento da visualização. Acredito que o alto desempenho do aplicativo front-end é nosso objetivo principal. Não deve ser um problema renderizar algumas partes da visualização enquanto o aplicativo aguarda os dados da API.
Considere esta configuração:
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'); } } });
A saída do console será:
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); } });
O que basicamente significa que:
Isso significa que antes que o usuário veja qualquer saída, ele deve esperar por todas as dependências. Precisamos ter esses dados, certo, ok. Se for absolutamente necessário tê-lo antes da visualização, coloque-o em um $digest()
quadra. Caso contrário, basta fazer a chamada para o serviço a partir do controlador e lidar com o estado de meia carga normalmente. Ver o trabalho em andamento - e o controlador já executado, portanto, está em andamento - é melhor do que deixar o aplicativo travar.
a) Causando muitos loops de resumo, como anexar controles deslizantes aos modelos
Este é um problema geral que pode resultar em erros do AngularJS, mas vou discuti-lo no exemplo dos controles deslizantes. Eu estava usando esta biblioteca de controle deslizante, controle deslizante de intervalo angular, porque precisava da funcionalidade estendida. Essa diretiva tem esta sintaxe na versão mínima:
ng-click
Considere o seguinte código no controlador:
input
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 do modelo real em todos os casos.
Então, vamos adicionar um modelo temporário vinculado para alterar o modelo de trabalho no tempo limite:
$timeout
e no controlador:
$http
b) Não usando $ applyAsync
O AngularJS não possui um mecanismo de pesquisa para chamar $watch
. Ele só é executado porque usamos as diretivas (por exemplo, .$applyAsync()
, $digest()
), serviços (applyAsync
, $http
) e métodos (mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });
) que avaliam nosso código e chame um resumo depois.
O que .click()
faz é atrasar a resolução das expressões até o próximo $apply()
ciclo, que é acionado após um tempo limite 0, que na verdade é de aproximadamente 10 ms.
Existem duas maneiras de usar $scope.$root.$digest()
agora. Uma forma automatizada para $rootScope.$digest()
pedidos, e uma forma manual para o resto.
Para que todas as solicitações http que retornam ao mesmo tempo sejam resolvidas em um resumo, faça:
$scope.$digest()
A forma manual mostra como realmente funciona. Considere alguma função que é executada no retorno de chamada para um ouvinte de evento JS vanilla ou um jQuery I AM DIRECTIVE$scope.$applyAsync()
, ou alguma outra biblioteca externa. Depois de executar e alterar os modelos, se você ainda não o embrulhou em um $digest()
você precisa ligar para remove directive
(function MainController($rootScope, $scope) { this.removeDirective = function () { $rootScope.$emit('destroyDirective'); }; } function testForApeeScape($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: '
), ou pelo menos scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });
. Caso contrário, você não verá nenhuma mudança.
Se você fizer isso várias vezes em um fluxo, ele pode começar a ficar lento. Considere ligar para … the isolated scope is not available here, look: {{ isolatedModel }}
nas expressões em vez disso. Ele definirá apenas um ciclo de resumo para todos eles.
c) Fazendo processamento pesado de imagens
Se tiver um desempenho ruim, você pode investigar o motivo usando a linha do tempo das Ferramentas de desenvolvedor do Chrome. Escreverei mais sobre essa ferramenta no erro nº 17. Se o gráfico da linha do tempo for 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 além dos problemas de desempenho do AngularJS (que seriam principalmente amarelos no gráfico). Como engenheiros front-end, precisamos pensar sobre o projeto final completo.
Reserve um momento para avaliar:
Se você respondeu “sim” a pelo menos três das opções acima, considere facilitar. Talvez você possa servir a vários tamanhos de imagem e não redimensionar nada. Talvez você pudesse adicionar o hack de processamento de GPU de força “transform: translateZ (0)”. Ou use requestAnimationFrame para manipuladores.
Muitas vezes você provavelmente já ouviu que não é recomendado usar jQuery com AngularJS e que deve ser evitado. É imperativo entender o motivo por trás dessas declarações. Existem pelo menos três razões, pelo que posso ver, mas nenhuma delas é um bloqueador real.
Razão 1: Ao executar o código jQuery, você precisa chamar 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]; }
você mesmo. Para muitos casos, existe um Solução AngularJS que é adaptado para AngularJS e pode ser mais útil dentro do Angular do que no 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ê tem adicionado JavaScript a sites, que são recarregados durante a navegação, não precisa se preocupar com o consumo excessivo de memória. Com aplicativos de página única, você precisa se preocupar. Se você não limpar, os usuários que gastam mais do que alguns minutos em seu aplicativo podem enfrentar problemas crescentes de desempenho.
Razão 3: Limpar não é realmente a coisa mais fácil de fazer e analisar. Não há como chamar um coletor de lixo a partir do script (no navegador). Você pode acabar com árvores DOM separadas. Eu criei um exemplo (jQuery é carregado em index.html):
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' }; }
Esta é uma diretiva simples que produz algum texto. Há um botão abaixo dele, que destruirá a diretiva manualmente.
Portanto, quando a diretiva é removida, permanece uma referência à árvore DOM em scope.toBeDetached. No chrome dev tools, se você acessar a guia “profiles” e “take heap snapshot”, você verá na saída:
Você pode viver com alguns, mas é ruim se você tiver uma tonelada. Principalmente se por algum motivo, como no exemplo, você o armazenar no osciloscópio. Todo o DOM será avaliado em cada resumo. A árvore DOM problemática separada é aquela com 4 nós. Então, como isso pode ser resolvido?
attrs.watchAttribute
A árvore DOM desanexada 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, já que você poderia armazená-lo em uma variável. No entanto, ainda ocuparia memória se qualquer fechamento que fizesse referência a essa variável ou qualquer outro do mesmo escopo de função vivesse.
Sempre que você precisar de uma diretiva que sabe que será usada em um único lugar, ou que você não espera que entre em conflito com qualquer ambiente em que seja usada, não há necessidade de usar o escopo isolado. Ultimamente, há uma tendência de criar componentes reutilizáveis, mas você sabia que as diretivas angulares centrais não usam escopo isolado?
Há dois motivos principais: você não pode aplicar duas diretivas de escopo isoladas a um elemento e pode encontrar problemas com processamento de aninhamento / herança / evento. Especialmente em relação à transclusão - os efeitos podem não ser os esperados.
Portanto, isso iria falhar:
scope.$watch()
E mesmo se você usar apenas uma diretiva, notará que nem os modelos de escopo isolados nem os eventos transmitidos em isolatedScopeDirective não estarão disponíveis para AnotherController. Isso é triste, você pode flexibilizar e usar a magia de transclusão para fazê-la funcionar - mas para a maioria dos casos de uso, não há necessidade de isolar.
MC.foo
Então, duas perguntas agora:
Existem duas maneiras, em ambas você passa valores para atributos. Considere este MainController:
$watch()
Isso controla esta visualização:
MC.foo
Observe que “watch-attribute” não é interpolado. Tudo funciona, devido à magia do JS. Aqui está a definição da diretiva:
$parse
Observe que $eval
é passado para $destroy
sem as aspas! Isso significa que o que foi realmente passado para $ watch foi a string 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. }); };
! Funciona, entretanto, porque qualquer string passada para $destroy
é avaliado em relação ao escopo e $digest()
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 dê uma olhada em {{ model }}
e
para ainda mais incrível.
O AngularJS faz algum trabalho em seu nome, mas não todo. O seguinte deve ser limpo manualmente:
vastArray
eventoSe você não fizer isso manualmente, encontrará um comportamento inesperado e vazamentos de memória. Pior ainda - eles não ficarão visíveis imediatamente, mas irão aparecer eventualmente. Lei de Murphy.
Surpreendentemente, o AngularJS fornece maneiras úteis de lidar com todos esses:
item.velocity
Observe o jQuery {{ someModel }}
evento. É chamado como o AngularJS, mas é tratado separadamente. Os observadores do escopo $ não reagirão ao evento jQuery.
Isso deve ser bastante simples agora. Há uma coisa a entender aqui: ng-if
. Para cada ligação ng-repeat
, AngularJS cria um inspetor. Em cada fase de digestão, cada ligação é avaliada e comparada com o valor anterior. Isso é chamado de verificação suja, e é isso que $ digest faz. Se o valor mudou desde a última verificação, o callback do inspetor é acionado. Se esse retorno de chamada do inspetor modificar um modelo (variável $ scope), um novo ciclo de $ digest é disparado (até um máximo de 10) quando uma exceção é lançada.
Os navegadores não têm problemas mesmo com milhares de ligações, a menos que as expressões sejam complexas. A resposta comum para “quantos observadores podem ter” é 2.000.
Então, como podemos limitar o número de observadores? Por não observar os modelos de escopo quando não esperamos que eles mudem. É bastante fácil partir do AngularJS 1.3, uma vez que as ligações únicas estão no núcleo agora.
$watch()
Depois de $Watchers
e $watch()
são avaliados uma vez, eles nunca mudarão novamente. Você ainda pode aplicar filtros ao array, eles funcionarão muito bem. Acontece que o próprio array não será avaliado. Em muitos casos, é uma vitória.
Este erro do AngularJS já foi parcialmente coberto pelos erros 9.be em 13. Esta é uma explicação mais completa. O AngularJS atualiza o DOM como resultado de funções de retorno de chamada para observadores. Cada ligação, essa é a diretiva $rootScope.$digest()
configura observadores, mas os observadores também são configurados para muitas outras diretivas como $scope
e $watch()
. Basta dar uma olhada no código-fonte, é muito legível. Os observadores também podem ser configurados manualmente, e você provavelmente já fez isso pelo menos algumas vezes.
$digest()
ers estão vinculados a escopos. .$apply
pode receber cadeias de caracteres, que são avaliadas em relação ao escopo de .$applyAsync
foi obrigado a. Eles também podem avaliar funções. E eles também aceitam chamadas de retorno. Portanto, quando .$evalAsync
é chamado, todos os modelos registrados (ou seja, $digest()
variáveis) são avaliados e comparados com seus valores anteriores. Se os valores não corresponderem, o retorno de chamada para o $digest()
É executado.
É importante entender que, embora o valor de um modelo tenha sido alterado, o retorno de chamada não dispara até a próxima fase de resumo. É chamada de “fase” por uma razão - pode consistir em vários ciclos de digestão. Se apenas um observador altera um modelo de escopo, outro ciclo de resumo é executado.
Mas describe('some module', function () { it('should call the name-it service…', function () { // leave this empty for now }); ... });
não é pesquisado para . É chamado a partir de diretivas, serviços, métodos principais etc. Se você alterar um modelo de uma função personalizada que não chama '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);
, console.log()
, debugInfo
ou qualquer outra coisa que eventualmente chame $(document.body).scope().$root
, as ligações não serão atualizadas.
A propósito, o código-fonte de $($0).scope()
é bastante complexo. No entanto, vale a pena ler, pois os avisos hilariantes compensam isso.
Se você segue as tendências de desenvolvimento de front-end e é um pouco preguiçoso - como eu - provavelmente tenta não fazer tudo manualmente. Acompanhar todas as suas dependências, processar conjuntos de arquivos de maneiras diferentes, recarregar o navegador após cada salvamento de arquivo - o desenvolvimento é muito mais do que apenas codificação.
Portanto, você pode usar o bower e talvez o npm, dependendo de como você atende seu aplicativo. É possível que você esteja usando grunt, gulp ou brunch. Ou bash, o que também é legal. Na verdade, você pode ter iniciado seu último projeto com algum gerador Yeoman!
Isso leva à pergunta: você entende todo o processo do que sua infraestrutura realmente faz? Você precisa do que tem, especialmente se passou horas tentando consertar a funcionalidade de carregamento do servidor da web de conexão?
Reserve um segundo para avaliar o que você precisa. Todas essas ferramentas estão aqui apenas para ajudá-lo, não há outra recompensa por usá-las. Os desenvolvedores mais experientes com quem falo tendem a simplificar as coisas.
Os testes não deixarão seu código livre de mensagens de erro AngularJS. O que eles farão é garantir que sua equipe não tenha problemas de regressão o tempo todo.
Estou escrevendo especificamente sobre testes de unidade aqui, não porque acho que eles são mais importantes do que os testes e2e, mas porque são executados muito mais rápido. Devo admitir que o processo que vou descrever é muito prazeroso.
Desenvolvimento orientado a testes como uma implementação para, por exemplo, gulp-karma runner, basicamente executa todos os seus testes de unidade em cada arquivo salvo. Minha maneira favorita de escrever testes é, eu apenas escrevo garantias vazias primeiro:
angular.reloadWithDebugInfo()
Depois disso, eu escrevo ou refatoro o código real e, em seguida, volto aos testes e preencho as garantias com o código de teste real.
Ter uma tarefa TDD em execução em um terminal acelera o processo em cerca de 100%. Os testes de unidade são executados em questão de segundos, mesmo se você os tiver muitos. Basta salvar o arquivo de teste e o executor irá pegá-lo, avaliar seus testes e fornecer feedback instantaneamente.
Com os testes e2e, o processo é muito mais lento. Meu conselho - divida os testes e2e em suítes de teste e apenas execute um de cada vez. Protractor tem suporte para eles, e abaixo está o código que eu uso para minhas tarefas de teste (eu gosto de gole).
var injector = $(document.body).injector(); var someService = injector.get('someService');
A - pontos de interrupção do cromo
As ferramentas de desenvolvimento do Chrome permitem que você aponte para um local específico em qualquer um dos arquivos carregados no navegador, pause a execução do código nesse ponto e deixe você interagir com todas as variáveis disponíveis a partir desse ponto. Isso é muito! Essa funcionalidade não exige que você adicione nenhum código, tudo acontece nas ferramentas de desenvolvimento.
Você não apenas obtém acesso a todas as variáveis, mas também vê a pilha de chamadas, imprime rastreamentos de pilha e muito mais. Você pode até configurá-lo para trabalhar com arquivos minimizados. Leia sobre isso Aqui .
Existem outras maneiras de obter acesso em tempo de execução semelhante, por exemplo, adicionando Ng-init
chamadas. Mas os pontos de interrupção são mais sofisticados.
O AngularJS também permite que você acesse o escopo por meio de elementos DOM (desde que ng-if
esteja habilitado) e injete os serviços disponíveis por meio do console. Considere o seguinte no console:
ng-repeat
ou aponte para um elemento no inspetor e:
var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });
Mesmo se debugInfo não estiver ativado, você pode fazer:
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 }); }); } }; }];
E tenha-o disponível após a recarga:
Para injetar e interagir com um serviço do console, tente:
$watch
B - cronograma do cromo
Outra ótima ferramenta que vem com ferramentas de desenvolvimento é a linha do tempo. Isso permitirá que você grave e analise o desempenho ao vivo do seu aplicativo enquanto o usa. A saída mostra, entre outros, o uso de memória, taxa de quadros e a dissecação dos diferentes processos que ocupam a CPU: carregamento, script, renderização e pintura.
Se você perceber que o desempenho do seu aplicativo diminui, provavelmente será capaz de encontrar a causa disso na guia da linha do tempo. Basta registrar suas ações que levaram a problemas de desempenho e ver o que acontece. Muitos observadores? Você verá barras amarelas ocupando muito espaço. Perda de memória? Você pode ver a quantidade de memória consumida ao longo do tempo em um gráfico.
Uma descrição detalhada: https://developer.chrome.com/devtools/docs/timeline
C - inspecionar aplicativos remotamente no iOS e Android
Se você estiver desenvolvendo um aplicativo híbrido ou um aplicativo da web responsivo, poderá acessar o console do seu dispositivo, a árvore DOM e todas as outras ferramentas disponíveis por meio das ferramentas de desenvolvimento do Chrome ou Safari. Isso inclui o WebView e o UIWebView.
Primeiro, inicie seu servidor da web no host 0.0.0.0 para que ele seja acessível de sua rede local. Habilite o inspetor da web nas configurações. Em seguida, conecte seu dispositivo ao desktop e acesse sua página de desenvolvimento local, usando o ip da sua máquina em vez do “localhost” regular. Isso é tudo o que é preciso, seu dispositivo agora deve estar disponível para você no navegador do seu desktop.
Aqui são as instruções detalhadas para Android E para iOS, guias não oficiais podem ser encontrados facilmente no google.
Recentemente, tive uma experiência legal com 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 registro do aplicativo iOS enquanto controlava a página no iPad a partir do meu desktop. Funcionou muito bem!
// when in doubt, comment it out! :)
, pelo que parece, deve ser semelhante a
|_+_|e
|_+_|, certo? Você já se perguntou por que há um comentário nos documentos que não deve ser usado? IMHO isso foi surpreendente! Eu esperaria que a diretiva inicializasse um modelo. Isso também é o que ele faz, mas ... é implementado de uma maneira diferente, ou seja, não observa o valor do atributo. Você não precisa navegar pelo código-fonte do AngularJS - deixe-me levá-lo para você:
|_+_|
Menos do que você esperaria? Bastante legível, além da estranha sintaxe de diretiva, não é? A sexta linha é o que importa.
Compare-o com o ng-show:
|_+_|
Novamente, a sexta linha. Existe um
|_+_|lá, é isso que torna essa diretiva dinâmica. No código-fonte do AngularJS, grande parte de todo o código são comentários que descrevem o código que foi quase todo legível desde o início. Eu acredito que é uma ótima maneira de aprender sobre AngularJS.
Este guia que cobre os erros mais comuns do AngularJS é quase duas vezes mais longo que os outros guias. Aconteceu assim naturalmente. A demanda por engenheiros de front-end JavaScript de alta qualidade é muito alta. AngularJS é tão quente agora , e tem mantido uma posição estável entre as ferramentas de desenvolvimento mais populares por alguns anos. Com o AngularJS 2.0 a caminho, ele provavelmente dominará nos próximos anos.
O que é ótimo no desenvolvimento de front-end é que ele é muito recompensador. Nosso trabalho é visível instantaneamente e as pessoas interagem diretamente com os produtos que entregamos. O tempo gasto aprendendo JavaScript , e acredito que devemos nos concentrar na linguagem JavaScript, é um investimento muito bom. É a linguagem da Internet. A competição é super forte! Existe um foco para nós - a experiência do usuário. Para ter sucesso, precisamos cobrir tudo.
O código-fonte usado nestes exemplos pode ser baixado de 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:
Também gostaria de agradecer a todas as pessoas excelentes nos canais #angularjs e #javascript do FreeNode por muitas conversas excelentes e suporte contínuo.
E, finalmente, lembre-se sempre:
|_+_|
Se você segue as tendências de desenvolvimento de front-end e é um pouco preguiçoso - como eu - provavelmente tenta não fazer tudo manualmente. Acompanhar todas as suas dependências, processar conjuntos de arquivos de maneiras diferentes, recarregar o navegador após cada salvamento de arquivo - o desenvolvimento é muito mais do que apenas codificação.
Portanto, você pode usar o bower e talvez o npm, dependendo de como você atende seu aplicativo. É possível que você esteja usando grunt, gulp ou brunch. Ou bash, o que também é legal. Na verdade, você pode ter iniciado seu último projeto com algum gerador Yeoman!
Isso leva à pergunta: você entende todo o processo do que sua infraestrutura realmente faz? Você precisa do que tem, especialmente se passou horas tentando consertar a funcionalidade de carregamento do servidor da web de conexão?
Reserve um segundo para avaliar o que você precisa. Todas essas ferramentas estão aqui apenas para ajudá-lo, não há outra recompensa por usá-las. Os desenvolvedores mais experientes com quem falo tendem a simplificar as coisas.
Os testes não deixarão seu código livre de mensagens de erro AngularJS. O que eles farão é garantir que sua equipe não tenha problemas de regressão o tempo todo.
Estou escrevendo especificamente sobre testes de unidade aqui, não porque acho que eles são mais importantes do que os testes e2e, mas porque são executados muito mais rápido. Devo admitir que o processo que vou descrever é muito prazeroso.
Desenvolvimento orientado a testes como uma implementação para, por exemplo, gulp-karma runner, basicamente executa todos os seus testes de unidade em cada arquivo salvo. Minha maneira favorita de escrever testes é, eu apenas escrevo garantias vazias primeiro:
angular.reloadWithDebugInfo()
Depois disso, eu escrevo ou refatoro o código real e, em seguida, volto aos testes e preencho as garantias com o código de teste real.
Ter uma tarefa TDD em execução em um terminal acelera o processo em cerca de 100%. Os testes de unidade são executados em questão de segundos, mesmo se você os tiver muitos. Basta salvar o arquivo de teste e o executor irá pegá-lo, avaliar seus testes e fornecer feedback instantaneamente.
Com os testes e2e, o processo é muito mais lento. Meu conselho - divida os testes e2e em suítes de teste e apenas execute um de cada vez. Protractor tem suporte para eles, e abaixo está o código que eu uso para minhas tarefas de teste (eu gosto de gole).
var injector = $(document.body).injector(); var someService = injector.get('someService');
A - pontos de interrupção do cromo
As ferramentas de desenvolvimento do Chrome permitem que você aponte para um local específico em qualquer um dos arquivos carregados no navegador, pause a execução do código nesse ponto e deixe você interagir com todas as variáveis disponíveis a partir desse ponto. Isso é muito! Essa funcionalidade não exige que você adicione nenhum código, tudo acontece nas ferramentas de desenvolvimento.
Você não apenas obtém acesso a todas as variáveis, mas também vê a pilha de chamadas, imprime rastreamentos de pilha e muito mais. Você pode até configurá-lo para trabalhar com arquivos minimizados. Leia sobre isso Aqui .
Existem outras maneiras de obter acesso em tempo de execução semelhante, por exemplo, adicionando Ng-init
chamadas. Mas os pontos de interrupção são mais sofisticados.
O AngularJS também permite que você acesse o escopo por meio de elementos DOM (desde que ng-if
esteja habilitado) e injete os serviços disponíveis por meio do console. Considere o seguinte no console:
ng-repeat
ou aponte para um elemento no inspetor e:
var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });
Mesmo se debugInfo não estiver ativado, você pode fazer:
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 }); }); } }; }];
E tenha-o disponível após a recarga:
Para injetar e interagir com um serviço do console, tente:
$watch
B - cronograma do cromo
Outra ótima ferramenta que vem com ferramentas de desenvolvimento é a linha do tempo. Isso permitirá que você grave e analise o desempenho ao vivo do seu aplicativo enquanto o usa. A saída mostra, entre outros, o uso de memória, taxa de quadros e a dissecação dos diferentes processos que ocupam a CPU: carregamento, script, renderização e pintura.
Se você perceber que o desempenho do seu aplicativo diminui, provavelmente será capaz de encontrar a causa disso na guia da linha do tempo. Basta registrar suas ações que levaram a problemas de desempenho e ver o que acontece. Muitos observadores? Você verá barras amarelas ocupando muito espaço. Perda de memória? Você pode ver a quantidade de memória consumida ao longo do tempo em um gráfico.
Uma descrição detalhada: https://developer.chrome.com/devtools/docs/timeline
C - inspecionar aplicativos remotamente no iOS e Android
Se você estiver desenvolvendo um aplicativo híbrido ou um aplicativo da web responsivo, poderá acessar o console do seu dispositivo, a árvore DOM e todas as outras ferramentas disponíveis por meio das ferramentas de desenvolvimento do Chrome ou Safari. Isso inclui o WebView e o UIWebView.
Primeiro, inicie seu servidor da web no host 0.0.0.0 para que ele seja acessível de sua rede local. Habilite o inspetor da web nas configurações. Em seguida, conecte seu dispositivo ao desktop e acesse sua página de desenvolvimento local, usando o ip da sua máquina em vez do “localhost” regular. Isso é tudo o que é preciso, seu dispositivo agora deve estar disponível para você no navegador do seu desktop.
Aqui são as instruções detalhadas para Android E para iOS, guias não oficiais podem ser encontrados facilmente no google.
Recentemente, tive uma experiência legal com 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 registro do aplicativo iOS enquanto controlava a página no iPad a partir do meu desktop. Funcionou muito bem!
// when in doubt, comment it out! :)
, pelo que parece, deve ser semelhante a
|_+_|e
|_+_|, certo? Você já se perguntou por que há um comentário nos documentos que não deve ser usado? IMHO isso foi surpreendente! Eu esperaria que a diretiva inicializasse um modelo. Isso também é o que ele faz, mas ... é implementado de uma maneira diferente, ou seja, não observa o valor do atributo. Você não precisa navegar pelo código-fonte do AngularJS - deixe-me levá-lo para você:
|_+_|
Menos do que você esperaria? Bastante legível, além da estranha sintaxe de diretiva, não é? A sexta linha é o que importa.
Compare-o com o ng-show:
|_+_|
Novamente, a sexta linha. Existe um
|_+_|lá, é isso que torna essa diretiva dinâmica. No código-fonte do AngularJS, grande parte de todo o código são comentários que descrevem o código que foi quase todo legível desde o início. Eu acredito que é uma ótima maneira de aprender sobre AngularJS.
Este guia que cobre os erros mais comuns do AngularJS é quase duas vezes mais longo que os outros guias. Aconteceu assim naturalmente. A demanda por engenheiros de front-end JavaScript de alta qualidade é muito alta. AngularJS é tão quente agora , e tem mantido uma posição estável entre as ferramentas de desenvolvimento mais populares por alguns anos. Com o AngularJS 2.0 a caminho, ele provavelmente dominará nos próximos anos.
O que é ótimo no desenvolvimento de front-end é que ele é muito recompensador. Nosso trabalho é visível instantaneamente e as pessoas interagem diretamente com os produtos que entregamos. O tempo gasto aprendendo JavaScript , e acredito que devemos nos concentrar na linguagem JavaScript, é um investimento muito bom. É a linguagem da Internet. A competição é super forte! Existe um foco para nós - a experiência do usuário. Para ter sucesso, precisamos cobrir tudo.
O código-fonte usado nestes exemplos pode ser baixado de 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:
Também gostaria de agradecer a todas as pessoas excelentes nos canais #angularjs e #javascript do FreeNode por muitas conversas excelentes e suporte contínuo.
E, finalmente, lembre-se sempre:
|_+_|