Aurelia vs. Angular 2: una comparación de código

Publicado: 2022-03-11

Angular y Aurelia, descendientes del bueno y antiguo JavaScript Angular 1, son feroces competidores, desarrollados y lanzados aproximadamente al mismo tiempo y con una filosofía similar, pero difieren en varias formas clave. En este artículo, haremos comparaciones en paralelo de esas diferencias en características y código.

Para resumir, Aurelia fue creada por Rob Eisenberg, conocido como el creador de Durandal y Caliburn. Trabajó en el equipo de Angular 2 en Google, pero se fue en 2014 cuando sus puntos de vista sobre cómo debería verse un marco moderno diferían de los de ellos.

Las similitudes también persisten en un nivel más técnico: las plantillas y los componentes (o elementos personalizados) asociados con ellas son fundamentales para las aplicaciones Angular y Aurelia, y ambas requieren que tenga un componente raíz (es decir, la aplicación). Además, tanto Angular como Aurelia utilizan mucho los decoradores para la configuración de componentes. Cada componente tiene un ciclo de vida fijo al que podemos engancharnos.

Entonces, ¿cuál es la diferencia entre Aurelia y Angular 2?

La diferencia clave, según Rob Eisenberg, está en el código: Aurelia es discreta. Al desarrollar una aplicación Aurelia (después de la configuración), escribe en ES6 o TypeScript, y las plantillas se ven como HTML absolutamente cuerdo, especialmente en comparación con Angular. Aurelia es una convención sobre la configuración, y el 95 % de las veces estará bien usando las convenciones predeterminadas (como nombres de plantillas, nombres de elementos, etc.), mientras que Angular requiere que proporcione la configuración para básicamente todo.

Aurelia también se considera más compatible con los estándares, aunque solo sea porque no distingue entre mayúsculas y minúsculas cuando se trata de etiquetas HTML, mientras que Angular 2 sí lo hace. Esto significa que Angular 2 no puede depender del analizador HTML del navegador, por lo que crearon el suyo propio.

Otro factor a considerar al elegir entre marcos de SPA es la comunidad, el ecosistema, que los rodea. Tanto Angular como Aurelia tienen todos los elementos básicos (enrutador, motor de plantillas, validación, etc.), y es fácil obtener un modal nativo o usar alguna biblioteca de terceros, pero no sorprende que Angular tenga una comunidad más grande y un mayor desarrollo. equipo.

Además, si bien ambos marcos son de código abierto, Google desarrolla principalmente Angular y no está destinado a comercializarse, mientras que Durandal, Inc., que emplea al equipo central, sigue el modelo de monetización de Ember.js a través de consultoría y capacitación.

Aurelia vs. Angular: una comparación de código

Veamos algunas de las características más notables que subrayan las filosofías detrás de cada marco.

Después de clonar proyectos semilla para Angular y Aurelia, tenemos una aplicación ES6 Aurelia (puede usar Jspm/System.js, Webpack y RequireJS en combinación con ES6 o TypeScript) y una aplicación TypeScript Angular (WebPack), respectivamente.

Vamos a rodar.

El enlace de datos

Antes de comparar ejemplos de trabajo uno al lado del otro, tenemos que echar un vistazo a algunas diferencias sintácticas entre Aurelia y Angular 2, concretamente en la funcionalidad clave de vincular valores desde el controlador a la vista. Angular 1 usó "verificación sucia" para todo, un método que escaneaba el alcance de los cambios. Esto, comprensiblemente, causó una serie de problemas de rendimiento. Ni Angular 2 ni Aurelia siguieron ese camino. En su lugar, utilizan el enlace de eventos.

Enlace de datos en Angular 2

En Angular, vincula datos con corchetes y usa paréntesis para vincular eventos, así:

 <element [property]="value"></a> <element (someEvent)="eventHandler($event)"></a>

El enlace bidireccional, para cuando desea que los cambios en los datos de la aplicación se reflejen en la vista y viceversa, es una combinación de corchetes y paréntesis. Entonces, para la entrada enlazada bidireccional, esto funcionaría bien:

 <input type="text" [(ngModel)]="text"> {{text}}

