Сравнение Angular 2 и Aurelia side by side

image

Не так давно в мире web-разработки произошло важное событие — вышла бета Angular 2. И уже можно строить предположения о том, как он будет выглядеть после релиза.

Но оценка сама по себе, в вакууме, напоминает выбор электроники по рекламным буклетам производителя. Все фишки, которые есть в гаджете, упомянуты. А все, которых нет — не упомянуты и мы можем про их существование даже не задуматься. Поэтому гораздо эффективнее оценивать сравнивая с чем-то еще.

Так и родилась мысль сравнить Angular 2 с новым, но весьма амбициозным проектом Aurelia, который так же недавно вышел в бету. А заодно пополнить копилку Хабра информацией об этом фреймворке, поскольку пока ее гораздо меньше, чем информации об Angular 2.
Код примеров из статьи в виде готовых для запуска проектов выложен на github. Примеры написаны на TypeScript и оба используют systemjs. Конфигурации systemjs были взяты из quick start guide для каждого фреймворка и достаточно сильно отличаются. Мне показалось разумным оставить их в том виде, в котором их предоставили авторы фреймворков, и не пытаться сделать похожими. Также отмечу, что используемый в проекте http-server не поддерживает pushState (надеюсь, скоро будет поддерживать, ибо одобренный pull request есть), и поэтому в Angular пришлось включить hash based routing.

Мы не будем сравнивать досконально каждую архитектурную особенность, а скорее оценим фреймворки с точки зрения конечного пользователя, который просто хочет понять какие плюшки он получит. Сравнить все в одной статье не получится, поэтому данная статья будет своего рода знакомством с общей структурой. А более сложные вопросы останутся на вторую часть.

Мы пойдем по следующему сценарию:

  • Узнаем, что такое Aurelia и почему ее правомерно сравнивать с Angular
  • Рассмотрим плюсы и минусы каждого с высоты птичьего полета
  • Обозначим фичи, термины и что с чем сранивать
  • Создадим на каждом фреймворке простейший компонент
  • Настроим routing
  • Добавим вложенные компоненты
  • Рассмотрим простейшие варианты data binding
  • Рассмотрим управляющие конструкции в data binding

Что такое Aurelia и почему ее правомерно сравнивать с Angular


Поскольку на том же Хабре информации по Aurelia совсем немного, можно предположить, что не каждый читатель слышал о ней. Поэтому расскажу предысторию создания Aurelia. Тем более, что она довольно интересна и даже немного драматична.

Aurelia это проект Роба Эйзенберга, автора весьма популярного MV*-фреймворка для XAML-платформ Caliburn.Micro. Позже он разработал MV*-фреймворк для web, получивший название Durandal. Durandal не стал супер популярным, но, тем не менее, в нем были очень интересные и элегантные решения и фреймворк собрал свою аудиторию приверженцев, которые его очень полюбили.

Но Роб Эйзенберг понимал все недостатки Durandal, поэтому вместе с его сопровождением занимался разработкой так называемого NextGen фреймворка.

В январе 2014 года, на конференции ngConf небезызвестный в мире веб-разработки John Papa поделился с менеджером Angular team Брэдом Грином идеями, которые были заложены Робом Эйзенбергом в Durandal и в NextGen framework. Эти идеи заинтересовали Грина и он решил пообщаться с Эйзенбергом.

Встретившись, Брэд Грин и Роб Эйзенберг поняли, что их взгляды на будущее веба и веб-разработки во многом совпадают, и они приняли решение объединить усилия и Эйзенберг начал работать в команде Angular над второй версией фреймворка.

Однако, спустя десять месяцев, он принял решение покинуть команду Angular, поскольку направление развития Angular 2, по его мнению, слишком сильно изменилось и отличалось от того Angular 2, на работу с которым он соглашался.

Эйзенберг за короткий срок собрал достаточно большую команду, в составе которой есть такие звезды как, например, Scott Allen, и вернулся к работе над фреймворком своей мечты. Итогом этой работы и стал Aurelia.

Общественность приняла фреймворк с интересом (как простейший способ оценки, на момент написания статьи Aurelia собрала на github 5000 звезд против 8000 у Angular 2).

Общие принципы, заложенные в Angular 2 и Aurelia, очень похожи — это игроки одного класса. Но видение в деталях и набор возможностей у них достаточно сильно отличаются, что и делает их сравнение интересным.

Плюсы и минусы Angular 2 и Aurelia с высоты птичьего полета


