Los 18 errores más comunes que cometen los desarrolladores de AngularJS
Publicado: 2022-03-11Las aplicaciones de una sola página exigen que los desarrolladores front-end se conviertan en mejores ingenieros de software. CSS y HTML ya no son la mayor preocupación, de hecho, ya no hay una sola preocupación. El desarrollador front-end debe manejar los XHR, la lógica de la aplicación (modelos, vistas, controladores), el rendimiento, las animaciones, los estilos, la estructura, el SEO y la integración con servicios externos. El resultado que surge de todos ellos combinados es la Experiencia de Usuario (UX) que siempre debe priorizarse.
AngularJS es un framework muy poderoso. Es el tercer repositorio con más estrellas en GitHub. No es difícil empezar a utilizarlo, pero los objetivos que se pretende lograr exigen comprensión. Los desarrolladores de AngularJS ya no pueden ignorar el consumo de memoria, porque ya no se restablecerá en la navegación. Esta es la vanguardia del desarrollo web. ¡Aceptémoslo!
Error común n.º 1: acceder al alcance a través del DOM
Se recomiendan algunos ajustes de optimización para la producción. Uno de ellos es deshabilitar la información de depuración.
DebugInfoEnabled
es una configuración que por defecto es verdadera y permite el acceso al alcance a través de los nodos DOM. Si desea probar eso a través de la consola de JavaScript, seleccione un elemento DOM y acceda a su alcance con:
angular.element(document.body).scope()
Puede ser útil incluso cuando no se usa jQuery con su CSS, pero no debe usarse fuera de la consola. La razón es que cuando $compileProvider.debugInfoEnabled
se establece en false, llamar a .scope()
en un nodo DOM devolverá undefined
.
Esa es una de las pocas opciones recomendadas para la producción.
Tenga en cuenta que aún puede acceder al alcance a través de la consola, incluso cuando está en producción. Llama angular.reloadWithDebugInfo()
desde la consola y la aplicación hará exactamente eso.
Error común n.º 2: no tener un punto allí
Probablemente hayas leído que si no tenías un punto en tu ng-model , lo estabas haciendo mal. Cuando se trata de herencia, esa declaración es a menudo cierta. Los ámbitos tienen un modelo prototipo de herencia, típico de JavaScript, y los ámbitos anidados son comunes a AngularJS. Muchas directivas crean ámbitos secundarios como ngRepeat
, ngIf
y ngController
. Al resolver un modelo, la búsqueda comienza en el ámbito actual y pasa por todos los ámbitos principales, hasta llegar a $rootScope
.
Pero, al establecer un nuevo valor, lo que sucede depende de qué tipo de modelo (variable) queremos cambiar. Si el modelo es un primitivo, el ámbito secundario simplemente creará un nuevo modelo. Pero si el cambio es a una propiedad de un objeto modelo, la búsqueda en los ámbitos principales encontrará el objeto al que se hace referencia y cambiará su propiedad real. No se establecería un nuevo modelo en el ámbito actual, por lo que no se produciría ningún enmascaramiento:
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>
Al hacer clic en el botón "Establecer primitivo" se establecerá foo en el ámbito interno a 2, pero no cambiará foo en el ámbito externo.
Al hacer clic en el botón "Cambiar objeto", se cambiará la propiedad de la barra del ámbito principal. Dado que no hay ninguna variable en el ámbito interno, no se producirá sombreado y el valor visible de la barra será 3 en ambos ámbitos.
Otra forma de hacer esto es aprovechar el hecho de que se hace referencia a los ámbitos primarios y al ámbito raíz desde todos los ámbitos. Los objetos $parent
y $root
se pueden usar para acceder al ámbito principal y $rootScope
, respectivamente, directamente desde la vista. Puede ser una forma poderosa, pero no soy un fanático de ella debido al problema de apuntar a un alcance particular en la corriente. Hay otra forma de establecer y acceder a las propiedades específicas de un ámbito: utilizando la sintaxis de controllerAs
.
Error común n.º 3: no usar la sintaxis de controllerAs
La forma alternativa y más eficiente de asignar modelos para usar un objeto de controlador en lugar del $scope inyectado. En lugar de inyectar alcance, 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>
Esto es mucho menos confuso. Especialmente cuando hay muchos ámbitos anidados, como puede ser el caso de los estados anidados.
Hay más en la sintaxis de controllerAs.
Error común n.º 4: no utilizar completamente la sintaxis de controllerAs
Hay algunas advertencias sobre cómo se expone el objeto del controlador. Es básicamente un objeto colocado en el alcance del controlador, como un modelo normal.
Si necesita observar una propiedad del objeto controlador, puede observar una función, pero no está obligado a hacerlo. Aquí hay un ejemplo:
function MainController($scope) { this.title = 'Some title'; $scope.$watch(angular.bind(this, function () { return this.title; }), function (newVal, oldVal) { // handle changes }); }
Es más fácil simplemente hacer:
function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }
Lo que significa que también en la cadena de alcance, puede acceder a MC desde un controlador secundario:
function NestedController($scope) { if ($scope.MC && $scope.MC.title === 'Some title') { $scope.MC.title = 'New title'; } }
Sin embargo, para poder hacer eso, debe ser coherente con el acrónimo que usa para controllerAs. Hay al menos tres formas de configurarlo. Ya viste el primero:
<div ng-controller="MainController as MC"> … </div>
Sin embargo, si usa ui-router
, especificar un controlador de esa manera es propenso a errores. Para los estados, los controladores deben especificarse en la configuración del estado:
angular.module('myApp', []) .config(function ($stateProvider) { $stateProvider .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/path/to/template.html' }) }). controller('MainController', function () { … });
Hay otra forma de anotar:
(…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })
Puedes hacer lo mismo en las directivas:
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);
La otra forma de anotar también es válida, aunque menos concisa:
function testForToptal() { return { controller: 'AnotherController', controllerAs: 'AC', template: '<p>{{ AC.text }}</p>' }; }
Error común n.° 5: no usar vistas con nombre con UI-ROUTER para obtener energía”
La solución de enrutamiento de facto para AngularJS ha sido, hasta ahora, el ui-router
. Eliminado del núcleo hace algún tiempo, el módulo ngRoute era demasiado básico para un enrutamiento más sofisticado.
Hay un nuevo NgRouter
en camino, pero los autores aún lo consideran demasiado pronto para la producción. Cuando escribo esto, el Angular estable es 1.3.15, y ui-router
oscila.
Las razones principales:
- Impresionante estado de anidación
- abstracción de ruta
- parámetros opcionales y requeridos
Aquí cubriré el anidamiento de estados para evitar errores de AngularJS.
Piense en esto como un caso de uso complejo pero estándar. Hay una aplicación, que tiene una vista de página de inicio y una vista de producto. La vista del producto tiene tres secciones separadas: la introducción, el widget y el contenido. Queremos que el widget persista y no se vuelva a cargar al cambiar de estado. Pero el contenido debería recargarse.
Considere la siguiente estructura de página de índice de productos 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>
Esto es algo que podríamos obtener del codificador HTML y ahora necesitamos separarlo en archivos y estados. Generalmente sigo la convención de que hay un estado PRINCIPAL abstracto, que conserva los datos globales si es necesario. Úselo en lugar de $rootScope. El estado Principal también mantendrá el HTML estático que se requiere en cada página. Mantengo index.html limpio.
<!— 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>
Luego veamos la página de índice del producto:
<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 puede ver, la página de índice del producto tiene tres vistas con nombre. Uno para la introducción, otro para el widget y otro para el producto. ¡Cumplimos con las especificaciones! Así que ahora configuremos el enrutamiento:
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);
Ese sería el primer acercamiento. Ahora, ¿qué sucede al cambiar entre main.product.index
y main.product.details
? El contenido y el widget se recargan, pero solo queremos recargar el contenido. Esto fue problemático, y los desarrolladores en realidad crearon enrutadores que admitirían solo esa funcionalidad. Uno de los nombres para esto fue vistas pegajosas . Afortunadamente, ui-router
lo admite desde el primer momento con una orientación de vista con nombre absoluta .
// 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' } } });
Al mover la definición de estado a la vista principal, que también es abstracta, podemos evitar que la vista secundaria se vuelva a cargar al cambiar las URL, lo que normalmente afecta a los hermanos de ese hijo. Por supuesto, el widget podría ser una directiva simple. Pero el punto es que también podría ser otro estado anidado complejo.
Hay otra forma de hacer esto mediante el uso de $urlRouterProvider.deferIntercept()
, pero creo que usar la configuración de estado es mejor. Si está interesado en interceptar rutas, escribí un pequeño tutorial sobre StackOverflow.
Error común #6: Declarar todo en el mundo angular usando funciones anónimas
Este error es de menor calibre y es más una cuestión de estilo que evitar los mensajes de error de AngularJS. Es posible que haya notado anteriormente que rara vez paso funciones anónimas a las declaraciones internas angulares. Por lo general, solo defino una función primero y luego la paso.
Esto se refiere a más que solo funciones. Obtuve este enfoque al leer guías de estilo, especialmente las de Airbnb y Todd Motto. Creo que hay varias ventajas y casi ningún inconveniente.
En primer lugar, puede manipular y mutar sus funciones y objetos mucho más fácilmente si se asignan a una variable. En segundo lugar, el código es más limpio y se puede dividir fácilmente en archivos. Eso significa mantenibilidad. Si no desea contaminar el espacio de nombres global, envuelva cada archivo en IIFE. La tercera razón es la capacidad de prueba. Considere este ejemplo:
'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);
Así que ahora podríamos burlarnos de publicMethod1
, pero ¿por qué deberíamos hacerlo si está expuesto? ¿No sería más fácil simplemente espiar el método existente? Sin embargo, el método es en realidad otra función: una envoltura delgada. Echa un vistazo a este enfoque:
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; }
No se trata solo de estilo, ya que en efecto el código es más reutilizable e idiomático. El revelador obtiene mayor poder expresivo. Dividir todo el código en bloques autónomos lo hace más fácil.
Error común n.º 7: hacer un procesamiento pesado en Angular AKA usando trabajadores
En algunos escenarios, puede ser necesario procesar una gran variedad de objetos complejos pasándolos a través de un conjunto de filtros, decoradores y, finalmente, un algoritmo de clasificación. Un caso de uso es cuando la aplicación debe funcionar sin conexión o cuando el rendimiento de la visualización de datos es clave. Y dado que JavaScript es de un solo subproceso, es relativamente fácil congelar el navegador.
También es fácil evitarlo con trabajadores web. No parece haber bibliotecas populares que manejen eso específicamente para AngularJS. Sin embargo, podría ser lo mejor, ya que la implementación es fácil.
Primero, configuremos el servicio:
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);
Ahora, el trabajador:
'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);
Ahora, inyecte el servicio como de costumbre y trate scoringService.scoreItems()
como lo haría con cualquier método de servicio que devuelva una promesa. El procesamiento pesado se llevará a cabo en un subproceso separado y no se dañará la UX.
Qué tener en cuenta:
- no parece haber una regla general sobre cuántos trabajadores generar. Algunos desarrolladores afirman que 8 es un buen número, pero usa una calculadora en línea y haz lo que quieras.
- comprobar la compatibilidad con navegadores más antiguos
- Me encuentro con un problema al pasar el número 0 del servicio al trabajador.
.toString()
en la propiedad pasada y funcionó correctamente.
Error común n.º 8: Resuelve el uso excesivo y los malentendidos
Las resoluciones añaden tiempo extra a la carga de la vista. Creo que el alto rendimiento de la aplicación front-end es nuestro objetivo principal. No debería ser un problema representar algunas partes de la vista mientras la aplicación espera los datos de la API.
Considere esta configuración:
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) }); }
La salida de la consola será:
Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed
Lo que básicamente significa que:
- Las resoluciones se ejecutan de forma asíncrona.
- No podemos confiar en una orden de ejecución (o al menos necesitamos flexionar un poco)
- Se bloquean todos los estados hasta que todas las resoluciones hagan lo suyo, aunque no sean abstractas.
Esto significa que antes de que el usuario vea cualquier resultado, debe esperar todas las dependencias. Necesitamos tener esos datos, claro, está bien. Si es absolutamente necesario tenerlo antes de la vista, colóquelo en un bloque .run()
. De lo contrario, simplemente haga la llamada al servicio desde el controlador y maneje el estado medio cargado con gracia. Ver el trabajo en progreso, y el controlador ya está ejecutado, por lo que en realidad es un progreso, es mejor que tener la aplicación bloqueada.
Error común n.° 9: no optimizar la aplicación: tres ejemplos
a) Causar demasiados bucles de resumen, como adjuntar controles deslizantes a los modelos
Este es un problema general que puede resultar en errores de AngularJS, pero lo discutiré en el ejemplo de los controles deslizantes. Estaba usando esta biblioteca de controles deslizantes, control deslizante de rango angular, porque necesitaba la funcionalidad extendida. Esa directiva tiene esta sintaxis en la versión 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 el siguiente código en el 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'); } } });
Entonces eso funciona lento. La solución informal sería establecer un tiempo de espera en la entrada. Pero eso no siempre es útil y, a veces, no queremos retrasar el cambio de modelo real en todos los casos.
Entonces agregaremos un modelo temporal destinado a cambiar el modelo de trabajo en el tiempo de espera:
<body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.priceTemporary" > </div> </body>
y en el 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) No usar $applyAsync
AngularJS no tiene un mecanismo de sondeo para llamar a $digest()
. Solo se ejecuta porque usamos las directivas (p. ej., ng-click
, input
), servicios ( $timeout
, $http
) y métodos ( $watch
) que evalúan nuestro código y luego llaman a un resumen.