En otras palabras, los paréntesis representan un evento mientras que los corchetes representan un valor que se ingresa.

El equipo de Angular hizo un gran trabajo al separar las direcciones de enlace: al DOM, desde el DOM y bidireccional. También hay mucho azúcar de sintaxis relacionado con las clases y estilos de enlace. Considere, por ejemplo, el siguiente fragmento como un ejemplo de enlace unidireccional:

 <div [class.red-container]="isRed"></div> <div [style.width.px]="elementWidth"></div>

Pero, ¿qué sucede si queremos vincular datos bidireccionales en un componente? Considere la siguiente configuración de entrada básica:

 <!-- parent component --> <input type="text" [(ngModel)]="text"> {{ text }} <my-component [(text)]="text"></my-component> import {Component, Input} from '@angular/core'; @Component(/* ... */) export class MyComponent { @Input() text : string; } <!-- child component --> <input [(ngModel)]="text"> Text in child: {{ text }}

Tenga en cuenta que, para usar ngModel , su módulo debe importar FormsModule desde @angular/forms . Ahora tenemos algo interesante. Actualizar el valor en la entrada principal cambia los valores en todas partes, pero modificar la entrada del elemento secundario solo afecta a ese elemento secundario. Si queremos que actualice el valor del padre, necesitamos un evento que informe al padre. La convención de nomenclatura para este evento es property name + 'Change' , así:

 import {Component, Input, Output, EventEmitter} from '@angular/core'; @Component(/* ... */) export class MyComponent { @Input() text : string; @Output() textChange = new EventEmitter(); triggerUpdate() { this.textChange.emit(this.text); } }

El enlace bidireccional comienza a funcionar correctamente justo después de vincularnos al evento ngModelChange :

 <!-- child component --> <input [(ngModel)]="text" (ngModelChange)="triggerUpdate($event)">

¿Qué pasa con el enlace de una sola vez, cuando le dices al marco que ignore los valores enlazados incluso si cambian?

En Angular 1, usamos {{::value}} para enlazar una vez. En Angular 2, el enlace único se complica: la documentación dice que puede usar el atributo changeDetection: ChangeDetectionStrategy.OnPush en la configuración del Componente, pero eso hará que todos sus enlaces sean únicos.

Vinculación de datos: la forma de Aurelia

A diferencia de Angular 2, vincular datos y eventos en Aurelia es realmente simple. Puede usar la interpolación, al igual que la property="${value}" , o usar uno de los siguientes tipos de enlace:

 property.one-time="value" property.one-way="value" property.two-way="value"

Los nombres se explican por sí mismos. Además, existe property.bind="value" , ​​que es azúcar de sintaxis que detecta automáticamente si el enlace debe ser unidireccional o bidireccional. Considerar:

 <!-- parent--> <template bindable="text"> <input type="text" value.bind="text"/> <child text.two-way="text"></child> </template> <!-- child custom element --> <template bindable="text"> <input type="text" value.bind="text"/> </template>

En el fragmento anterior, tanto @bindable como @Input son configurables, por lo que puede cambiar fácilmente cosas como el nombre de la propiedad que se vincula, etc.

¿Qué pasa con los eventos? Para enlazar eventos en Aurelia, usa .trigger y .delegate . Por ejemplo, para que un componente secundario active un evento, puede hacer lo siguiente:

 // child.js this.element.dispatchEvent(new CustomEvent('change', { detail: someDetails }));

Luego, para escuchar eso en el padre:

 <child change.trigger="myChangeHandler($event)"></child> <!-- or --> <child change.delegate="myChangeHandler($event)"></child>

La diferencia entre esos dos es que .trigger crea un controlador de eventos en ese elemento en particular, mientras que .delegate agrega un oyente en el document . Esto ahorra recursos, pero obviamente no funcionará para eventos que no sean burbujeantes.