Из осязаемых характеристик, которые можно сравнить при выборе фреймворка, посмотрим на perfomance. Aurelia показывает интересные результаты в бенчмарке dbmonster, выбивая чуть лучшие баллы, чем Angular 2, и заметно лучшие, чем React и Angular1.

Что такое dbmonster
Dbmonster это rendering benchmark, который разработал Ryan Florence. Для оценки скорости работы рендерится массив с данными, которые постоянно обновляются и это дает возможность оценить скорость работы фреймворка. Изначально тест был написан для Angular 1, Angular 2 и React. Позже один из разработчиков Aurelia, Jeremy Danyow, представил реализацию для Aurelia.
При оценке бенчмарка полезно обратить внимание на следующие моменты:
  • Плавный скролл — страница должна скроллиться без «прыжков»
  • Всплывающие подсказки — если вести курсор мыши по списку, то отрисовывается всплывающая подсказка. Она должна отрисовываться плавно и данные должны отображаться без задержек
  • Repaint rate и memory rate — в правом нижнем углу есть два индикатора. Первый показывает количество перерисовок в секунду, второй показывает расход памяти
  • Скорость изменения данных — в верхней части страницы есть слайдер, который позволяет регулировать частоту изменения данных. Если при уменьшении скорости изменения данных repaint rate не становится больше, то это значит, что рассматриваемый фреймворк неэффективно отслеживает изменения и принимает решения об обновлении DOM

Для того, чтобы все индикаторы работали корректно и для получения наиболее «чистого» результата, рекомендуется использовать браузер chrome и запускать его следующей командой:
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --user-data-dir="C:\chrome\dev-sessions\perf" --enable-precise-memory-info --enable-benchmarking --js-flags="--expose-gc"


Из неосязаемых характеристик, попробую оценить самые сильные и слабые стороны обоих фреймворков (осторожно, ИМХО автора).

Главные плюсы Angular

  • У Angular заведомо больше сообщество, а значит будет больше идей, больше расширений, проще найти ответ на имеющийся вопрос.
  • У Angular в три раза больше команда и они гораздо быстрее развивают свой проект. Также команда Angular уже заручилась поддержкой авторов инструментов для разработчиков и поддержка Angular будет в очень многих инструментах (а еще уже есть работающий и очень крутой Batarangle, пусть он еще и в developer preview).


Главные плюсы Aurelia

  • Aurelia может противопоставить Angular-у достаточно много интересных фишек. Например, это продвинутый механизм composition, и template parts. Aurelia разработана с акцентом на unobtrusive, количество конструкций фреймворка в конечном коде минимально. Aurelia более компактен и сопровождаем, в то время как Angular порой просто вынуждает плодить копипаст.
  • Команда Aurelia будет обеспечивать коммерческую поддержку клиентов, и есть реальная возможность повлиять на направление развития проекта. А в случае с Angular 2 мы вынуждены работать с тем, что есть.


Главные минусы Angular

  • Если смотреть на историю развития Angular, то складывается впечатление, что команда не очень четко видит, каким должен получиться Angular 2 в итоге. Я следил за Angular 2 с момента анонса работ над ним и вижу, что фреймворк действительно очень сильно и не всегда последовательно меняется. По этой же причине Эйзенберг покинул команду Angular 2 из-за того, как поменялась архитектура проекта по сравнению с изначальной. И по этой же причине появляются подобные посты.


Главные минусы Aurelia

  • Главный вопрос по поводу Aurelia — сдюжит ли она против Angular 2. Это не праздный вопрос, поскольку Эйзенберг противопоставляет Aurelia и Angular 2. О некоторой ангажированности говорят его записи с блогах (раз, два, три) его выступления (NDC London последние минуты видео) и даже некоторые комментарии к статьям про Angular 2. Мне очень нравится aurelia, но у меня вызывает опасения вероятность, что выбранный фреймворк в один прекрасный день перестанет поддерживаться ибо автору надоело. С другой стороны, не прошло и недели, как большой facebook объявил о закрытии parse.com, так что не застрахован никто и ни от чего.

Возможности Angular 2 и Aurelia, термины и что с чем сранивать


Архитектуры Angular 2 и Aurelia очень похожи. Ниже попытаюсь в пару абзацев сформулировать принципы работы обоих, обозначая курсивом основные термины и конструкции, которые имеет смысл рассматривать и сравнивать. Надеюсь, это получится читаемым текстом, а не месивом из терминов.

Основу приложения как на Angular 2, так и на Aurelia составляют компоненты, ассоциированные с соответствующим шаблоном.
Обязательно наличие root-компонента, который олицетворяет собой приложение (app). К компонентам могут/должны быть привязаны метаданные при помощи декораторов.

