Aurelia vs. Angular 2 - Uma comparação de código

Publicados: 2022-03-11

Angular e Aurelia, descendentes do bom e velho JavaScript Angular 1, são concorrentes ferozes, desenvolvidos e lançados aproximadamente ao mesmo tempo e com uma filosofia semelhante, mas diferindo em vários aspectos importantes. Neste artigo, faremos comparações lado a lado dessas diferenças em recursos e código.

Para encurtar a história, Aurelia foi criada por Rob Eisenberg, conhecido como o criador de Durandal e Caliburn. Ele trabalhou na equipe Angular 2 no Google, mas saiu em 2014 quando suas opiniões sobre como uma estrutura moderna deveria ser diferente da deles.

As semelhanças também persistem em um nível mais técnico: os modelos e os componentes (ou elementos personalizados) associados a eles são essenciais para os aplicativos Angular e Aurelia, e ambos exigem que você tenha um componente raiz (ou seja, o aplicativo). Além disso, tanto o Angular quanto o Aurelia usam muito decoradores para configuração de componentes. Cada componente tem um ciclo de vida fixo ao qual podemos nos conectar.

Então, qual é a diferença entre Aurelia e Angular 2?

A principal diferença, de acordo com Rob Eisenberg, está no código: Aurelia é discreta. Ao desenvolver um aplicativo Aurelia (após a configuração), você escreve em ES6 ou TypeScript, e os modelos parecem HTML absolutamente sensato, especialmente quando comparados ao Angular. Aurelia é convenção sobre configuração, e 95% das vezes você ficará bem usando convenções padrão (como nomeação de modelo, nomeação de elemento, etc.), enquanto o Angular exige que você forneça configuração para basicamente tudo.

O Aurelia também é considerado mais compatível com os padrões, mesmo porque não diferencia maiúsculas de minúsculas quando se trata de tags HTML, enquanto o Angular 2 é. Isso significa que o Angular 2 não pode confiar no analisador HTML do navegador, então eles criaram o seu próprio.

Outro fator a ser considerado ao escolher entre estruturas de SPA é a comunidade – o ecossistema – ao seu redor. Tanto o Angular quanto o Aurelia têm todo o básico (roteador, mecanismo de modelo, validação, etc), e é fácil obter um modal nativo ou usar alguma biblioteca de terceiros, mas não é surpresa que o Angular tenha uma comunidade maior e maior desenvolvimento equipe.

Além disso, embora ambas as estruturas sejam de código aberto, o Angular é desenvolvido principalmente pelo Google e não se destina a ser comercializado enquanto a Durandal, Inc., que emprega a equipe principal, segue o modelo de monetização da Ember.js por meio de consultoria e treinamento.

Aurelia vs. Angular: uma comparação de código

Vejamos alguns dos recursos mais notáveis ​​que sublinham as filosofias por trás de cada estrutura.

Após a clonagem de projetos seed para Angular e Aurelia, temos um aplicativo ES6 Aurelia (você pode usar Jspm/System.js, Webpack e RequireJS em combinação com ES6 ou TypeScript) e um aplicativo TypeScript Angular (WebPack), respectivamente.

Vamos rolar.

Ligação de dados

Antes de compararmos exemplos de trabalho lado a lado, temos que dar uma olhada em algumas diferenças sintáticas entre Aurelia e Angular 2, ou seja, na funcionalidade chave de vincular valores do controlador à visualização. O Angular 1 usava “verificação suja” para tudo, um método que verificava o escopo para alterações. Isso, compreensivelmente, causou uma série de problemas de desempenho. Nem Angular 2 nem Aurelia seguiram esse caminho. Em vez disso, eles usam a associação de eventos.

Ligação de dados em Angular 2

Em Angular, você vincula dados com colchetes e usa parênteses para vincular eventos, assim:

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

A associação bidirecional — para quando você deseja que as alterações nos dados do aplicativo reflitam na exibição e vice-versa — é uma combinação de colchetes e colchetes. Portanto, para entrada vinculada bidirecional, isso funcionaria bem:

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

Em outras palavras, os parênteses representam um evento, enquanto os colchetes representam um valor que está sendo enviado para entrada.

A equipe Angular fez um ótimo trabalho separando as direções de ligação: para o DOM, do DOM e bidirecional. Há também muito açúcar de sintaxe relacionado a classes e estilos de associação. Considere, por exemplo, o snippet a seguir como um exemplo de vinculação unidirecional:

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