Un ejemplo básico de Aurelia vs. Angular

Ahora que hemos cubierto el enlace, vamos a crear un componente básico que represente un gráfico vectorial escalable (SVG). Será increíble, por lo tanto, lo llamaremos awesome-svg . Este ejercicio ilustrará tanto la funcionalidad básica como la filosofía de Aurelia y Angular 2. Los ejemplos de código de Aurelia de este artículo están disponibles en GitHub.

Ejemplo de Rectángulos SVG en Aurelia

Primero construyamos el archivo JavaScript:

 // awesome-svg.js import {bindable} from 'aurelia-framework'; export class AwesomeSvgCustomElement { @bindable title; @bindable colors = []; }

Ahora para el HTML.

En Aurelia, puede especificar la plantilla (o usar la en línea) con las anotaciones @template , @inlineView o incluso @noView , pero fuera de la caja, busca el archivo .html con el mismo nombre que el .js expediente. Lo mismo ocurre con el nombre del elemento personalizado: puede configurarlo con @customElement('awesome-svg') , pero si no lo hace, Aurelia convertirá el título en guión y buscará una coincidencia.

Como no especificamos lo contrario, el elemento se llamará awesome-svg y Aurelia buscará la plantilla con el mismo nombre que el archivo js (es decir awesome-svg.html ) en el mismo directorio:

 <!-- awesome-svg.html --> <template> <h1>${title}</h1> <svg> <rect repeat.for="color of colors" fill.bind="color" x.bind="$index * 100" y="0" width="50" height="50"></rect> </svg> </template>

¿Observe la etiqueta <template> ? Todas las plantillas deben incluirse en una etiqueta <template> . También vale la pena notar que usas `for... of and the string interpolation ${title}` tal como lo haces en ES6.

Ahora, para usar el componente, debemos importarlo en una plantilla con <require from="path/to/awesome-svg"></require> o, si se usa en toda la aplicación, globalizar el recurso en la función de configuración del marco con aurelia.use.globalResources('path/to/awesome-svg'); , que importará el componente awesome-svg una vez por todas.

[Tenga en cuenta que si no hace nada de esto, <awesome-svg></awesome-svg> se tratará como cualquier otra etiqueta HTML, sin errores].

Puede mostrar el componente con:

 <awesome-svg colors.bind="['#ff0000', '#00ff00', '#0000ff']"></awesome-svg>

Esto representa un conjunto de 3 rectángulos:

Ejemplo de Rectángulos SVG en Aurelia

Ejemplo de rectángulos SVG en Angular 2

Ahora hagamos el mismo ejemplo en Angular 2, también disponible en GitHub. Angular 2 requiere que especifiquemos tanto la plantilla como el nombre del elemento:

 // awesome-svg.component.ts import {Component, Input} from '@angular/core'; @Component({ selector: 'awesome-svg', templateUrl: './awesome-svg.component.html' }) export class AwesomeSvgComponent { @Input() title : string; @Input() colors : string[] = [] }

La vista es donde las cosas se complican un poco. En primer lugar, Angular trata silenciosamente las etiquetas HTML desconocidas de la misma manera que lo hace un navegador: dispara un error que dice algo como my-own-tag es un elemento desconocido. Hace lo mismo con cualquier propiedad que enlace, por lo que si tuviera un error tipográfico en alguna parte del código, llamaría mucho la atención porque la aplicación fallaría. Suena bien, ¿verdad? Sí, porque te darías cuenta de inmediato si rompieras la aplicación, y no, porque esto es simplemente una mala forma.

Considere este fragmento, que está perfectamente bien en términos de sintaxis vinculante:

 <svg> <rect [fill]="color"></rect> </svg>