Инициализация компонентов выполняется при помощи dependency injection. Также, каждый компонент имеет декларированный жизненный цикл, в который можно встраиваться при помощи lifecycle hooks. Компоненты могут быть составлены в иерархическую структуру.

Синхронизация состояний и коммуникация между компонентом и шаблоном выполняется при помощи data binding. В процесс рендеринга шаблона в конечный HTML можно встроиться при помощи pipes (Angular) или value converters+binding behaviours (Aurelia).

Для перехода между изолированными областями приложения используется routing. Для коммуникации между модулями приложения могут быть использованы события.

И, наконец, переходим к примерам.

Создаем первый компонент


Angular 2
Начнем с создания простейшего компонента, который будет представлять собой root component.

import {Component} from 'angular2/core';

@Component({selector: 'angular-app', templateUrl: 'app/app.html'} })
export class App {
    message: string = 'Welcome to Angular 2!';
}

Для того, чтобы Angular понял что наш модуль представляет из себя компонент, необходимо обернуть его декоратором @Component.

Примечание — строго говоря, @Component это не декоратор (decorator), а аннотация (annotation). Про разницу можно почитать здесь. В статье оставим термин декоратор, поскольку в документации к Angular 2 аннотации лежат в разделе Decorators.

Как минимум, декоратор должен указывать селектор, куда отрисовывать шаблон. В данном случае это элемент .

Для декларации шаблона есть два варианта:

  1. Указать html-строку в качестве параметра template декоратора @Component. Такой подход полезен на тот случай, если шаблон компактный и не хочется делать ради него отдельную html-страницу
  2. Указать url шаблона в качестве параметра templateUrl. У нас потребуется отдельная страница, поэтому мы будем использовать этот вариант

Шаблон будет выглядеть следующим образом:

    

Вот, собственно, и весь компонент. Достаточно просто.
Что хорошо — код чище и свободнее от конструкций самого фреймворка, нежели это было в Angular 1. Это не может не радовать.

Что смущает — необходимость использования декораторов даже для простейшего компонента. Цитата из документации:

Each Angular component requires a single @Component and at least one View annotation. The @Component annotation specifies when a component is instantiated, and which properties and hostListeners it binds to.


При этом параметры, конифгурируемые в данном примере, у большинства разработчиков в любом случае подчиняются какой-то логике, и вместо явной конфигурации можно было использовать конвенции, а декораторы подключать для более сложных сценариев.Aurelia
Поскольку Aurelia построен основываясь на конвенциях, компонент это обычный модуль без каких-либо метаданных:

export class App {
    message: string = "Welcome to Aurelia!";
}


По стандартной конвенции, для декларации шаблона нам необходимо создать html-файл с аналогичным компоненту именем, то есть для нашего примера это будет файл app.html:



Каждый шаблон в Aurelia должен быть обернут в элемент template. Для создания inline шаблона по аналогии с Angular 2 можно использовать декоратор inlineView. Также в Aurelia можно изменять конвенции и выполнять дополнительную настройку. Детали можно посмотреть здесь.

Что хорошо — код предельно чистый и понятный. Никаких конструкций фреймворка.

Что смущает — для настройки может понадобиться множество аннотаций, решающих схожие вопросы. Например, это inlineView, noView, useView и useViewStrategy. Документацию пока что обильной не назовешь, и в ней даже нет поиска, так что есть риск просто запутаться, что и где использовать.

Настраиваем routing


В нашем случае каждое приложение будет иметь несколько страничек, на которых будут рассматриваться аспекты, обозначенные в статье. Вложенный routing мы рассматривать не будем, поскольку как в Angular, так и в Aurelia, он по сути аналогичен routing-у как таковому.
Angular 2
Для того, чтобы настроить routing в Angular 2 нам необходимо импортировать декоратор @RouteConfig, импортировать модули, которые будут привязаны к маршрутам и декларировать нашу карту роутинга. Сделаем это в нашем компоненте app:

...
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {BindingSample} from './binding-sample/binding-sample';
import {ComponentSample} from './component-sample/component-sample';
...
@RouteConfig([
    { path: '/component-sample', name: 'ComponentSample', component: ComponentSample, useAsDefault: true },
    { path: '/binding-sample', name: 'BindingSample', component: BindingSample }
])
export class App {
   ...
}

Для каждого маршрута мы указываем:

  1. паттерн маршрута
  2. имя маршрута (оно понадобится для привязки в разметке)
  3. модуль компонента, который Angular будет создавать при активации маршрута
  4. опциональный параметр useAsDefault, указывающий что это маршрут по умолчанию