Mas e se quisermos vincular dados bidirecionais em um componente? Considere a seguinte configuração básica de entrada:

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

Observe que, para usar ngModel , seu módulo deve importar FormsModule de @angular/forms . Agora temos algo interessante. A atualização do valor na entrada pai altera os valores em todos os lugares, mas a modificação da entrada do filho afeta apenas esse filho. Se quisermos atualizar o valor pai, precisamos de um evento informando o pai. A convenção de nomenclatura para este evento é property name + 'Change' , assim:

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

A ligação bidirecional começa a funcionar corretamente logo após a vinculação ao evento ngModelChange :

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

E quanto à vinculação única, quando você efetivamente diz à estrutura para ignorar os valores vinculados, mesmo que eles mudem?

Em Angular 1, usamos {{::value}} para vincular uma vez. No Angular 2, a vinculação única fica complicada: a documentação diz que você pode usar o atributo changeDetection: ChangeDetectionStrategy.OnPush na configuração do Componente, mas isso fará todas as suas vinculações uma única vez.

Vinculação de dados: o caminho da Aurélia

Em contraste com o Angular 2, vincular dados e eventos no Aurelia é muito simples. Você pode usar a interpolação, assim como a property="${value}" ou usar um dos seguintes tipos de associação:

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

Os nomes são autoexplicativos. Além disso, há property.bind="value" , ​​que é o açúcar de sintaxe que detecta automaticamente se a ligação deve ser unidirecional ou bidirecional. 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>

No trecho acima, @bindable e @Input são configuráveis, para que você possa alterar facilmente coisas como o nome da propriedade que está sendo vinculada etc.

E os eventos? Para vincular a eventos no Aurelia, você usa .trigger e .delegate . Por exemplo, para que um componente filho acione um evento, você pode fazer o seguinte:

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

Então, para ouvir isso no pai:

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

A diferença entre esses dois é que .trigger cria um manipulador de eventos nesse elemento específico, enquanto .delegate adiciona um ouvinte no document . Isso economiza recursos, mas obviamente não funcionará para eventos não borbulhantes.

Um exemplo básico de Aurelia vs. Angular

Agora que abordamos a vinculação, vamos criar um componente básico que renderiza um gráfico vetorial escalável (SVG). Será incrível, portanto, vamos chamá-lo awesome-svg . Este exercício ilustrará a funcionalidade e a filosofia básicas do Aurelia e do Angular 2. Os exemplos de código do Aurelia deste artigo estão disponíveis no GitHub.

Exemplo de retângulos SVG em Aurelia

Vamos primeiro construir o arquivo JavaScript:

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

Agora para o HTML.

No Aurelia, você pode especificar o template (ou usar o inline) com as anotações @template , @inlineView ou até mesmo @noView , mas fora da caixa, ele procura o arquivo .html com o mesmo nome do .js Arquivo. O mesmo vale para o nome do elemento personalizado - você pode configurá-lo com @customElement('awesome-svg') , mas se não o fizer, Aurelia converterá o título em maiúsculas e minúsculas e procurará uma correspondência.

Como não especificamos o contrário, o elemento será chamado awesome-svg e Aurelia procurará o modelo com o mesmo nome do arquivo js (ou seja awesome-svg.html ) no mesmo diretório:

 <!-- 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 a tag <template> ? Todos os templates precisam ser encapsulados em uma tag <template> . Também vale a pena notar que você usa ` for … of and the string interpolation ${title}` exatamente como você faz no ES6.

Agora, para usar o componente, devemos importá-lo em um modelo com <require from="path/to/awesome-svg"></require> ou, se for usado em todo o aplicativo, globalizar o recurso na função configure do framework com aurelia.use.globalResources('path/to/awesome-svg'); , que importará o componente awesome-svg uma vez por todas.

[Observe, se você não fizer nenhum desses, <awesome-svg></awesome-svg> será tratado como qualquer outra tag HTML, sem erros.]

Você pode exibir o componente com:

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

Isso renderiza um conjunto de 3 retângulos:

Exemplo de retângulos SVG em Aurelia

Exemplo de retângulos SVG em Angular 2

Agora vamos fazer o mesmo exemplo em Angular 2, também disponível no GitHub. Angular 2 requer que especifiquemos o template e o nome do 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[] = [] }