Aunque se lee bien, obtendrá un error como "no se puede enlazar con 'rellenar' ya que no es una propiedad conocida de ':svg:rect'". Para arreglar esto, necesitas usar la sintaxis [attr.fill]="color" en su lugar. También tenga en cuenta que es necesario especificar el espacio de nombres en los elementos secundarios dentro de <svg/>: <svg:rect> para que Angular sepa que esto no debe tratarse como HTML. Expandamos nuestro fragmento:

 <!-- awesome-svg.component.html--> <h1>{{ title }}</h1> <svg> <rect *ngFor="let color of colors; let i = index" [attr.fill]="color" [attr.x]="i * 100" y="0" width="50" height="50" ></rect> </svg>

Aquí vamos. A continuación, impórtalo en la configuración del módulo:

 @NgModule({ declarations: [ AwesomeSvgComponent ] //... })

Ahora el componente se puede usar en este módulo, así:

 <awesome-svg [colors]="['#ff0000', '#00ff00', '#0000ff']" title="Rectangles"></awesome-svg> 

Ejemplo de rectángulos SVG en AngularJS 2

Elementos personalizados

Supongamos ahora que queremos que nuestro código de rectángulo sea un componente personalizado con su propia lógica.

Elementos personalizados: The Angular 2 Way

Dado que Angular 2 representa los componentes por lo que coincide con su selector definido, es extremadamente fácil definir un componente personalizado, así:

 @Component({ selector: 'g[custom-rect]', ... })

El fragmento anterior representaría el elemento personalizado en cualquier <g custom-rect></div> , lo cual es extremadamente útil.

Elementos personalizados: el estilo de Aurelia

Aurelia nos permite crear elementos personalizados solo de plantilla:

 <template bindable="colors, title"> <h1>${title}</h1> <svg> <rect repeat.for="color of colors" fill.bind="color" x.bind="$index * 100" y="0" width="50" height="50"></rect> </svg> </template>

El elemento personalizado se nombrará con respecto al nombre del archivo. La única diferencia con respecto a nombrar otros componentes es que al importar, ya sea en configure o mediante la etiqueta <require> , debe poner .html al final. Entonces, por ejemplo: <require from="awesome-svg.html"></require> .

Aurelia también tiene atributos personalizados, pero no cumplen el mismo propósito que en Angular 2. Por ejemplo, en Aurelia, puede usar la anotación @containerless en el elemento rect personalizado. @containerless también se puede usar con plantillas personalizadas sin el controlador y <compose> , que básicamente procesa cosas en el DOM.

Considere el siguiente código que contiene la anotación @containerless :

 <svg> <custom-rect containerless></custom-rect> </svg>

La salida no contendría la etiqueta del elemento personalizado ( custom-rect ), sino que obtendríamos:

 <svg> <rect ...></rect> </svg>

Servicios

En cuanto a los servicios, Aurelia y Angular son muy similares, como verás en los siguientes ejemplos. Supongamos que necesitamos NumberOperator que depende de NumberGenerator .

Servicios en Aurelia

Así es como se definen nuestros dos servicios en Aurelia:

 import {inject} from 'aurelia-framework'; import {NumberGenerator} from './number-generator' export class NumberGenerator { getNumber(){ return 42; } } @inject(NumberGenerator) export class NumberOperator { constructor(numberGenerator){ this.numberGenerator = numberGenerator; this.counter = 0; } getNumber(){ return this.numberGenerator.getNumber() + this.counter++; } }

Ahora, para un componente, inyectamos de la misma manera:

 import {inject} from 'aurelia-framework'; import {NumberOperator} from './_services/number-operator'; @inject(NumberOperator) export class SomeCustomElement { constructor(numberOperator){ this.numberOperator = numberOperator; //this.numberOperator.getNumber(); } }

Como puede ver, con la inyección de dependencia, cualquier clase puede ser un servicio completamente extensible, por lo que incluso puede escribir sus propios resolutores.

Fábricas en Aurelia

Si lo que necesita es una fábrica o una nueva instancia ( Factory y NewInstance son solo un par de resolutores populares que se proporcionan listos para usar), puede hacer lo siguiente:

 import { Factory, NewInstance } from 'aurelia-framework'; @inject(SomeService) export class Stuff { constructor(someService, config){ this.someService = someService; } } @inject(Factory.of(Stuff), NewInstance.of(AnotherService)) export class SomethingUsingStuff { constructor(stuffFactory, anotherService){ this.stuff = stuffFactory(config); this.anotherServiceNewInstance = anotherService; } }

Servicios angulares

Aquí está el mismo conjunto de servicios en Angular 2:

 import { Injectable } from '@angular/core'; import { NumberGenerator } from './number-generator'; @Injectable() export class NumberGenerator { getNumber(){ return 42; } } @Injectable() export class NumberOperator { counter : number = 0; constructor(@Inject(NumberGenerator) private numberGenerator) { } getNumber(){ return this.numberGenerator.getNumber() + this.counter++; } }

Se requiere la anotación @Injectable , y para inyectar realmente un servicio, debe especificar el servicio en la lista de proveedores en la configuración del componente o en la configuración del módulo completo, así:

 @Component({ //... providers: [NumberOperator, NumberGenerator] })

O, no recomendado, también puede especificarlo en la bootstrap(AppComponent, [NumberGenerator, NumberOperator]) .

Tenga en cuenta que debe especificar tanto NumberOperator como NumberGenerator , independientemente de cómo lo inyecte.

El componente resultante se verá así:

 @Component({ //... providers: [NumberOperator, NumberGenerator], }) export class SomeComponent { constructor(@Inject(NumberOperator) public service){ //service.getNumber(); } }
Fábricas en Angular 2

En Angular 2, puede crear fábricas con la anotación de provide , que también se usa para servicios de alias para evitar colisiones de nombres. La creación de una fábrica podría parecerse a lo siguiente:

 let stuffFactory = (someService: SomeService) => { return new Stuff(someService); } @Component({ //... providers: [provide(Stuff, {useFactory: stuffFactory, deps: [SomeService]})] })

transclusión

Angular 1 tenía la capacidad de incluir contenido, una "ranura", de una plantilla a otra mediante la transclusión. Veamos qué tienen para ofrecer sus descendientes.

Proyección de contenido con Angular 2

En Angular 2, la transclusión se llama "proyección de contenido" y funciona de la misma manera que lo hizo ng-transclude ; es decir, hablando en términos de Angular 1, el contenido transcluido usa el ámbito principal. Coincidirá con la etiqueta de contenido transcluido según el selector de configuración. Considerar:

 @Component({ selector: 'child', template: `Transcluded: <ng-content></ng-content>` }) export class MyComponent {}

Luego puede usar el componente con <child-component>Hello from Translusion Component</child-component> , y obtendremos el Yes HTML transcluido exacto representado en el componente secundario.

Para la transclusión de múltiples ranuras, Angular 2 tiene selectores que puede usar de la misma manera que lo haría para la configuración de @Component :

 <!-- child.component.html --> <h4>Slot 1:</h4> <ng-content select=".class-selector"></ng-content> <h4>Slot 2:</h4> <ng-content select="[attr-selector]"></ng-content>
 <!-- parent.component.html --> <child> <span class="class-selector">Hello from Translusion Component</span> <p class="class-selector">Hello from Translusion Component again</p> <span attr-selector>Hello from Translusion Component one more time</span> </child> 

Ranuras en Angular2

Puede usar select en sus etiquetas personalizadas, pero recuerde que Angular 2 debe conocer la etiqueta.

Tragamonedas con Aurelia

¿Recuerdas cuando dije que Aurelia sigue los estándares web siempre que sea posible? En Aurelia, la transclusión se llama ranuras, y es solo un polyfill para Web Components Shadow DOM. Shadow DOM aún no se ha creado para tragamonedas, pero sigue las especificaciones W3C.

 <!-- child --> <template> Slot: <slot></slot> </template> <!-- parent --> <template> <child>${textValue}</child> </template>

Aurelia se diseñó para cumplir con los estándares y Angular 2 no. Como resultado, podemos hacer cosas más asombrosas con las máquinas tragamonedas de Aurelia, como usar contenido alternativo (intentar usar contenido alternativo en Angular 2 falla con <ng-content> element cannot have content ). Considerar:

 <!-- child --> <template> Slot A: <slot name="slot-a"></slot> <br /> Slot B: <slot name="slot-b"></slot> Slot C: <slot name="slot-c">Fallback Content</slot> </template> <!-- parent --> <template> <child> <div slot="slot-a">A value</div> <div slot="slot-b">B value</div> </child> </template>

De la misma manera que Angular 2, Aurelia representará todas las apariciones de la ranura en función de una coincidencia de nombre.

Tragamonedas en Aurelia

También vale la pena señalar que tanto en Aurelia como en Angular, puede compilar partes de plantilla y representar componentes dinámicamente (usando <compose> con view-model en Aurelia o ComponentResolver en Angular 2).

Sombra DOM

Tanto Aurelia como Angular admiten Shadow DOM.

En Aurelia, solo use el decorador @useShadowDOM y estará listo para comenzar:

 import {useShadowDOM} from 'aurelia-framework'; @useShadowDOM() export class YetAnotherCustomElement {}

En Angular, se puede hacer lo mismo con ViewEncapsulation.Native :

 import { Component, ViewEncapsulation } from '@angular/core'; @Component({ //... encapsulation: ViewEncapsulation.Native, }) export class YetAnotherComponent {}

Recuerde comprobar si su navegador es compatible con Shadow DOM.

Representación del lado del servidor

Estamos en 2017 y el renderizado del lado del servidor está muy de moda. Ya puedes renderizar Angular 2 en el back-end con Angular Universal, y Aurelia tendrá esto en 2017 como se indica en las resoluciones de Año Nuevo de su equipo. De hecho, hay una demostración ejecutable en el repositorio de Aurelia.

Además de eso, Aurelia ha tenido capacidades de mejora progresiva durante más de un año, algo que Angular 2 no tiene debido a su sintaxis HTML no estándar.

Tamaño, rendimiento y lo que viene después

Si bien no nos muestra la imagen completa, los puntos de referencia de DBMonster, con configuraciones predeterminadas e implementación optimizada, pintan una buena imagen de comparación: Aurelia y Angular muestran resultados similares de aproximadamente 100 renderizaciones por segundo (como se probó en una MacBook Pro), mientras que Angular 1 mostró algo alrededor de la mitad de ese resultado. Tanto Aurelia como Angular superan a Angular 1 unas cinco veces, y ambos están un 40 % por delante de React. Ni Aurelia ni Angular 2 tienen una implementación de Virtual DOM.

En cuanto al tamaño, Angular es aproximadamente el doble de grande que Aurelia, pero los chicos de Google están trabajando en ello: la hoja de ruta de Angular incluye el lanzamiento de Angular 4 con planes para hacerlo más pequeño y liviano mientras se mejora la experiencia del desarrollador. No hay Angular 3 y, en realidad, el número de versión debe omitirse cuando se habla de Angular, ya que los lanzamientos principales están planificados cada 6 meses. Si observa el camino que tomó Angular de alfa a su versión actual, verá que no siempre fue coherente con cosas como el cambio de nombre de los atributos de una compilación a otra, etc. El equipo de Angular promete que romper los cambios será fácil. migrar.

A partir de 2017, el equipo de Aurelia planea lanzar Aurelia UX, proporcionar más integraciones y herramientas e implementar la representación del lado del servidor (que ha estado en la hoja de ruta durante mucho tiempo).

Angular 2 vs. Aurelia: Cuestión de Gustos

Tanto Angular como Aurelia son buenos, y elegir uno u otro es una cuestión de gustos y prioridades personales. Si necesita una solución más delgada, Aurelia es su mejor opción, pero si necesita el apoyo de la comunidad, Angular es su ganador. No es una cuestión de "¿Este marco me permite...?" porque la respuesta es simplemente “sí”. Proporcionan aproximadamente la misma funcionalidad pero siguen diferentes filosofías y estilos junto con un enfoque completamente diferente de los estándares web.