Los 8 errores más comunes que cometen los desarrolladores de Ember.js

Publicado: 2022-03-11

Ember.js es un marco integral para crear aplicaciones complejas del lado del cliente. Uno de sus principios es "la convención sobre la configuración" y la convicción de que hay una gran parte del desarrollo común a la mayoría de las aplicaciones web y, por lo tanto, una única mejor manera de resolver la mayoría de estos desafíos cotidianos. Sin embargo, encontrar la abstracción correcta y cubrir todos los casos requiere tiempo y aportes de toda la comunidad. Como dice el razonamiento, es mejor tomarse el tiempo para obtener la solución correcta para el problema central y luego integrarlo en el marco, en lugar de tirar las manos y dejar que todos se las arreglen por sí mismos cuando necesiten encontrar una solución.

Ember.js evoluciona constantemente para facilitar aún más el desarrollo. Pero, como con cualquier marco avanzado, todavía hay trampas en las que los desarrolladores de Ember pueden caer. Con la siguiente publicación, espero proporcionar un mapa para evadirlos. ¡Entremos de inmediato!

Error común n.º 1: esperar que el gancho del modelo se active cuando se pasan todos los objetos de contexto

Supongamos que tenemos las siguientes rutas en nuestra aplicación:

 Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });

La ruta de la band tiene un segmento dinámico, id . Cuando la aplicación se carga con una URL como /bands/24 , 24 se pasa al enlace de model de la ruta correspondiente, band . El enlace del modelo tiene la función de deserializar el segmento para crear un objeto (o una matriz de objetos) que luego se puede usar en la plantilla:

 // app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });

Hasta aquí todo bien. Sin embargo, hay otras formas de ingresar rutas además de cargar la aplicación desde la barra de navegación del navegador. Uno de ellos es usar el asistente de link-to de las plantillas. El siguiente fragmento pasa por una lista de bandas y crea un enlace a sus respectivas rutas de band :

 {{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}

El último argumento para link-to, band , es un objeto que completa el segmento dinámico de la ruta y, por lo tanto, su id se convierte en el segmento de id de la ruta. La trampa en la que cae mucha gente es que no se llama al enlace del modelo en ese caso, ya que el modelo ya se conoce y se ha pasado. Tiene sentido y podría guardar una solicitud al servidor, pero es cierto que no lo es. intuitivo. Una forma ingeniosa de evitarlo es pasar, no el objeto en sí, sino su identificación:

 {{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}} 

Ember.js

Plan de Mitigación de Ember

Los componentes enrutables llegarán a Ember en breve, probablemente en la versión 2.1 o 2.2. Cuando aterrizan, siempre se llamará al gancho del modelo, sin importar cómo se haga la transición a una ruta con un segmento dinámico. Lea el RFC correspondiente aquí.

Error común n.º 2: olvidar que los controladores basados ​​en rutas son únicos

Las rutas en Ember.js configuran propiedades en los controladores que sirven como contexto para la plantilla correspondiente. Estos controladores son singletons y, en consecuencia, cualquier estado definido en ellos persiste incluso cuando el controlador ya no está activo.

Esto es algo que es muy fácil pasar por alto y me topé con esto también. En mi caso, tenía una aplicación de catálogo de música con bandas y canciones. El indicador songCreationStarted en el controlador de songs indica que el usuario ha comenzado a crear una canción para una banda en particular. El problema era que si el usuario cambiaba a otra banda, el valor de songCreationStarted persistía y parecía que la canción a medio terminar era para la otra banda, lo cual era confuso.

La solución es restablecer manualmente las propiedades del controlador que no queremos que permanezcan. Un lugar posible para hacer esto es el setupController de la ruta correspondiente, que se llama en todas las transiciones después del afterModel (que, como su nombre sugiere, viene después del enlace model ):

 // app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });

Plan de Mitigación de Ember

Una vez más, el surgimiento de los componentes enrutables resolverá este problema, poniendo fin a los controladores por completo. Una de las ventajas de los componentes enrutables es que tienen un ciclo de vida más consistente y siempre se rompen cuando se alejan de sus rutas. Cuando lleguen, el problema anterior desaparecerá.

Error común n.º 3: no llamar a la implementación predeterminada en setupController

Las rutas en Ember tienen un puñado de enlaces de ciclo de vida para definir el comportamiento específico de la aplicación. Ya vimos el model que se usa para obtener datos para la plantilla correspondiente y setupController , para configurar el controlador, el contexto de la plantilla.

Este último, setupController , tiene un valor predeterminado razonable, que asigna el modelo, desde el enlace del model como la propiedad del model del controlador:

 // ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }

(el context es el nombre utilizado por el paquete de ember-routing para lo que llamo model arriba)

El setupController se puede anular para varios propósitos, como restablecer el estado del controlador (como en el error común n.° 2 anterior). Sin embargo, si uno se olvida de llamar a la implementación principal que copié anteriormente en Ember.Route, uno puede estar en una larga sesión de rascarse la cabeza, ya que el controlador no tendrá establecida su propiedad de model . Así que siempre llama a this._super(controller, model) :

 export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });

Plan de Mitigación de Ember

Como se indicó anteriormente, los controladores, y con ellos, el setupController , desaparecerán pronto, por lo que esta trampa ya no será una amenaza. Sin embargo, hay una lección mayor que aprender aquí, que es tener en cuenta las implementaciones en los antepasados. La función init , definida en Ember.Object , la madre de todos los objetos en Ember, es otro ejemplo que debe tener en cuenta.

Error común n.º 4: usar this.modelFor con rutas que no son principales

El enrutador Ember resuelve el modelo para cada segmento de ruta a medida que procesa la URL. Supongamos que tenemos las siguientes rutas en nuestra aplicación:

 Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });

Dada una URL de /bands/24/songs , se llama al gancho model de bands , bands.band y luego bands.band.songs , en este orden. La API de ruta tiene un método particularmente útil, modelFor , que se puede usar en rutas secundarias para obtener el modelo de una de las rutas principales, ya que ese modelo seguramente se habrá resuelto en ese punto.

Por ejemplo, el siguiente código es una forma válida de obtener el objeto banda en la ruta bands.band :

 // app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); return bands.filterBy('id', params.id); } });

Sin embargo, un error común es usar un nombre de ruta en modelFor que no sea un padre de la ruta. Si las rutas del ejemplo anterior se modificaron ligeramente:

 Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });

Nuestro método para obtener la banda designada en la URL fallaría, ya que la ruta de las bands ya no es un padre y, por lo tanto, su modelo no se ha resuelto.

 // app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); // `bands` is undefined return bands.filterBy('id', params.id); // => error! } });

La solución es usar modelFor solo para las rutas principales y usar otros medios para recuperar los datos necesarios cuando no se puede usar modelFor , como obtenerlos de la tienda.

 // app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });

Error común n.º 5: confundir el contexto en el que se activa la acción de un componente

Los componentes anidados siempre han sido una de las partes más difíciles de razonar de Ember. Con la introducción de parámetros de bloque en Ember 1.10, gran parte de esta complejidad se ha aliviado, pero en muchas situaciones, todavía es complicado ver de un vistazo en qué componente se activará una acción, disparada desde un componente secundario.

Supongamos que tenemos un componente band-list que tiene band-list-items , y podemos marcar cada banda como favorita en la lista.

 // app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}

El nombre de la acción que debe invocarse cuando el usuario hace clic en el botón se pasa al componente band-list-item y se convierte en el valor de su propiedad faveAction .

Veamos ahora la plantilla y la definición de componentes de band-list-item :

 // app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "faveBand"}}>Fave this</button>
 // app/components/band-list-item.js export default Ember.Component.extend({ band: null, faveAction: '', actions: { faveBand: { this.sendAction('faveAction', this.get('band')); } } });

Cuando el usuario hace clic en el botón "Fave this", se activa la acción faveBand , que activa la faveAction del componente que se pasó ( setAsFavorite , en el caso anterior), en su componente principal , band-list .

Eso hace tropezar a muchas personas, ya que esperan que la acción se active de la misma manera que las acciones de las plantillas basadas en rutas, en el controlador (y luego burbujeando en las rutas activas). Lo que empeora esto es que no se registra ningún mensaje de error; el componente principal simplemente se traga el error.

La regla general es que las acciones se disparan en el contexto actual. En el caso de las plantillas que no son componentes, ese contexto es el controlador actual, mientras que en el caso de las plantillas de componentes, es el componente principal (si lo hay), o nuevamente el controlador actual si el componente no está anidado.

Entonces, en el caso anterior, el componente band-list tendría que volver a disparar la acción recibida del band-list-item para enviarla al controlador o la ruta.

 // app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });

Si la band-list de bandas se definió en la plantilla de bands , entonces la acción setFavoriteBand tendría que manejarse en el controlador de bands o la ruta de bands (o una de sus rutas principales).

Plan de Mitigación de Ember

Puede imaginar que esto se vuelve más complejo si hay más niveles de anidamiento (por ejemplo, al tener un componente fav-button dentro band-list-item ). Tienes que perforar un agujero a través de varias capas desde el interior para sacar tu mensaje, definiendo nombres significativos en cada nivel ( setAsFavorite , favoriteAction , faveAction , etc.)

Esto se simplifica con el "RFC de acciones mejoradas", que ya está disponible en la rama maestra, y probablemente se incluirá en 1.13.

El ejemplo anterior se simplificaría a:

 // app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band setFavBand=(action "setFavoriteBand")}} {{/each}}
 // app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "setFavBand" band}}>Fave this</button>

Error común n.º 6: usar propiedades de matriz como claves dependientes