A vista é onde as coisas ficam um pouco complicadas. Em primeiro lugar, o Angular trata silenciosamente tags HTML desconhecidas da mesma forma que um navegador: ele dispara um erro dizendo que algo como my-own-tag é um elemento desconhecido. Ele faz o mesmo para todas as propriedades que você vincula, portanto, se você tivesse um erro de digitação em algum lugar do código, atrairia maior atenção porque o aplicativo travaria. Parece bom, certo? Sim, porque você notaria imediatamente se quebrasse o aplicativo, e não, porque isso é apenas uma forma ruim.

Considere este trecho, que está perfeitamente bem em termos de sintaxe de ligação:

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

Mesmo que leia bem, você receberá um erro como “não é possível vincular a 'preencher' porque não é uma propriedade conhecida de ':svg:rect'.” Para corrigir isso, você precisa usar a sintaxe [attr.fill]="color" em vez disso. Observe também que é necessário especificar namespace em elementos filho dentro de <svg/>: <svg:rect> para que o Angular saiba que isso não deve ser tratado como HTML. Vamos expandir nosso trecho:

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

Aqui vamos nós. Em seguida, importe-o na configuração do módulo:

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

Agora o componente pode ser usado neste módulo, assim:

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

Exemplo de retângulos SVG em AngularJS 2

Elementos personalizados

Suponha agora que queremos que nosso código retangular seja um componente personalizado com sua própria lógica.

Elementos personalizados: O Angular 2 Way

Como o Angular 2 renderiza componentes pelo que corresponde ao seletor definido, é extremamente fácil definir um componente personalizado, assim:

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

O snippet acima renderizaria o elemento personalizado para qualquer tag <g custom-rect></div> , o que é extremamente útil.

Elementos personalizados: o jeito Aurelia

Aurelia nos permite criar elementos personalizados somente de modelo:

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

O elemento personalizado será nomeado em relação ao nome do arquivo. A única diferença de nomear outros componentes é que ao importar, seja no configure ou através da tag <require> , você deve colocar .html no final. Por exemplo: <require from="awesome-svg.html"></require> .

O Aurelia também tem atributos personalizados, mas eles não servem ao mesmo propósito que no Angular 2. Por exemplo, no Aurelia, você pode usar a anotação @containerless no elemento rect personalizado. @containerless também pode ser usado com templates customizados sem o controller e <compose> , que basicamente renderiza coisas no DOM.

Considere o seguinte código contendo a anotação @containerless :

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

A saída não conteria a tag do elemento customizado ( custom-rect ), mas em vez disso obtemos:

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

Serviços

Em relação aos serviços, Aurelia e Angular são muito semelhantes, como você verá nos exemplos a seguir. Suponha que precisamos NumberOperator que depende de NumberGenerator .

Serviços em Aurélia

Veja como definir nossos dois serviços no 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++; } }

Agora, para um componente, injetamos da mesma maneira:

 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 você pode ver, com injeção de dependência, qualquer classe pode ser um serviço totalmente extensível, então você pode até escrever seus próprios resolvedores.

Fábricas em Aurélia

Se o que você precisa é de uma fábrica ou uma nova instância ( Factory e NewInstance são apenas alguns resolvedores populares fornecidos prontos para uso), você pode fazer o seguinte:

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

Serviços angulares

Aqui está o mesmo conjunto de serviços em 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++; } }

A anotação @Injectable é necessária e, para realmente injetar um serviço, você precisa especificar o serviço na lista de provedores na configuração do componente ou em toda a configuração do módulo, assim:

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

Ou, não recomendado, você também pode especificá-lo na bootstrap(AppComponent, [NumberGenerator, NumberOperator]) .

Observe que você precisa especificar NumberOperator e NumberGenerator , independentemente de como você o injeta.

O componente resultante será algo assim:

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

No Angular 2, você pode criar fábricas com a anotação de provide , que também é usada para serviços de alias para evitar colisões de nomes. A criação de uma fábrica pode ter a seguinte aparência:

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

Transclusão

O Angular 1 tinha a capacidade de incluir conteúdo, um “slot”, de um modelo para outro usando transclusão. Vamos ver o que seus descendentes têm a oferecer.

Projeção de conteúdo com Angular 2

Em Angular 2, a transclusão é chamada de “projeção de conteúdo” e funciona da mesma forma que o ng-transclude ; ou seja, falando em termos Angular 1, o conteúdo transcluído usa o escopo pai. Ele corresponderá à tag de conteúdo transcluído com base no seletor de configuração. Considerar:

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