Lo que hace .$applyAsync()
es retrasar la resolución de las expresiones hasta el siguiente ciclo de $digest()
, que se activa después de un tiempo de espera de 0, que en realidad es de ~10ms.
Hay dos formas de usar applyAsync
ahora. Una forma automatizada para las solicitudes de $http
y una forma manual para el resto.
Para hacer que todas las solicitudes http que regresan aproximadamente al mismo tiempo se resuelvan en un resumen, haga lo siguiente:
mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });
La forma manual muestra cómo funciona realmente. Considere alguna función que se ejecute en la devolución de llamada a un detector de eventos JS estándar o jQuery .click()
, o alguna otra biblioteca externa. Después de que se ejecuta y cambia los modelos, si aún no lo envolvió en $apply()
, debe llamar a $scope.$root.$digest()
( $rootScope.$digest()
), o al menos $scope.$digest()
. De lo contrario, no verá ningún cambio.
Si lo hace varias veces en un flujo, podría comenzar a funcionar lentamente. Considere llamar a $scope.$applyAsync()
en las expresiones en su lugar. Establecerá solo llamar a un ciclo de resumen para todos ellos.
c) Hacer un procesamiento pesado de imágenes.
Si experimenta un mal rendimiento, puede investigar el motivo utilizando la línea de tiempo de Chrome Developer Tools. Escribiré más sobre esta herramienta en el error #17. Si su gráfico de línea de tiempo está dominado por el color verde después de la grabación, sus problemas de rendimiento pueden estar relacionados con el procesamiento de imágenes. Esto no está estrictamente relacionado con AngularJS, pero puede ocurrir además de los problemas de rendimiento de AngularJS (que serían en su mayoría amarillos en el gráfico). Como ingenieros front-end, debemos pensar en el proyecto final completo.
Tómese un momento para evaluar:
- ¿Usas paralaje?
- ¿Tiene varias capas de contenido superpuestas entre sí?
- ¿Mueves tus imágenes?
- ¿Escale las imágenes (por ejemplo, con tamaño de fondo)?
- ¿Cambia el tamaño de las imágenes en bucles y tal vez provoca bucles de resumen al cambiar el tamaño?
Si respondió "sí" a al menos tres de los anteriores, considere facilitarlo. Tal vez pueda servir varios tamaños de imagen y no cambiar el tamaño en absoluto. Tal vez podría agregar el truco de procesamiento de GPU forzado "transform: translateZ (0)". O use requestAnimationFrame para controladores.
Error común #10: jQuerying It - Árbol DOM separado
Muchas veces probablemente escuche que no se recomienda usar jQuery con AngularJS y que debe evitarse. Es imperativo entender la razón detrás de estas declaraciones. Hay al menos tres razones, por lo que puedo ver, pero ninguna de ellas es un bloqueador real.
Motivo 1: cuando ejecuta el código jQuery, debe llamar a $digest()
usted mismo. Para muchos casos, existe una solución AngularJS que está diseñada para AngularJS y puede ser de mejor uso dentro de Angular que jQuery (por ejemplo, ng-click o el sistema de eventos).
Razón 2: El método de pensamiento sobre la construcción de la aplicación. Si ha estado agregando JavaScript a los sitios web, que se recargan al navegar, no tenía que preocuparse demasiado por el consumo de memoria. Con las aplicaciones de una sola página, tienes que preocuparte. Si no limpia, los usuarios que pasan más de unos minutos en su aplicación pueden experimentar problemas de rendimiento cada vez mayores.
Razón 3: Limpiar no es realmente lo más fácil de hacer y analizar. No hay forma de llamar a un recolector de basura desde el script (en el navegador). Puede terminar con árboles DOM separados. Creé un ejemplo (jQuery está cargado en 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 es una directiva simple que genera un texto. Hay un botón debajo, que simplemente destruirá la directiva manualmente.
Entonces, cuando se elimina la directiva, queda una referencia al árbol DOM en scope.toBeDetached. En las herramientas de desarrollo de Chrome, si accede a la pestaña "perfiles" y luego "tomar una instantánea del montón", verá en el resultado:
Puedes vivir con unos pocos, pero es malo si tienes una tonelada. Especialmente si por alguna razón, como en el ejemplo, lo almacena en el alcance. Todo el DOM se evaluará en cada resumen. El árbol DOM separado problemático es el que tiene 4 nodos. Entonces, ¿cómo se puede resolver esto?
scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });
¡Se elimina el árbol DOM separado con 4 entradas!
En este ejemplo, la directiva usa el mismo alcance y almacena el elemento DOM en el alcance. Fue más fácil para mí demostrarlo de esa manera. No siempre se vuelve tan malo, ya que podría almacenarlo en una variable. Sin embargo, aún ocuparía memoria si se mantuviera algún cierre que hiciera referencia a esa variable o cualquier otro del mismo ámbito de función.
Error común n.º 11: uso excesivo del alcance aislado
Siempre que necesite una directiva que sepa que se usará en un solo lugar, o que no espere que entre en conflicto con el entorno en el que se use, no hay necesidad de usar un alcance aislado. Últimamente, hay una tendencia a crear componentes reutilizables, pero ¿sabía que las directivas angulares centrales no usan un alcance aislado en absoluto?
Hay dos razones principales: no puede aplicar dos directivas de alcance aisladas a un elemento, y puede encontrar problemas con el anidamiento/herencia/procesamiento de eventos. Especialmente con respecto a la transclusión: los efectos pueden no ser los esperados.
Entonces esto fallaría:
<p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"></p>
E incluso si usa solo una directiva, notará que ni los modelos de alcance aislado ni los eventos transmitidos en isolatedScopeDirective no estarán disponibles para AnotherController. Eso es triste, puede flexionar y usar la magia de transclusión para que funcione, pero para la mayoría de los casos de uso, no hay necesidad de aislarse.
<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>
Entonces, dos preguntas ahora:
- ¿Cómo puede procesar modelos de alcance principal en una directiva del mismo alcance?
- ¿Cómo se pueden crear instancias de nuevos valores del modelo?
Hay dos formas, en ambas se pasan valores a los 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]; }
Eso controla esta vista:
<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>
Tenga en cuenta que el "atributo de observación" no está interpolado. Todo funciona, debido a la magia de JS. Aquí está la definición de la directiva:
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
se pasa a scope.$watch()
sin las comillas! ¡Eso significa que lo que realmente se pasó a $watch fue la cadena MC.foo
! Sin embargo, funciona porque cualquier cadena que se pase a $watch()
se evalúa en el alcance y MC.foo
está disponible en el alcance. Esa es también la forma más común en que los atributos se observan en las directivas centrales de AngularJS.
Vea el código en github para la plantilla y busque en $parse
y $eval
para obtener aún más genialidad.
Error común n.° 12: no limpiar lo que ensucia usted mismo: observadores, intervalos, tiempos de espera y variables
AngularJS hace algo de trabajo en su nombre, pero no todo. Lo siguiente debe limpiarse manualmente:
- Cualquier observador que no esté vinculado al ámbito actual (p. ej., vinculado a $rootScope)
- Intervalos
- Tiempos de espera
- Variables que hacen referencia a DOM en directivas
- Complementos dudosos de jQuery, por ejemplo, aquellos que no tienen controladores que reaccionen al evento JavaScript
$destroy
Si no lo hace manualmente, encontrará un comportamiento inesperado y pérdidas de memoria. Peor aún, estos no serán visibles al instante, pero eventualmente aparecerán sigilosamente. Ley de murphy.
Sorprendentemente, AngularJS proporciona formas prácticas de lidiar con todo eso:
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 el evento jQuery $destroy
. Se llama como AngularJS, pero se maneja por separado. Los observadores de alcance $ no reaccionarán al evento jQuery.
Error común n.º 13: tener demasiados vigilantes
Esto debería ser bastante simple ahora. Hay una cosa que entender aquí: $digest()
. Para cada enlace {{ model }}
, AngularJS crea un observador. En cada fase de resumen, cada unión de este tipo se evalúa y compara con el valor anterior. Eso se llama verificación sucia, y eso es lo que hace $digest. Si el valor cambió desde la última verificación, se activa la devolución de llamada del observador. 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.
Primero, inicie su servidor web en el host 0.0.0.0 para que sea accesible desde su red local. Habilite el inspector web en la configuración. Luego, conecte su dispositivo a su escritorio y acceda a su página de desarrollo local, usando la ip de su máquina en lugar del "localhost" normal. Eso es todo lo que se necesita, su dispositivo ahora debería estar disponible para usted desde el navegador de su escritorio.
Aquí están las instrucciones detalladas para Android y para iOS, las guías no oficiales se pueden encontrar fácilmente a través de Google.
Recientemente tuve una experiencia genial con browserSync. Funciona de manera similar a livereload, pero también sincroniza todos los navegadores que están viendo la misma página a través de browserSync. Eso incluye la interacción del usuario, como desplazarse, hacer clic en los botones, etc. Estaba mirando el resultado del registro de la aplicación iOS mientras controlaba la página en el iPad desde mi escritorio. ¡Funcionó muy bien!
Error común #18: No leer el código fuente en el ejemplo NG-INIT
Ng-init
, por lo que parece, debería ser similar a ng-if
y ng-repeat
, ¿verdad? ¿Alguna vez se preguntó por qué hay un comentario en los documentos que no debe usarse? En mi humilde opinión eso fue sorprendente! Esperaría que la directiva inicializara un modelo. Eso también lo hace, pero… se implementa de otra manera, es decir, no mira el valor del atributo. No necesita navegar a través del código fuente de AngularJS; déjeme traerlo:
var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });
¿Menos de lo que esperabas? Bastante legible, además de la sintaxis directiva incómoda, ¿no? La sexta línea es de lo que se trata.
Compáralo con 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 }); }); } }; }];
De nuevo, la sexta línea. Hay un $watch
allí, eso es lo que hace que esta directiva sea dinámica. En el código fuente de AngularJS, una gran parte de todo el código son comentarios que describen el código que en su mayoría era legible desde el principio. Creo que es una excelente manera de aprender sobre AngularJS.
Conclusión
Esta guía que cubre los errores más comunes de AngularJS es casi el doble de larga que las otras guías. Resultó así naturalmente. La demanda de ingenieros front-end de JavaScript de alta calidad es muy alta. AngularJS está muy de moda en este momento y ha mantenido una posición estable entre las herramientas de desarrollo más populares durante algunos años. Con AngularJS 2.0 en camino, probablemente dominará en los próximos años.
Lo bueno del desarrollo front-end es que es muy gratificante. Nuestro trabajo es visible al instante y las personas interactúan directamente con los productos que entregamos. El tiempo dedicado a aprender JavaScript, y creo que deberíamos centrarnos en el lenguaje JavaScript, es una muy buena inversión. Es el lenguaje de Internet. ¡La competencia es súper fuerte! Hay un enfoque para nosotros: la experiencia del usuario. Para tener éxito, tenemos que cubrir todo.
El código fuente utilizado en estos ejemplos se puede descargar de GitHub. Siéntete libre de descargarlo y hacerlo tuyo.
Quería dar créditos a los cuatro desarrolladores editoriales que más me inspiraron:
- ben nadel
- Lema de Todd
- Pascual Precht
- Sandeep Panda
También quería agradecer a todas las personas excelentes en los canales #angularjs y #javascript de FreeNode por muchas conversaciones excelentes y apoyo continuo.
Y finalmente, recuerda siempre:
// when in doubt, comment it out! :)