Las propiedades calculadas de Ember dependen de otras propiedades, y el desarrollador debe definir explícitamente esta dependencia. Digamos que tenemos una propiedad isAdmin que debería ser verdadera si y solo si uno de los roles es admin . Así es como uno podría escribirlo:

 isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')

Con la definición anterior, el valor de isAdmin solo se invalida si el objeto de la matriz de roles cambia, pero no si se agregan o eliminan elementos de la matriz existente. Existe una sintaxis especial para definir que las adiciones y eliminaciones también deben desencadenar un recálculo:

 isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')

Error común n.° 7: no utilizar métodos amigables para el observador

Ampliemos el ejemplo (ahora corregido) del error común n.º 6 y creemos una clase de usuario en nuestra aplicación.

 var User = Ember.Object.extend({ initRoles: function() { var roles = this.get('roles'); if (!roles) { this.set('roles', []); } }.on('init'), isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]') });

Cuando agregamos el rol de admin a dicho User , nos llevamos una sorpresa:

 var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?

El problema es que los observadores no se activarán (y, por lo tanto, las propiedades calculadas no se actualizarán) si se utilizan los métodos Javascript estándar. Esto podría cambiar si mejora la adopción global de Object.observe en los navegadores, pero hasta entonces, tenemos que usar el conjunto de métodos que proporciona Ember. En el caso actual, pushObject es el equivalente amigable para el observador de push :

 user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!

Error común n.º 8: mutación aprobada en propiedades en componentes

Imagine que tenemos un componente star-rating que muestra la clasificación de un elemento y permite configurar la clasificación del elemento. La calificación puede ser para una canción, un libro o la habilidad de regate de un jugador de fútbol.

Lo usarías así en tu plantilla:

 {{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}

Supongamos además que el componente muestra estrellas, una estrella completa para cada punto y estrellas vacías después de eso, hasta una calificación máxima. Cuando se hace clic en una estrella, se activa una acción set en el controlador y debe interpretarse como que el usuario desea actualizar la calificación. Podríamos escribir el siguiente código para lograr esto:

 // app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); item.set('rating', newRating); return item.save(); } } });

Eso haría el trabajo, pero hay un par de problemas con eso. En primer lugar, se supone que el elemento pasado tiene una propiedad de rating , por lo que no podemos usar este componente para administrar la habilidad de regate de Leo Messi (donde esta propiedad podría llamarse score ).

En segundo lugar, modifica la calificación del elemento en el componente. Esto conduce a escenarios en los que es difícil ver por qué cambia una determinada propiedad. Imagina que tenemos otro componente en la misma plantilla donde también se usa esa calificación, por ejemplo, para calcular el puntaje promedio del jugador de fútbol.

La consigna para mitigar la complejidad de este escenario es “Data down, actions up” (DDAU). Los datos deben transmitirse (desde la ruta hasta el controlador y los componentes), mientras que los componentes deben usar acciones para notificar a su contexto sobre los cambios en estos datos. Entonces, ¿cómo se debe aplicar DDAU aquí?

Agreguemos un nombre de acción que debe enviarse para actualizar la calificación:

 {{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}

Y luego use ese nombre para enviar la acción:

 // app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); this.sendAction('setAction', { item: this.get('item'), rating: newRating }); } } });

Finalmente, la acción es manejada aguas arriba, por el controlador o la ruta, y aquí es donde se actualiza la calificación del elemento:

 // app/routes/player.js export default Ember.Route.extend({ actions: { updateRating: function(params) { var skill = params.item, rating = params.rating; skill.set('score', rating); return skill.save(); } } });

Cuando esto sucede, este cambio se propaga hacia abajo a través del enlace pasado al componente star-rating , como resultado, la cantidad de estrellas completas que se muestran cambia.

De esta forma, la mutación no ocurre en los componentes, y dado que la única parte específica de la aplicación es el manejo de la acción en la ruta, la reutilización del componente no se ve afectada.

También podríamos usar el mismo componente para las habilidades futbolísticas:

 {{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}

Ultimas palabras

Es importante tener en cuenta que algunos (¿la mayoría?) de los errores que he visto cometer a la gente (o que he cometido yo mismo), incluidos los que he escrito aquí, desaparecerán o se mitigarán en gran medida al principio de la serie 2.x. de Ember.js.

Lo que queda se aborda en mis sugerencias anteriores, por lo que una vez que esté desarrollando en Ember 2.x, ¡no tendrá excusa para cometer más errores! Si desea este artículo en formato pdf, diríjase a mi blog y haga clic en el enlace en la parte inferior de la publicación.

Sobre mí

Llegué al mundo front-end con Ember.js hace dos años y estoy aquí para quedarme. Me entusiasmé tanto con Ember que comencé a bloguear intensamente tanto en publicaciones de invitados como en mi propio blog, además de presentarme en conferencias. Incluso escribí un libro, Rock and Roll con Ember.js , para cualquiera que quiera aprender sobre Ember. Puede descargar un capítulo de muestra aquí.