Если смущает необходимость уже на старте загружать все модули, которые имеют маршрут, то можно воспользоваться асинхронной загрузкой. Для этого вместо параметра component используем параметр loader, указывая функцию, которая вернет promise, импортирующий нужный модуль. Мы так и сделаем, но пример кода чуть позже.

Чтобы использовать routing в шаблонах для отрисовки навигации и указания секции, куда отрисовывать текущий компонент, нам необходимо дополнительно импортировать коллекцию директив ROUTER_DIRECTIVES и добавить их в параметр directives декоратора @Component. Итого, модуль app.ts получается вот такой:

import {Component} from 'angular2/core';
import {View} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';

@Component({
    selector: 'angular-app', templateUrl: 'app/app.html', directives: [ROUTER_DIRECTIVES]
})
@RouteConfig([
    { path: '/component-sample', name: 'ComponentSample', 
            loader : () => System.import('app/component-sample/component-sample').then(m => m.ComponentSample), useAsDefault: true },
    { path: '/binding-sample', name: 'BindingSample', 
            loader : () => System.import('app/binding-sample/binding-sample').then(m => m.BindingSample) }
])
export class App {
    message: string = "Welcome to Angular 2!";
}

Теперь добавляем навигацию в app.html. Для этого используем директиву routerLink, передавая в качестве параметра массив, первым элементом в котором является строка с именем маршрута, которое мы указали при настройке @RouteConfig.


...

Параметр является массивом на случай вложенного routing, тогда мы передаем массив из нескольких строк с именами маршрутов.

Если маршрут необходимо параметризовать, то объект с параметрами передается последним элементом в массиве:

...
    Binding sample
...


И последний момент. Нам необходимо в разметке указать область, куда будут отрисовываться шаблоны для текущего маршрута. Делаем это при помощи директивы router-outlet в том же app.html:

Что хорошо — новый routing приятнее, чем routeProvider, живший в Angular 1.x, но все равно вызывает целый ряд вопросов. Абсолютное большинство типов, касающихся routing в документации вообще еще не имеют описания, поэтому пока трудно что-то говорить окончательно.

Что смущает — смущает несколько вещей. Прежде всего, это необходимость настройки через декоратор — если в нашем приложении, к примеру, 50 маршрутов, то код нашего модуля просто потеряется во всех этих настройках. И если нам понадобится при построении схемы навигации какая-либо if-логика, то наш код рискует превратиться в кошмар.
Второе, это отсутствие явного доступа ко всей коллекции route-ов, которую можно было бы просто перебрать в разметке для отрисовки всей навигации, а не ручная отрисовка каждой ссылки (которую мы будем забывать делать). Опять же, при наличии if-логики для построения маршрутов, нам придется дублировать эту логику в шаблоне, чтобы не нарисовать лишнего.

Aurelia
Согласно конвенции в Aurelia, чтобы настроить routing для компонента нам необходимо реализовать метод configureRouter, который Aurelia вызовет автоматически. То же самое верно и для вложенного routing-а — любой компонент, имеющий метод configureRouter, будет формировать схему routing-а.

export class App {
    message: string = "Welcome to Aurelia!";
    router: any;
    configureRouter(config, router) {
        config.title = 'Welcome to Aurelia!';
        config.map([
            { 
              route: ['', 'component-sample'], moduleId: 'app/component-sample/component-sample', nav: true, title: 'Component sample' 
            },
            { 
              route: 'component-sample', moduleId: 'app/binding-sample/binding-sample', nav: true, title: 'Binding sample' 
            }
        ]);
        this.router = router;
    }
}


Для каждого маршрута мы указываем:

  1. паттерн маршрута (или набор паттернов, паттерн в виде пустой строки означает маршрут по умолчанию).
  2. идентификатор модуля, который инициировать при активации маршрута.
  3. опционально свойство title — при активации маршрута его значение будет дописываться в title страницы.
  4. опционально свойство nav — оно сообщает должен ли маршрут попасть в navigation model. Если указать число, то оно будет означать порядок элемента в коллекции navigation model.


На основании переданной конфигурации Aurelia составит navigation model, который в шаблоне можно будет перебрать и отрисовать навигацию:

    

Последним шагом указываем в разметке область, куда будут отрисовываться шаблоны для текущего маршрута. Делаем это при помощи директивы router-view в том же app.html

    

Что хорошо — настройка простая и вполне очевидная. Приятно, что Aurelia пытается нам помочь и строит коллекцию для навигации.