Você pode então usar o componente com <child-component>Hello from Translusion Component</child-component> , e obteremos o HTML exato transcluído Yes renderizado no componente filho.

Para transclusão de vários slots, o Angular 2 possui seletores que você pode usar da mesma maneira que usaria para a configuração do @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> 

Slots em Angular2

Você pode usar select em suas tags personalizadas, mas lembre-se de que a tag deve ser conhecida pelo Angular 2.

Slots com Aurélia

Lembra quando eu disse que a Aurelia segue os padrões da web sempre que possível? No Aurelia, a transclusão é chamada de slots e é apenas um polyfill para Web Components Shadow DOM. Shadow DOM ainda não foi criado para slots, mas segue as especificações do W3C.

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

O Aurelia foi projetado para ser compatível com os padrões e o Angular 2 não. Como resultado, podemos fazer coisas mais incríveis com os slots da Aurelia, como usar o conteúdo de fallback (tentar usar o conteúdo de fallback no Angular 2 falha com <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>

Da mesma forma que Angular 2, Aurelia renderizará todas as ocorrências do slot com base em uma correspondência de nome.

Caça-níqueis em Aurélia

Também vale a pena notar que tanto no Aurelia quanto no Angular, você pode compilar partes do modelo e renderizar componentes dinamicamente (usando <compose> com view-model no Aurelia ou ComponentResolver no Angular 2).

Shadow DOM

Tanto Aurelia quanto Angular suportam Shadow DOM.

No Aurelia, basta usar o decorador @useShadowDOM e pronto:

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

Em Angular, o mesmo pode ser feito com ViewEncapsulation.Native :

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

Lembre-se de verificar se o seu navegador suporta Shadow DOM.

Renderização do lado do servidor

É 2017, e a renderização do lado do servidor está muito na moda. Você já pode renderizar Angular 2 no back-end com Angular Universal, e Aurelia terá isso em 2017, conforme declarado nas resoluções de Ano Novo de sua equipe. Na verdade, há uma demonstração executável no repositório de Aurelia.

Além disso, o Aurelia possui recursos de aprimoramento progressivo há mais de um ano, algo que o Angular 2 não possui devido à sua sintaxe HTML não padrão.

Tamanho, desempenho e o que vem a seguir

Embora não nos mostre toda a imagem, os benchmarks DBMonster, com configurações padrão e implementação otimizada, pintam uma boa imagem de comparação: Aurelia e Angular mostram resultados semelhantes de aproximadamente 100 re-renderizações por segundo (como testado em um MacBook Pro), enquanto Angular 1 mostrou algo em torno da metade desse resultado. Tanto o Aurelia quanto o Angular superam o Angular 1 em cerca de cinco vezes, e ambos estão 40% à frente do React. Nem Aurelia nem Angular 2 têm uma implementação do Virtual DOM.

Na questão do tamanho, o Angular é aproximadamente duas vezes mais gordo que o Aurelia, mas os caras do Google estão trabalhando nisso: o roteiro do Angular inclui o lançamento do Angular 4 com planos de torná-lo menor e mais leve, melhorando a experiência do desenvolvedor. Não há Angular 3 e, na verdade, o número da versão deve ser descartado quando se fala em Angular, já que os principais lançamentos são planejados a cada 6 meses. Se você olhar para o caminho que o Angular tomou de alpha para sua versão atual, você verá que nem sempre foi coerente com coisas como atributos sendo renomeados de build para build, etc. A equipe do Angular promete que quebrar mudanças será fácil migrar.

A partir de 2017, a equipe da Aurelia planeja lançar o Aurelia UX, fornecer mais integrações e ferramentas e implementar a renderização do lado do servidor (que está no roteiro há muito tempo).

Angular 2 vs. Aurelia: uma questão de gosto

Tanto o Angular quanto o Aurelia são bons, e escolher um em detrimento do outro é uma questão de gosto e prioridades pessoais. Se você precisa de uma solução mais enxuta, o Aurelia é sua melhor opção, mas se você precisa de suporte da comunidade, o Angular é o seu vencedor. Não é uma questão de “Esse framework me permite…?” porque a resposta é simplesmente “sim”. Eles fornecem aproximadamente a mesma funcionalidade enquanto seguem filosofias e estilos diferentes, juntamente com uma abordagem completamente diferente aos padrões da web.