Aurelia vs Angular 2 - Une comparaison de code
Publié: 2022-03-11Angular et Aurelia, descendants du bon vieux JavaScript Angular 1, sont de féroces concurrents, développés et publiés à peu près au même moment et avec une philosophie similaire, mais différant sur un certain nombre de points clés. Dans cet article, nous ferons des comparaisons côte à côte de ces différences de fonctionnalités et de code.
Pour faire court, Aurelia a été créée par Rob Eisenberg, connu comme le créateur de Durandal et Caliburn. Il a travaillé dans l'équipe Angular 2 de Google, mais est parti en 2014 lorsque ses opinions sur l'apparence d'un framework moderne différaient des leurs.
Les similitudes persistent également à un niveau plus technique : les modèles et les composants (ou éléments personnalisés) qui leur sont associés sont au cœur des applications Angular et Aurelia, et les deux nécessitent que vous disposiez d'un composant racine (c'est-à-dire l'application). De plus, Angular et Aurelia utilisent fortement les décorateurs pour la configuration des composants. Chaque composant a un cycle de vie fixe auquel nous pouvons nous accrocher.
Alors, quelle est la différence entre Aurelia et Angular 2 ?
La principale différence, selon Rob Eisenberg, réside dans le code : Aurelia est discrète. Lors du développement d'une application Aurelia (après configuration), vous écrivez en ES6 ou TypeScript, et les modèles ressemblent à du HTML absolument sain, surtout par rapport à Angular. Aurelia est une convention sur la configuration, et 95% du temps, vous irez très bien en utilisant les conventions par défaut (telles que la dénomination des modèles, la dénomination des éléments, etc.), tandis qu'Angular vous oblige à fournir une configuration pour pratiquement tout.
Aurelia est également considérée comme plus conforme aux normes, ne serait-ce que parce qu'elle n'est pas sensible à la casse en ce qui concerne les balises HTML, alors qu'Angular 2 l'est. Cela signifie qu'Angular 2 ne peut pas s'appuyer sur l'analyseur HTML du navigateur, ils ont donc créé le leur.
Un autre facteur à prendre en compte lors du choix entre les cadres SPA est la communauté - l'écosystème - qui les entoure. Angular et Aurelia ont toutes les deux les bases (routeur, moteur de modèle, validation, etc.), et il est facile d'obtenir un modal natif ou d'utiliser une bibliothèque tierce, mais il n'est pas surprenant qu'Angular ait une plus grande communauté et un plus grand développement équipe.
De plus, bien que les deux frameworks soient open source, Angular est principalement développé par Google et n'est pas destiné à être commercialisé tandis que Durandal, Inc., qui emploie l'équipe principale, suit le modèle de monétisation d'Ember.js via le conseil et la formation.
Aurelia vs Angular : une comparaison de code
Examinons quelques-unes des caractéristiques les plus remarquables qui soulignent les philosophies derrière chaque framework.
Après avoir cloné des projets de départ pour Angular et Aurelia, nous avons une application ES6 Aurelia (vous pouvez utiliser Jspm/System.js, Webpack et RequireJS en combinaison avec ES6 ou TypeScript) et une application TypeScript Angular (WebPack), respectivement.
Roulons.
Liaison de données
Avant de comparer des exemples de travail côte à côte, nous devons examiner certaines différences syntaxiques entre Aurelia et Angular 2, notamment dans la fonctionnalité clé des valeurs de liaison du contrôleur à la vue. Angular 1 utilisait la "vérification sale" pour tout, une méthode qui analysait la portée des modifications. Ceci, naturellement, a causé un certain nombre de problèmes de performances. Ni Angular 2 ni Aurelia n'ont suivi cette voie. Au lieu de cela, ils utilisent la liaison d'événement.
Liaison de données dans Angular 2
Dans Angular, vous liez des données avec des crochets et utilisez des parenthèses pour lier des événements, comme ceci :
<element [property]="value"></a> <element (someEvent)="eventHandler($event)"></a>
La liaison bidirectionnelle, lorsque vous souhaitez que les modifications apportées aux données d'application se répercutent sur la vue et vice versa, est une combinaison de crochets et de parenthèses. Donc, pour une entrée liée bidirectionnelle, cela fonctionnerait très bien :
<input type="text" [(ngModel)]="text"> {{text}}
En d'autres termes, les parenthèses représentent un événement tandis que les crochets représentent une valeur poussée en entrée.
L'équipe Angular a fait un excellent travail en séparant les directions de liaison : vers le DOM, depuis le DOM et bidirectionnel. Il y a aussi beaucoup de sucre syntaxique lié aux classes et aux styles de liaison. Considérez, par exemple, l'extrait de code suivant comme exemple de liaison unidirectionnelle :
<div [class.red-container]="isRed"></div> <div [style.width.px]="elementWidth"></div>
Mais que se passe-t-il si nous voulons lier des données bidirectionnelles dans un composant ? Considérez la configuration d'entrée de base suivante :
<!-- 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 }}
Notez que, pour utiliser ngModel
, votre module doit importer FormsModule depuis @angular/forms
. Maintenant, nous avons quelque chose d'intéressant. La mise à jour de la valeur dans l'entrée parent modifie les valeurs partout, mais la modification de l'entrée de l'enfant n'affecte que cet enfant. Si nous voulons qu'il mette à jour la valeur parent, nous avons besoin d'un événement informant le parent. La convention de dénomination de cet événement est property name + 'Change'
, comme ceci :
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); } }
La liaison bidirectionnelle commence à fonctionner correctement juste après la liaison à l'événement ngModelChange
:
<!-- child component --> <input [(ngModel)]="text" (ngModelChange)="triggerUpdate($event)">
Qu'en est-il de la liaison unique, lorsque vous dites effectivement au framework d'ignorer les valeurs liées même si elles changent ?
Dans Angular 1, nous avons utilisé {{::value}} pour lier une fois. Dans Angular 2, la liaison unique se complique : la documentation indique que vous pouvez utiliser l'attribut changeDetection: ChangeDetectionStrategy.OnPush
dans la configuration du composant, mais cela rendra toutes vos liaisons uniques.
Liaison de données : la méthode Aurelia
Contrairement à Angular 2, la liaison des données et des événements dans Aurelia est vraiment simple. Vous pouvez utiliser l'interpolation, tout comme la property="${value}"
d'Angular, ou utiliser l'un des types de liaison suivants :
property.one-time="value" property.one-way="value" property.two-way="value"
Les noms sont explicites. De plus, il y a property.bind="value"
, qui est syntax-sugar qui détecte automatiquement si la liaison doit être unidirectionnelle ou bidirectionnelle. Considérer:
<!-- 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>
Dans l'extrait ci-dessus, @bindable
et @Input
sont configurables, vous pouvez donc facilement modifier des éléments tels que le nom de la propriété liée, etc.
Qu'en est-il des événements ? Pour vous lier à des événements dans Aurelia, vous utilisez .trigger
et .delegate
. Par exemple, pour qu'un composant enfant déclenche un événement, procédez comme suit :
// child.js this.element.dispatchEvent(new CustomEvent('change', { detail: someDetails }));
Ensuite, pour écouter cela dans le parent:
<child change.trigger="myChangeHandler($event)"></child> <!-- or --> <child change.delegate="myChangeHandler($event)"></child>
La différence entre ces deux est que .trigger
crée un gestionnaire d'événements sur cet élément particulier tandis que .delegate
ajoute un écouteur sur document
. Cela économise des ressources, mais cela ne fonctionnera évidemment pas pour les événements non bouillonnants.
Un exemple de base d'Aurelia contre Angular
Maintenant que nous avons couvert la liaison, créons un composant de base qui restitue un graphique vectoriel évolutif (SVG). Ce sera génial, donc nous l'appellerons awesome-svg
. Cet exercice illustrera à la fois les fonctionnalités de base et la philosophie d'Aurelia et d'Angular 2. Les exemples de code Aurelia de cet article sont disponibles sur GitHub.
Exemple de rectangles SVG dans Aurelia
Construisons d'abord le fichier JavaScript :
// awesome-svg.js import {bindable} from 'aurelia-framework'; export class AwesomeSvgCustomElement { @bindable title; @bindable colors = []; }
Maintenant pour le HTML.
Dans Aurelia, vous pouvez spécifier le modèle (ou utiliser celui en ligne) avec les annotations @template
, @inlineView
ou même le @noView
, mais hors de la boîte, il recherche le fichier .html
avec le même nom que le .js
fichier. Il en va de même pour le nom de l'élément personnalisé - vous pouvez le définir avec @customElement('awesome-svg')
, mais si vous ne le faites pas, Aurelia convertira le titre en dash-case et cherchera une correspondance.
Comme nous n'avons pas précisé le contraire, l'élément s'appellera awesome-svg
et Aurelia cherchera le modèle portant le même nom que le fichier js
(c'est-à-dire awesome-svg.html
) dans le même répertoire :
<!-- 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>
Remarquez la <template>
? Tous les modèles doivent être enveloppés dans une <template>
. Il convient également de noter que vous utilisez ` for … of and the string interpolation
${title}` comme vous le faites dans ES6.
Maintenant, pour utiliser le composant, nous devons soit l'importer dans un modèle avec <require from="path/to/awesome-svg"></require>
ou, s'il est utilisé dans l'application, globaliser la ressource dans la fonction de configuration du framework avec aurelia.use.globalResources('path/to/awesome-svg');
, qui importera le composant awesome-svg
une fois pour toutes.
[Notez que si vous ne faites ni l'un ni l'autre, <awesome-svg></awesome-svg>
sera traité comme n'importe quelle autre balise HTML, sans erreur.]
Vous pouvez afficher le composant avec :
<awesome-svg colors.bind="['#ff0000', '#00ff00', '#0000ff']"></awesome-svg>
Cela rend un ensemble de 3 rectangles :
Exemple de rectangles SVG dans Angular 2
Faisons maintenant le même exemple dans Angular 2, également disponible sur GitHub. Angular 2 nécessite que nous spécifions à la fois le modèle et le nom de l'élément :
// 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 vue est là où les choses se compliquent un peu. Tout d'abord, Angular traite silencieusement les balises HTML inconnues de la même manière qu'un navigateur : il déclenche une erreur indiquant que quelque chose comme my-own-tag
est un élément inconnu. Il en va de même pour toutes les propriétés que vous liez, donc si vous aviez une faute de frappe quelque part dans le code, cela attirerait l'attention car l'application se bloquerait. Ça sonne bien, non ? Oui, parce que vous remarquerez tout de suite si vous cassez l'application, et non, parce que c'est juste une mauvaise forme.
Considérez cet extrait, qui est parfaitement correct en termes de syntaxe de liaison :
<svg> <rect [fill]="color"></rect> </svg>
Même s'il se lit bien, vous obtiendrez une erreur du type "Impossible de se lier à 'remplir' car ce n'est pas une propriété connue de ':svg:rect'". Pour résoudre ce problème, vous devez utiliser la syntaxe [attr.fill]="color"
à la place. Notez également qu'il est nécessaire de spécifier l'espace de noms dans les éléments enfants à l'intérieur de <svg/>: <svg:rect>
pour faire savoir à Angular que cela ne doit pas être traité comme HTML. Développons notre extrait :
<!-- 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>
Nous y voilà. Ensuite, importez-le dans la configuration du module :

@NgModule({ declarations: [ AwesomeSvgComponent ] //... })
Maintenant, le composant peut être utilisé dans ce module, comme ceci :
<awesome-svg [colors]="['#ff0000', '#00ff00', '#0000ff']" title="Rectangles"></awesome-svg>
Éléments personnalisés
Supposons maintenant que nous voulions que notre code rectangle soit un composant personnalisé avec sa propre logique.
Éléments personnalisés : l'Angular 2 Way
Étant donné que Angular 2 rend les composants en fonction de ce qui correspond à son sélecteur défini, il est extrêmement facile de définir un composant personnalisé, comme ceci :
@Component({ selector: 'g[custom-rect]', ... })
L'extrait ci-dessus rendrait l'élément personnalisé dans toutes les balises <g custom-rect></div>
, ce qui est extrêmement pratique.
Éléments personnalisés : la méthode Aurelia
Aurelia nous permet de créer des éléments personnalisés uniquement pour les modèles :
<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>
L'élément personnalisé sera nommé en fonction du nom du fichier. La seule différence avec le fait de nommer d'autres composants est que lors de l'importation, que ce soit dans configure ou via la <require>
, vous devez mettre .html
à la fin. Ainsi, par exemple : <require from="awesome-svg.html"></require>
.
Aurelia a également des attributs personnalisés, mais ils ne servent pas le même objectif que dans Angular 2. Par exemple, dans Aurelia, vous pouvez utiliser l'annotation @containerless
sur l'élément rect
personnalisé. @containerless
peut également être utilisé avec des modèles personnalisés sans le contrôleur et <compose>
, qui restitue essentiellement des éléments dans le DOM.
Considérez le code suivant contenant l'annotation @containerless
:
<svg> <custom-rect containerless></custom-rect> </svg>
La sortie ne contiendrait pas la balise d'élément personnalisé ( custom-rect
), mais à la place, nous obtenons :
<svg> <rect ...></rect> </svg>
Prestations de service
En ce qui concerne les services, Aurelia et Angular sont très similaires, comme vous le verrez dans les exemples suivants. Supposons que nous ayons besoin NumberOperator
qui dépend de NumberGenerator
.
Services à Aurélia
Voici comment définir nos deux services à 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++; } }
Maintenant, pour un composant, on injecte de la même façon :
import {inject} from 'aurelia-framework'; import {NumberOperator} from './_services/number-operator'; @inject(NumberOperator) export class SomeCustomElement { constructor(numberOperator){ this.numberOperator = numberOperator; //this.numberOperator.getNumber(); } }
Comme vous pouvez le voir, avec l'injection de dépendances, n'importe quelle classe peut être un service entièrement extensible, vous pouvez donc même écrire vos propres résolveurs.
Usines à Aurélia
Si vous avez besoin d'une usine ou d'une nouvelle instance ( Factory
et NewInstance
ne sont que quelques résolveurs populaires fournis prêts à l'emploi), vous pouvez procéder comme suit :
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; } }
Services angulaires
Voici le même ensemble de services dans 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++; } }
L'annotation @Injectable
est requise, et pour injecter réellement un service, vous devez spécifier le service dans la liste des fournisseurs dans la configuration du composant ou dans la configuration complète du module, comme ceci :
@Component({ //... providers: [NumberOperator, NumberGenerator] })
Ou, non recommandé, vous pouvez également le spécifier dans l' bootstrap(AppComponent, [NumberGenerator, NumberOperator])
.
Notez que vous devez spécifier à la fois NumberOperator
et NumberGenerator
, quelle que soit la façon dont vous l'injectez.
Le composant résultant ressemblera à ceci :
@Component({ //... providers: [NumberOperator, NumberGenerator], }) export class SomeComponent { constructor(@Inject(NumberOperator) public service){ //service.getNumber(); } }
Usines dans Angular 2
Dans Angular 2, vous pouvez créer des usines avec l'annotation provide
, qui est également utilisée pour les services d'alias afin d'éviter les collisions de noms. La création d'une fabrique peut ressembler à ceci :
let stuffFactory = (someService: SomeService) => { return new Stuff(someService); } @Component({ //... providers: [provide(Stuff, {useFactory: stuffFactory, deps: [SomeService]})] })
transclusion
Angular 1 avait la capacité d'inclure du contenu, un "emplacement", d'un modèle à un autre en utilisant la transclusion. Voyons ce que ses descendants ont à offrir.
Projection de contenu avec Angular 2
Dans Angular 2, la transclusion est appelée "projection de contenu", et elle fonctionne de la même manière que ng-transclude
; c'est-à-dire, parlant en termes angulaires 1, le contenu transclus utilise la portée parent. Il correspondra à la balise de contenu transclus en fonction du sélecteur de configuration. Considérer:
@Component({ selector: 'child', template: `Transcluded: <ng-content></ng-content>` }) export class MyComponent {}
Vous pouvez ensuite utiliser le composant avec <child-component>Hello from Translusion Component</child-component>
, et nous obtiendrons le rendu HTML Yes
exact rendu dans le composant enfant.
Pour la transclusion multi-slots, Angular 2 dispose de sélecteurs que vous pouvez utiliser de la même manière que pour la configuration @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>
Vous pouvez utiliser select
sur vos balises personnalisées, mais n'oubliez pas que la balise doit être connue d'Angular 2.
Machines à sous avec Aurelia
Vous vous souvenez quand j'ai dit qu'Aurelia suivait les normes du Web dans la mesure du possible ? Dans Aurelia, la transclusion est appelée slots, et c'est juste un polyfill pour Web Components Shadow DOM. Shadow DOM n'est pas encore créé pour les machines à sous, mais il suit les spécifications du W3C.
<!-- child --> <template> Slot: <slot></slot> </template> <!-- parent --> <template> <child>${textValue}</child> </template>
Aurelia a été conçu pour être conforme aux normes, et Angular 2 ne l'était pas. En conséquence, nous pouvons faire des choses plus impressionnantes avec les machines à sous d'Aurelia, comme utiliser du contenu de secours (essayer d'utiliser du contenu de secours dans Angular 2 échoue avec <ng-content> element cannot have content
). Considérer:
<!-- 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 même manière que Angular 2, Aurelia rendra toutes les occurrences de la fente en fonction d'une correspondance de nom.
Il convient également de noter que dans Aurelia et Angular, vous pouvez compiler des parties de modèle et rendre les composants de manière dynamique (en utilisant <compose>
avec view-model
dans Aurelia ou ComponentResolver
dans Angular 2).
DOM fantôme
Aurelia et Angular prennent en charge Shadow DOM.
Dans Aurelia, utilisez simplement le décorateur @useShadowDOM
et vous êtes prêt à partir :
import {useShadowDOM} from 'aurelia-framework'; @useShadowDOM() export class YetAnotherCustomElement {}
Dans Angular, la même chose peut être faite avec ViewEncapsulation.Native
:
import { Component, ViewEncapsulation } from '@angular/core'; @Component({ //... encapsulation: ViewEncapsulation.Native, }) export class YetAnotherComponent {}
N'oubliez pas de vérifier si votre navigateur prend en charge Shadow DOM.
Rendu côté serveur
Nous sommes en 2017 et le rendu côté serveur est très tendance. Vous pouvez déjà rendre Angular 2 sur le back-end avec Angular Universal, et Aurelia l'aura en 2017 comme indiqué dans les résolutions du Nouvel An de son équipe. En fait, il y a une démo exécutable dans le repo d'Aurelia.
En plus de cela, Aurelia a des capacités d'amélioration progressive depuis plus d'un an, ce qu'Angular 2 ne fait pas en raison de sa syntaxe HTML non standard.
Taille, performances et prochaines étapes
Bien qu'il ne nous montre pas l'image entière, les benchmarks DBMonster, avec des configurations par défaut et une implémentation optimisée, brossent un bon tableau de comparaison : Aurelia et Angular montrent des résultats similaires d'environ 100 re-rendus par seconde (comme testé sur un MacBook Pro), tandis que Angular 1 a montré environ la moitié de ce résultat. Aurelia et Angular surpassent Angular 1 d'environ cinq fois, et les deux ont 40% d'avance sur React. Ni Aurelia ni Angular 2 n'ont une implémentation Virtual DOM.
En ce qui concerne la taille, Angular est environ deux fois plus gros qu'Aurelia, mais les gars de Google y travaillent : la feuille de route d'Angular comprend la sortie d'Angular 4 avec des plans pour le rendre plus petit et plus léger tout en améliorant l'expérience des développeurs. Il n'y a pas d'Angular 3, et vraiment, le numéro de version devrait être abandonné quand on parle d'Angular, puisque des versions majeures sont prévues tous les 6 mois. Si vous regardez le chemin emprunté par Angular de l'alpha à sa version actuelle, vous verrez qu'il n'était pas toujours cohérent avec des choses comme les attributs renommés de build en build, etc. L'équipe Angular promet que les changements de rupture seront faciles migrer.
À partir de 2017, l'équipe Aurelia prévoit de publier Aurelia UX, de fournir plus d'intégrations et d'outils, et de mettre en œuvre le rendu côté serveur (qui est sur la feuille de route depuis très longtemps).
Angular 2 contre Aurelia : une question de goût
Angular et Aurelia sont bons, et choisir l'un plutôt qu'un autre est une question de goût personnel et de priorités. Si vous avez besoin d'une solution plus fine, Aurelia est votre meilleure option, mais si vous avez besoin d'un support communautaire, Angular est votre gagnant. Ce n'est pas une question de « Est-ce que ce cadre me permet de… ? car la réponse est simplement "oui". Ils offrent à peu près les mêmes fonctionnalités tout en suivant des philosophies et des styles différents ainsi qu'une approche complètement différente des normes Web.