Что смущает — по сравнению с навигацией в Durandal убрали возможность добавлять в настройки маршрутов произвольные свойства. С одной стороны это, конечно, правильно, ибо нефиг. С другой стороны, это сильно снижает вероятность использования navigation model в сторону ручной отрисовки. Navigation model будет бесполезен если захочется добавить к пунктам меню не только title, но и, например, tooltip-ы.

Добавляем вложенные компоненты


Angular 2
Для примера работы с вложенными компонентами в тестовом проекте есть папка component-sample, в ней лежит все, что нам нужно.
Итак, для создания вложенного компонента в Angular 2 нам необходимо:

  1. Объявить компонент и привязать к нему метаданные. Для инициализации свойств данными от родительского компонента, мы добавляем параметр inputs в декораторе @Component, передавая ему имена соответствующих свойств:
    import {Component} from 'angular2/core';
    
    @Component({
        selector: 'test-child-component',
        inputs: ['inputMessage'],
        template: `
    Child component title
    Message from parent component is: {{inputMessage}}
    ` }) export class TestChildComponent { inputMessage: string }

  2. Импортируем в родительском компоненте (component-sample.ts) дочерний и передаем его в массив directives декоратора @Component
  3. В шаблоне родительского компонента размещаем элемент соответствующий указанному селектору из вложенного компонента и передающий параметры

В итоге, наш родительский компонент выглядит следующим образом:

import {Component} from 'angular2/core';
import {TestChildComponent} from './test-сhild-сomponent';
@Component({
    template: `
    

{{message}}

`, directives: [TestChildComponent] }) export class ComponentSample { message: string = 'This is a component with child component sample'; messageForChild: string = 'Hello to child component!'; }

Что хорошо — в целом, все просто и понятно.

Что смущает — не хватает приятных плюшек, которые есть в Aurelia. Они описаны ниже.

Aurelia
В Aurelia отрисовать вложенный компонент можно двумя способами: custom element и composition.

Первый способ, в целом, аналогичен Angular 2 и используется для создания сложных контролов и т.д. В коде тестового проекта этот подход демонстрируется в файле test-custom-element.html.

Второй используется преимущественно для сценариев master-detail и более гибок, поскольку мы можем динамически указывать какой загрузить компонент, какой отрисовать шаблон и какие данные передать. Данный подход продемонстрирован в тестовом проект в файле test-сhild-сomponent.ts.

Разберем оба варианта по очереди.

Вариант 1 — custom element:
Для создания вложенного компонента при помощи custom element нам необходимо:

  1. Создать обычный компонент, дополнительно отметив декоратором @bindable свойства, значения для которых мы будем передавать через шаблон как параметры (по аналогии с параметром inputs в Angular 2). Дополнительно, если наш элемент простой и не имеет поведения, то можно обойтись без создания компонента, а просто создать шаблон и в нем перечислить bindable-свойства при помощи одноименного атрибута. Компонент Aurelia создаст «на лету»:
    
    
    

  2. Добавить в шаблон родительского компонента элемент require, указывающий на нужный нам модуль (аналогично параметру directives в Angular 2, но в шаблоне) и отрисовать кастомный элемент в разметке там, где он нам понадобится. Передача параметров выполняется при помощи атрибута <имя свойства>.bind. В итоге, шаблон родительского компонента будет выглядеть следующим образом:
    
    
    


Если элемент используется повсеместно, то его можно зарегистрировать как global resource. Это позволит не писать в каждом шаблоне. Как это сделать написано здесь в разделе «Making Resources Global»

Еще одна приятная опция, которая есть в Aurelia и которую мне не удалось найти в Angular — передача разметки из родительского шаблона. Если в родительском шаблоне внутри элемента test-custom-element декларировать разметку, а в дочернем добавить элемент




то разметка из родительского шаблона будет отрисована в дочернем. Также при работе с custom elements можно использовать уже упомянутые ранее template parts и объявить несколько заменяемых областей.

Что хорошо — все просто и логично.
Что смущает — возможно, не всем понравится необходимость декларировать зависимости в разметке.

Вариант 2 — Сompose:
Для создания вложенного компонента при помощи composition нам необходимо:

  1. Создать обычный компонент. Поскольку compose предполагает слабую связанность компонент, для передачи данных нам необходимо использовать lifecycle hooks (они также имеются и у Angular 2, но про них в следующей статье). В данном случае мы используем метод activate. Шаблон мы сделаем inline, поскольку он маленький:
    import {inlineView} from 'aurelia-templating';
    
    @inlineView('