Многие впервые увидев синтаксис шаблонов Angular2 начинают причитать, мол ужас какой сделали, неужто нельзя было как в Angular1 хотя-бы. Зачем нужно было вводить это разнообразие скобочек, звездочек и прочей ерунды! Однако при ближайшем рассмотрении все становится куда проще, главное не пугаться.

Так как шаблоны в AngularJS являются неотъемлемой его частью, важно разобраться с ними в самом начале знакомства с новой версии этого фреймворка. Заодно обсудим, какие преимущества дает нам данный синтаксис по сравнению с angular 1.x. Причем лучше всего будет рассматривать это на небольших примерах.

Данная статья во многом основана на материалах этих двух статей:



Для того, что бы упростить подачу материала, давайте разберемся. Под AngularJS я буду подразумевать всю ветку Angular 1.x, в то время как под Angular2 — ветку 2.x.

Так же спасибо господину Bronx за ценное дополнение, которое я включил в текст статьи.

Примечание: вечер выходного дня, потому о опечатках и т.д. сообщайте в личку. Премного благодарен и приятного чтения.


Биндинг свойств элементов


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

Давайте освежим в памяти как работают биндинги на атрибуты в AngularJS. Для этого мы обычно напрямую пробрасывали выражения (интерполируемые при компиляции шаблона) прямо в атрибут элемента, либо пользовались одной из множества директив. Например ngValue для установки значения свойства value у инпутов.

Приведем пример как это работает в AngularJS:

<input ng-value="expression" />

Так же не забываем что мы можем просто интерполировать результат выражения напрямую в качестве значения аргумента:

<input value="{{expression}}" />

Заметим интересную особенность. Второй вариант многие его избегают, так как можно увидеть промежуточное состояние до того, как angular интерполирует значения. Однако первый вариант использует директивы. То есть для того что бы у нас все было хорошо, красиво и удобно, нам надо сделать по директиве на каждое свойство всех элементов. Согласитесь, не слишком то удобно. Почему бы не добавить какое-то обозначение для атрибута, которое бы говорило ангуляру замэпить значение на него. Причем было бы неплохо что бы синтаксис был валидным. И они добавили, теперь для этого надо всего-лишь обернуть интересующий нас атрибут (любой атрибут) в квадратные скобки — [].

<input [value]="valueExpression" [placeholder]="placeholderExpression" />


По сути синтаксис [] это ни что иное как сокращенная запись для bind-prop. Если убрать сахар то пример выше будет записан как:

<input bind-value="valueExpression" bind-placeholder="placeholderExpression" />


Однако вспомним как можно поменять атрибуты в Javascript:

element[prop] = 'value';


именно отсюда и берутся эти квадратные скобочки. Мы поясняем что значения будут мэпится прямо на свойства элементов. То есть вместо директивы ng-bind-html мы можем просто биндить результат выражения на [inner-html]. Или вместо ng-hide мы можем использовать свойство [hidden] самого элемента. Это значительно уменьшает количество специфичных для angular вещей которые нужно знать и приближает нас к DOM, при этом у нас все еще сохраняется абстракция от оного.

Ну и закрыть этот вопрос стоит указав, что у нас так же есть возможность мэпить интерполируемое значение, так же как это было в AngularJS:

<input value="{{ valueExpression }}" placeholder="{{ placeholderExpression }}" />



События


В AngularJS мы могли подписаться на события элементов используя специальные директивы. Так же, как и в случае со свойствами, нам приходится иметь дело с целой кучей возможных событий. И на каждое событие приходилось делать директиву. Пожалуй, самой популярной из таких директив является ngClick:

<button ng-click="doSomething($event)">

Учитывая что мы уже решили такую проблему для свойств элементов, то наверное так же стоит решать и эту проблему? Именно это и сделали! Для того, что бы подписаться на событие, достаточно прописать атрибут используюя следующий синтаксис: (eventName). Таким образом у нас есть возможность подписаться на любое событие генерируемое нашим элементом, при этом без надобности писать отдельную директиву:

<button (click)="doSomething($event)">


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

<button on-click="doSomething($event)">


А вид скобочек получается из ассоциаций с установлением обработчиков событий в javascript:

element.addEventListener('click', ($event) => doSomething($event));


Стоит так же отметить важное отличие в поведении, если сравнивать с биндингом событий в AngularJS. Теперь Angular2 будет выбрасывать нам ошибки в случае обращения к несуществующему методу. Как если бы мы вызывали код прямо в javascript. Я думаю многие из тех у кого бывали глупые баги из-за опечаток в этом контексте будут рады. В некоторых случаях это добавляет определенные неудобства и потому был добавлен так же Elvis-оператор, о котором мы поговорим чуть позже.


Двусторонний биндинг


Существует распространенное мнение, что двусторонний биндинг это плохо, и что это основной грех ангуляра. Это не совсем так. Проблема с двусторонним биндингом в AnugularJS была в том, что он используется повсеместно, не давая разработчикам альтернативы (возможно ситуация с этим в скором времени изменится).

Все же иногда возникают случаи когда двусторонний биндинг здорово упрощает разработку, особенно в случае форм. Так как же реализован оный в Angular2? Давайте подумаем, как организовать двусторонний биндинг имея односторонний биндинг свойст элементов и биндинг событий:

<input type="text" [value]="firstName" (input)="firstName=$event.target.value" />

Опять же, не очень то удобно. Посему в Angular2 так же есть синтаксический сахар с использованием ngModel. Результат будет идентичен тому, что мы привели выше:

<input type="text" [(ngModel)]="firstName" />

Локальные переменные


Для передачи данных между элементами в пределах одного шаблона используются локальные переменные. Наиболее близкой аналогией в AngularJS, пожалуй, можно считать доступ к элементам формы по имени через ngForm. Конечно, это не совсем корректное сравнение, так как работает это только за счет директивы ngForm. В Angular2 вы можете использовать ссылку на любой объект или DOM элемент в пределах элемента шаблона и его потомков, используя локальные переменные #.

<video #movieplayer ...>
  <button (click)="movieplayer.play()">
</video>

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

Помимо символа #, вы так же можете объявлять переменные используя префикс var-. В этом случае вместо #movieplayer мы могли бы записать var-movieplayer.

Благодаря локальным переменным нам больше не нужно делать новые директивы всякий раз, когда действия над одними элементами должны менять что-то у других. Например, в примере выше, мы можем быстро добавить кнопку которая запускает просмотр видео. В этом собственно главное концептуальное отличие Angular2 от AngularJS, меньше бесполезных микро-директив, большая концентрация внимания на компонентах.


Звездочка (символ *)


Символ * вызывает больше всего вопросов. Давайте разберемся, зачем он понадобился. Для осознания причин добавления этого символа, нам стоит вспомнить о таком элементе как template.

Элемент template позволяет нам задекларировать кусочек DOM, который мы можем инициализировать позже, что дает нам более высокую производительность и более рациональное использование ресурсов. Чем-то это похоже на documentFragment в контексте HTML.

Пожалуй, будет проще показать зачем оно надо на примере:

<div style="display:none">
  <img src="path/to/your/image.png" />
</div>

В этом небольшом примере мы можем видеть, что блок скрыт (display:none). Однако браузер все равно будет пытаться загрузить картинку, даже если она не понадобится. Если подобных вещей на странице много, это может пагубно отразиться на общей производительности страницы.

Решением этой проблемы станет использование элемента template.

<template>
  <img src="path/to/your/image.png" />
</template>

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

Но вернемся к нашим баранам. Использование символа * перед директивой элемента позволит ангуляру при компиляции обернуть элемент в шаблон. Проще посмотреть на примере:

<hero-detail *ngIf="isActive" [hero]="currentHero"></hero-detail>

Этот шаблон будет трансформирован в такой:

<template [ngIf]="isActive">
  <hero-detail [hero]="currentHero"></hero-detail>
</template>

Теперь должно стать ясно, что этот символ предоставляет синтаксический сахар для достижения более высокой производительности при использовании условных директив вроде ngFor, ngIf и ngSwitch. Логично что нет нужды в создании экземпляра компонента hero-detail пока isActive не является истиной.

Пайпы


Пайпы — это прямой аналог фильтров из AngularJS. В общем и целом синтаксис их применения не особо поменялся:

<p>My birthday is {{ birthday | date:"MM/dd/yy" }} </p>

Зачем понадобилось менять название с уже привычных фильтров на новые пайпы — отдельный вопрос. Сделано это было что бы подчеркнуть новую механику работы фильтров. Теперь это не синхронные фильтры, а асинхронные пайпы (по аналогии с unix pipes).

В AngularJS фильтры запускаются синхронно на каждый $digest цикл. Этого требует механизм отслеживания изменений в AngularJs. В Angular2 же отслеживание изменений учитывает зависимости данных, посему это позволяет оптимизировать целый ряд концепций. Так же появилось разделение на stateful и stateless пайпы (в то время как фильры AngularJS заведомо считались stateful).

Stateless пайпы, как это может быть понятно из названия, не имеют собственного состояния. Это чистые функции. Они выполняются только один раз (или если изменились входящие данные). Большинство пайпов в Angular2 являются stateless пайпами. Это позволяет существенно увеличить производительность.

Stateful пайпы напротив, имеют свое состояние и они выполняются часто в связи с тем что внутренне состояние может поменяться. Примером подобного пайпа является Async. Он получает на вход промис, подписывается на изменения и возвращает заресолвленное значение.

// это не TypeScript, это babel со stage-1, ну так, к сведенью
@Component({
  selector: 'my-hero',
  template: 'Message: {{delayedMessage | async}}',
})
class MyHeroAsyncMessageComponent {
  delayedMessage = new Promise((resolve, reject) => {
    setTimeout(() => resolve('You are my Hero!'), 500);
  });
}
// повелись? Неее, это просто TypeScript без определения типов.

В этом примере компонент my-hero выведет Message: You are my Hero! только после того, как будет заресолвлен промис delayedMessage.

Для того что бы сделать stateful пайпы мы должны явно объявить это в метаданных оного. Иначе Angular2 будет считать его stateless.

Elvis оператор


В AngularJS мы могли делать обращения к чему угодно совершенно безболезненно, что частенько выливалось в весьма коварные баги и затрудняло отладку. Вам наверняка приходилось сталкиваться с опечатками в ngClick, при которых код не выполнял требуемых действий а фреймворк не давал нам никаких подсказок что же пошло не так. В Angular2 мы наконец будем получать ошибки! Однако подобное решение не всем может придтись по душе без дополнительного сахара.

В Javascript нам частенько приходится проверять наличие каких-либо свойств. Думаю все мы писали что-то подобное:

if (cordova && cordova.plugins && cordova.plugins.notification){
  // use cordova.plugins.notification
}

Делая такие проверки мы конечно же хотим избежать подобного:

TypeError: Cannot read property 'notification' of undefined.

Для решения этой проблемы был введен Elvis оператор, как сокращенная версия тернарного оператора. Вы могли видеть подобный в Coffeescript. В Angular2 решили эту проблему используя тот же оператор, но на уровне шаблонов:

<button (click)="employer?.goToWork()">Go To Work</button>

Данная запись означает, что свойство employer опционально, и если оно имеет пустое значение, то остальная часть выражения игнорируется. Если убрать сахар, то эта запись будет выглядеть так:

<button (click)="employer === undefined ? : employer.goToWork()">Go To Work</button>

Если бы мы не воспользовались этим оператором и при этом не делали бы подобной проверки, то в этой ситуации мы бы получили TypeError.

Так же как и в coffescript данный оператор можно использовать сколько угодно раз в рамках одного выражения, например так: a?.b?.c?.d

Выводы


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

Комментарии (64)


  1. Houston
    09.01.2016 22:05
    +13

    Простой и понятный обзор, спасибо за публикацию!


  1. cold147
    09.01.2016 23:25
    +1

    Спасибо за статью! а продолжение будет? было бы интересно прочитать про роутера.


    1. Fesor
      09.01.2016 23:35

      На данный момент есть только angular/router, и я пока не нашел времени детально с ним разобраться. uiRouter 1.0 на подходе (и он намного лучше 0.x ветки) но судя по всему сразу angular2 они поддерживать не будут.

      В целом различия в роутинге и подходах с оными было бы нелпохо освятить, я согласен. Тем более что хорошие статьи которые я встречал на данный момент слегка устарели в деталях.


  1. ReklatsMasters
    10.01.2016 01:38
    +10

    > [value]
    > (click)
    > <video #movieplayer>
    > *ngIf

    Весь этот новый синтаксис, конечно, решает многие проблемы из первой части, однако не перестаёт казаться какой-то нелепицей.


    1. Fesor
      10.01.2016 01:48
      +3

      На первый взгляд — да, это и побудило написать статью. Многие начинают рубить с плеча мол «ужас и кашмар». Да и я сам когда в первый раз увидел немного удивился. А разобравшись — не так страшно и есть свои плюсы. Вопросы реально возникают только с последним пунктом, так как выбор символа и способ применения не очевидны сразу.

      Так же многие жалуются мол «зачем эти скобочки, почему нельзя было сделать это просто обычными атрибутами». Имеется в виду что если мы выбрали путь добавления префиксов/суффиксов то почему бы просто не сделать префикс on- и префикс bind- и т.д. И в чем-то можно согласиться, хотя с другой стороны видя подобный кусок HTML:

      <input 
          type="text"
          [disabled]="isDisabled"
          placeholder="Some Field"
          [value]="modelValue"
          required="required"
          maxlength="12" />
      


      сразу видно где биндинги. В темплейтах побольше на поиск подобных конструкций уходит существенно меньше времени. Ну и опять же, сделав префиксы bind-/on- мы таким образом сразу бы ввели целый класс зарезервированных имет для атрибутов.


      1. Bronx
        10.01.2016 06:25
        +7

        > почему бы просто не сделать префикс on- и префикс bind- и т.д

        Безо всяких «бы» — они есть в Angular2: «bind-attr=» вместо "[attr]=", «on-event=» вместо "(event)=" и «bindon-ngModel=» вместо "[(ngModel)]=", «var-name» вместо "#name".

        > сразу видно где биндинги

        Более того, сразу видно, где привязки одностороние, а где — двусторонние, а где их вообще нет. В первом ангуляре эту разницу увидеть труднее.


        1. Fesor
          10.01.2016 11:58

          Дополнил статью, это на самом деле важный аспект.


      1. vintage
        10.01.2016 09:58
        -6

        А можно было бы наглядней:

        input 
            .type : =text
            .disabled < isDisabled
            .placeholder : =Some Field
            .value < modelValue
            .required : true
            .maxLength : 12
        


        1. Fesor
          10.01.2016 12:36
          +3

          Это наглядно для вас. Для непосвещенного человека это целый сэт новых вещей.


          1. vintage
            10.01.2016 14:12

            А ангуляровские скобочки и звёздочки — это не сет новых вещей?


            1. Fesor
              10.01.2016 14:33
              +1

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

              В вашем примере например не понятно в чем разница между

              .disabled < isDisabled
              

              и
              .placeholder : =Some Field
              


              По идее же должно быть одно и то же, нет? Сначала я подумал что одно это биндинг (по аналогии с редиректом в bash) а другое — просто значение, но тогда последние два свойства не согласуются.

              А в angular2 запись
              <div [inner-html]="expression"></div>
              


              намекает нам о

              div['innerHtml'] = expression();
              


              1. vintage
                10.01.2016 15:04
                +1

                «bindon-ngModel=» — выглядит ещё более странно, чем с сахаром.

                Разница в том, что стрелочка показывает откуда куда перемещается значение (биндинг, да), а двоеточие задаёт значение по умолчанию.

                .disabled < isDisabled : true
                


                Не понял, что там не согласуется?

                div['innerHtml'] = expression();
                

                В этом коде статическая типизация идёт лесом, да и не удобно так писать, поэтому лучше так:
                div.innerHtml = expression();
                

                И тогда вся ассоциация теряется. Тем более, что такая запись её в конец убивает:
                [(ngModel)]="firstName"
                

                Кроме того, у меня не укладывается в голове, как этот код:
                <button (click)="doSomething($event)">
                

                Может быть ассоциирован с этим:
                element.addEventListener('click', event => doSomething(event));
                

                Да, именно таким, ибо чтобы ставить тут лишние скобочки и доллары, нужно быть редкостным фанатом этих символов.
                Ну а уж про объявление переменных через решётку, как и вообще объявление переменных в html, я вообще молчу.


                1. Fesor
                  10.01.2016 15:19

                  Не понял, что там не согласуется?

                  .placeholder : =Some Field
                  


                  Это типа двусторонний биндинг? В условиях хоть сколько нибудь большого проекта такие вещи довольно легко потерять извиду. Искать где там у меня двусторонние биндинги выйдет сильно тяжело.

                  В этом коде статическая типизация идёт лесом, да и не удобно так писать, поэтому лучше так:

                  начнем с того что статическая типизация вообще не причем тут. Вы наверное хотели сказать про статический анализ. Ну так с ним все хорошо так как… мы же не динамически вычисляем имя свойства. То что никто в здравом уме так не пишет — это да, но это единственная аналогия которая объясняет направление данных и при этом удобно читается (удобнее чем один символ точки, его легко не заметить).

                  Тем более, что такая запись её в конец убивает:

                  жизнь состоит из маленьких компромисов. Повторюсь — это синтаксический сахар и не более. С учетом того что теперь можно работать с формами через ngFormModel, пользоваться этой нотацией так часто вам не придется.

                  Да, именно таким, ибо чтобы ставить тут лишние скобочки и доллары

                  Доллар — это признак детали реализации. Все внутренние сервисы и объекты имеют префикс $ что бы отличать их от других. Не более.

                  По поводу ассоциаций — просто посмотрите какие скобочки используются в этой записи :).

                  Вообще у нас выходит весьма глупый спор. Вы оцениваете «интуитивность» решений исходя из своего опыта и знаний. Возьмите десяток джуниоров, покажите им ваш пример и посчитайте сколько из них разберутся что что означает.

                  Для меня основное преимущество использования обычного HTML в качестве основы для синтаксиса шаблонов — проще построить процесс разработки. У меня могут быть на проекте парочка толковых верстальщиков, которые могут работать с приложением и при этом не касаться JS кода. Фронтэнд разработчик же будет предоставлять им базовые конструкции на которых они уже будут реализовывать UI. Ну и серьезно, объяснить этот ML я могу за минут 15 новому разработчику, и на код ревью будет удобно следить за всем этим добром. В вашем же варианте я просто могу проглядеть какие-то мелочи при беглом ревью кода. Если бы я работал с вами — то я не особо бы парился, но мир не идеален.


                  1. vintage
                    10.01.2016 15:58

                    Это типа двусторонний биндинг?
                    Нет, это установка значения, а биндинги через стрелочки. Точнее это даже не биндинги, а делегирование, что эквивалентно двустороннему биндингу.

                    Искать где там у меня двусторонние биндинги выйдет сильно тяжело.
                    А зачем вам искать абстрактные биндинги? Обычно всё же интересует конкретное свойство — находим его по имени, а дальше смотрим что там для него указано.

                    удобнее чем один символ точки, его легко не заметить
                    А как без точки сделать так?

                    .style.top < scrollTop : 0
                    


                    Кстати, как Ангуляр2 вообще предлагает разруливать такие ситуации? Есть специальный синтаксис для установки стилей?

                    Возьмите десяток джуниоров, покажите им ваш пример и посчитайте сколько из них разберутся что что означает.
                    В синтаксисе ангуляра точно никто не разберётся без усердного вкуривания документации. В моём, впрочем, тоже, правда синтаксис гораздо проще и единообразней (его легче запомнить).

                    Мой ML можно вообще за 5 минут объяснить, при этом он позволяет полностью описывать компоненты (без поведения, конечно). А сколько потребуется времени, чтобы объяснить верстальщику как создавать директивы в ангуляре, как работают скоупы, почему в именах свойств всегда должна быть точечка?


                    1. cold147
                      10.01.2016 18:42

                      стати, как Ангуляр2 вообще предлагает разруливать такие ситуации? Есть специальный синтаксис для установки стилей?

                      Из документации:
                      <div [style.width.px]="mySize">
                      


                      для атрибутов можно так:
                      <div [attr.role]="myAriaRole">
                      


        1. Pryada
          10.01.2016 14:28
          +2

          .required : true
          

          Это очень далеко от js. Похоже на мапу, но натыкаешься на "<" и такой wtf?!
          А в ts ещё и мимикрирует под объявление типа, и по-невнимательности легко перепутать с
          var required: boolean
          


          1. vintage
            10.01.2016 14:36
            +1

            Ну а это похоже на деструктивное присваивание:

            [disabled]="isDisabled"
            


            И что? Если бы тут хватало одного JS, то на JS бы и писали, а не выдумывали свои языки шаблонов.


            1. Fesor
              10.01.2016 14:48

              деструктурированное присваивание — сильно зависит от контекста и будет смущать людей просто в JS. Вот честно пока вы не написали я бы даже не подумал об этом в таком ключе (как минимум потому что я против использования оного для переопределения значений переменных). То есть я привык именно к записи в контексте определения значения новых переменных.


            1. Pryada
              10.01.2016 14:49

              Мне сложно вспомнить хтмл шаблонизатор без собственного синтаксиса. [disabled] хотя бы наглядное и по размерам больше, его легче увидеть, чем "<".


              1. vintage
                10.01.2016 15:18

                Не понимаю с чего бы свойства с биндингом должны быть более заметны, чем свойства без оного. Представьте, что в JS у нас был бы аналогичный синтасис:

                // присваиваем констатну
                foo = 123
                
                // присваиваем значение переменной
                [foo] = bar
                

                Казалось бы, семантически отличаются лишь выражения справа, но синтаксически мы почему-то меняем выражение слева.


                1. Fesor
                  10.01.2016 15:22

                  потому что тут не совсем так

                  foo = 123 // константа, обычное присвоение
                  
                  [foo] = bar // интерпритируется как
                  
                  watchFor('bar', () => foo = bar);
                  


                  согласитесь, совершенно различная семантика присваивания. Первая запись говорит нам о простом присвоении, а вторая запись — о постоянном присвоении значения, мы гарантируем что foo всегда будет иметь значение bar. Аналог присвоения по ссылке, мы же имеем для этого обычно отдельную конструкцию:

                  foo &= bar;


                  1. vintage
                    10.01.2016 16:08

                    Лучше бы оно интерпретировалось в:

                    @FRProp
                    get foo( ){ return 123 }
                    

                    И:
                    @FRProp
                    get foo( ) { return this.bar }
                    


                    Но всё это детали реализации, которые не должны влиять на апи.


                    1. Fesor
                      10.01.2016 16:58

                      ну оно по сути так и интерпритируется внутри. Только чуть сложнее.


  1. ruslanys
    10.01.2016 06:14

    Спасибо. Статья кардинально поменяла моё отношение к Angular2!

    P.S. Поставил бы плюсик статье, если бы в карме на один голос больше было))


  1. dema
    10.01.2016 21:58
    -2

    Как раз только что читал статью с критикой шаблонов Angular2 по сравнению с JSX React'а medium.com/@housecor/angular-2-versus-react-there-will-be-blood-66595faafd51

    Вкратце — Angular 1/2 и многие другие шаблонизаторы это втаскивание JS в HTML (в более или менее кастрированном виде), подход React — втаскивание HTML в JS. При этом в JSX у нас есть весь JavaScript. Так же там был комментарий, под которым я подпишусь — «Изучая Angular, вы инвестируете своё время в Angular, изучая React, вы изучаете JavaScript»



    1. Fesor
      10.01.2016 22:42
      +2

      Это все популистские лозунги. По факту же и там и там смесь HTML и JS.

      Посмотрим на первый попавшийся пример компонента на ReactJS: пример. Что мы тут видим.

      Собственно почти то же самое что мы бы увидели в Angular2 версии. Ну то есть опять же все в одном файлике, HTML с биндингами напрямую к свойствам DOM (то есть никаких специфичных вещей учить не надо почти). Серьезно, я реально не особо вижу разницы с концептуальной точки зрения. То что мы вместо прямой работы с виртуальным DOM должны использовать директивы как элементы декларативной разметки ангуляра — я считаю это плюсом, подобные вещи чуть проще тестировать. В целом же это единственное концептуальное различие. Но если сравнивать именно Angular2 и React — различия не большие и мне это нравится. Angular первой ветки действительно отстает от всего этого сильно.

      По поводу высказывания — все верно. Потому что React кроме компонентов ничего более не предоставляет. Потому все остальное — исключительно забота разработчика. Ванила JS ему в руки. Хочет пусть с нуля все делает, хочет, пусть инфраструктуру готовую использует. Свобода.


  1. Crandel
    11.01.2016 17:11

    Очень подробный обзор, убедил меня приглядеться к Angular 2


  1. th0r
    12.01.2016 12:29
    +1

    Ребят, я сам из реактоводов, поэтому сильно ногами не пинайте. Недавно, в силу остоятельств, пришлось активно курить первый ангуляр и периодически, по мере продвижения по developer guide, у меня на заднице шевелились волосы. Честное слово!

    Большинство этих моментов было связано с шаблонами (и самый ужасающий из них, это когда я читал про передачу параметров в колбэк директивы, отмеченный амперсандом) и поэтому, наткнувшись на эту статью, с любопытством ее открыл в надежде на лучшее.

    Ну что я могу сказать: уже сильно лучше, но направление так и осталось неверное. Ну реально, ребят, неужели никому из вас не кажется, что *ngIf — это ужасающе адский костыль, а присутствие таких вот вещей как раз и говорит о фундаментальных архитектурных просчетах…


    1. Vilyx
      12.01.2016 14:49
      +1

      Если ngif — костыль, то что не костыль? Помесь кода и разметки? Вам это может быть и привычно, но на самом деле это выглядит странно. А ng-if это всего-лишь еще один аттрибут, чем он хуже value?


      1. th0r
        12.01.2016 15:54

        Я не про конкретно ngIf, а про дивную звездочку вначале, которая делает вообще черт знает что.


        1. Fesor
          12.01.2016 16:50
          +1

          директивы, меняющие структуру DOM дерева, требуют того, что бы разработчики использовали элемент template. Вот и все. Что бы не писать

          <template [ngIf]="expression">
            <span>Conditional HTML</span>
          </template>
          


          ввели звездочку, как синтаксический сахар.

          <span *ngIf="expression">Conditional HTML</span>
          


          вы можете не пользоваться звездочками, и иметь явную разметку. А можете пользоваться, и тогда можно будет быстро находить в темплейтах места, где происходит изменение subtree.


          1. th0r
            12.01.2016 17:05

            Ну, если не пользоваться * то вместо вот этого:

            <todo-cmp *ngFor="#t of todos; #i=index" [model]="t" [index]="t"></todo-cmp>
            

            придется писать вот это:

            <template ngFor [ngForOf]="todos" #t="$implicit" #index="i">
              <todo-cmp [model]="t" [index]="i"></todo-cmp>
            </template>
            


            Тут меня пугает очередная непонятная переменная $implicit))


      1. vintage
        12.01.2016 18:17

        Не костыль — это с разделением вёрстки и логики. Например:

        Вёрстка:

        $mol_spoiler : $mol_block child
        	< switcher : $mol_button
        		child < title : null
        		eventClick < eventToggle : null
        	< content : null
        


        Логика:
        get child( ){ return [
                this.switcher ,
                this.expanded ? this.content : null ,
        ] )
        


        И верстальщих счастлив, что у него нет логики, и программист рад, что не надо лезть в вёрстку.


        1. Fesor
          12.01.2016 19:25

          упаси меня сатана от проектов с подобными «разметками».


          1. vintage
            12.01.2016 19:42
            +1

            А что плохого в подобной разметке кроме вашего нежелания разобраться в синтаксисе? Попытка скрестить ужа с ежом как в Ангуляре — сильно лучше?


            1. Fesor
              12.01.2016 19:51
              +2

              нежелания разобраться в синтаксисе?


              Моя субъективная оценка — синтаксис непроработан до конца и нуждается в «чистке». Скажем не совсем понятно зачем постоянно писать дефолтное значение (это же оно там?) даже если его нет (null), или child — это что-то типа кострукция для определения вложенности? А зачем?

              И да, мне нравится то что получилось у ребят из ангуляра. Вы не подумайте, больше решений разных, но конкретно вот это — как по мне путь в никуда. С учетом того что есть довольно много неплохих вариантов описания DSL для UI и т.д. И мне в качестве оного более чем нравится html с особенностями (шаблоны ангуляра — валидный HTML)


              1. vintage
                12.01.2016 20:42

                Предложения по улучшению всегда приветствуются. Насчёт null по умолчанию — так было бы практичней, согласен. child — обычное свойство, их может быть и много, в отличие от html:

                $mol_spolier_demo : $mol_spoiler
                	title : $mol_icon type < iconType : =expand
                	content : $mol_demo_filler
                


                Аргумент про разделение вёрстки и логики — мимо кассы?


    1. aav
      12.01.2016 15:15
      +2

      Ну кому-то больше нравится собирать интерефейс из кусочков, разложенных по переменным:

      var loginButton;
      if (loggedIn) {
        loginButton = <LogoutButton />;
      } else {
        loginButton = <LoginButton />;
      }
      
      return (
        <nav>
          <Home />
          {loginButton}
        </nav>
      );
      


      Не говоря уж о всей мощи JS:
      return (
        <section>
          <h1>Color</h1>
          <h3>Name</h3>
          <p>{this.state.color || "white"}</p>
          <h3>Hex</h3>
          <p>
            {(() => {
              switch (this.state.color) {
                case "red":   return "#FF0000";
                case "green": return "#00FF00";
                case "blue":  return "#0000FF";
                default:      return "#FFFFFF";
              }
            })()}
          </p>
        </section>
      );
      


      Представляю, что там в render будет в сколько-нибудь больших проектах, если по рукам сильно не бить и искуственно не ограничивать «всю мощь JS в HTML».


      1. th0r
        12.01.2016 16:10
        -2

        Вот вам бы как раз я по рукам-то и дал)

        Пример 1:

        return (
          <nav>
            <Home />
            {loggedIn ?
               <LogoutButton />
               :
               <LoginButton />
            }
          </nav>
        );
        

        … а в большинстве случаев лучше даже так:

        render() {
          const { isLoggedIn } = this.props;
        
          return (
            <nav>
              <Home />
              {loggedIn ?
                this.renderSomething()
                :
                this.renderSomethingElse()
              }
            </nav>
          );
        }
        
        renderSomething() {
          return (
            <LogoutButton />
          );
        }
        
        renderSomethingElse() {
          return (
            <LoginButton />
          );
        }
        


        Пример 2:
        const COLORS_IN_HEX = {
          red: '#FF0000',
          green: '#00FF00',
          blue: '#0000FF'
        };
        
        render() {
          const { color } = this.state;
        
          return (
            <section>
              <h1>Color</h1>
              <h3>Name</h3>
              <p>{color || 'white'}</p>
              <h3>Hex</h3>
              <p>
              {this.getColorInHex(color)}
              </p>
            </section>
          );
        }
        
        getColorInHex(color) {
          return COLORS_IN_HEX[color] || '#FFFFFF';
        }
        
        

        Вот она — вся МОЩЬ JS в HTML!)


        1. aav
          12.01.2016 16:25
          +2

          Это не мне — это документации React-а.

          А чего там мощного, вы фактически и идете по пути ограничения всея мощи, чтобы if-ов не было (только тернарный оператор), switch-ей тоже, т.е. искусственно накладываете ограничения на используемый синтаксис.


          1. th0r
            12.01.2016 16:32

            Да есть у вас и if-ы, и switch-и, только их использовать надо так, чтобы читаемость не страдала. Хотить switch — запихните его в getColorInHex и все.


        1. Fesor
          12.01.2016 16:52

          А я бы за тернарный оператор бил по рукам. Вариант приведенный в документации мне лично нравится больше.

          Мне нравится подход react-а, но для моих задач angular подходит как мне кажется больше, да и некоторые аспекты он делает намного проще (а некоторые сложнее, не спорю).


    1. Fesor
      12.01.2016 16:29
      +2

      но направление так и осталось неверное

      Для того что бы ваш комментарий был более конструктивным, расскажите, какое по вашему направление было бы верным и удобным? Ну вот серьезно. Давайте разберемся.

      Oсновное концептуальное различие React и Angular — derty checking, а точнее на каком этапе кто из них его запускает. Angular следит за состоянием данных, React — за виртуальным DOM. Проследим жизненный цикл одной итерации изменений у каждого.

      React представляет UI как композицию компонентов, каждый из которых представлен чистой функцией. На вход функции мы подаем кусок состояния, на выход — кусочек виртуального DOM. Из всех кусочков потом можно сформировать цельное представление, а derty checking происходит за счет сравнение того DOM что бы с тем что у нас сейчас, после чего React генерит патч, который применяется к реальному DOM наиболее эффективным образом. О преимуществах, которые это дает, поговорим чуть чуть позже.

      Angular — тут пошли стандартным путем, и вставили dirty checking между view и моделью (как в классическом MVVM). Шаблоны в этом случае являются декларативной разметкой, описывающей как должен выглядеть UI в зависимости от состояния данных. Вместо того что бы лесть в DOM руками, нам предоставляют директивы, которые вешают биндинги и отслеживают состояния данных, и адаптируют UI под них.

      Время обновления UI у ангуляра и реактор по сути зависит от одного и того же параметра — количество элементов, задействованных в dirty checking цикле. Если сравнивать первую версию angularjs и react, то и там и там весьма примитивная реализация оного. Берем все и сравниваем со всем. При такой реализации преимущество несомненно на стороне React так как мы имеем весьма простое сравнение элементов (в то время как в angular в выражениях разработчик сам прописывает за чем следить и может описать более чем странные конструкции, и не забываем о фильтрах которые замедляют derty checking). Намного меньше подводных камней при реализации приложения. В Angualr2 отслеживание изменений носит уже более реактивных характер, потому таких проблем как в angular1 это не вызывает.

      То есть для меня лично концепция, лежащая в основе ангуляра, если ты правильно описал UI в декларативных шаблонах, то тебе надо просто проследить что ты правильно составляешь состояние. Это немного развязывает руки в плане процессов. Я могу вынести из компонентов шаблоны в отдельные html или jade файлы, и дать доступ на редактирование этого добра какому-нибудь верстальщику, что бы тот уже занимался рутиной вроде подгонки UI под все браузеры и т.д. Это существенно упрощает разделение труда. Ну и аспект с тестированием. Что бы протестировать приложение на angular мне по сути надо проверить что во view экспоузится нужное состояние. И для меня это наиболее ценная штука, поскольку тесты становится намного проще писать, а логика полностью отвязана от UI. Я пока не видел достаточно приложений на react, потому моя выборка может быть нерепрезентативной, но для меня тестирование компонентов в react выглядит намного более неудобным нежели в angular. Хотя плохо и неудобно можно сделать всегда.


      1. th0r
        12.01.2016 17:01

        Нет-нет, вы не поняли, я говорил лишь про view-layer. Что меня ужаснуло в первом ангуляре и от чего так и не избавились во втором — это изобретение своего собственного неочевидного синтаксиса шаблонов.

        В качестве примера достаточно взглянуть на ngOptions: само существование такой директивы заставляет насторожиться.
        «Это же простая итерация по массиву объектов! Зачем она вообще?» — спросит реактовод.
        «А затем, что иначе вы не сможете в значение option передать что-то кроме строки!»
        «Ну, знаете ли...»

        Или еще один пример (о котором я писал выше) про передачу параметров в колбэк директивы, отмеченный амперсандом.
        Для этого надо вызвать ее с объектом в качестве параметра, ключи которого магическим образом превратятся в переменные, доступные в скоупе (js-скоупе) этого колбэка…
        Я перечитывал это раз 10, потому что просто не верил своим глазам.
        «Какого хрена вообще?!» — спросит реактовод.
        «Ну, потому что по-другому сделать не получилось...»
        «Ну, знаете ли...»


        1. Fesor
          12.01.2016 17:09

          В качестве примера достаточно взглянуть на ngOptions


          ну тут соглашусь, это компромис для достижения простой цели — UI как декларативная разметка. В React мы можем использовать все что захотим для формирования DOM (виртуального) а уж как оно в реальности будет рендриться не наша забота. А если ты хочешь сделать UI полностью декларативным, придется придумывать такие вот кастыли. И не могу сказать что это совсем уж что-то страшное.

          Или еще один пример (о котором я писал выше) про передачу параметров в колбэк директивы


          Тут уже попроще. Это не «коллбэк» директивы. Ну тоесть по сути это он родимый и есть, но не совсем так. Это выражение (опциональное обычно), которое имеет возможность запускать директива не теряя при этом изоляции. Ну то есть прямой аналог тут будет:

          function link(scope, element, attrs) {
              scope.$apply(attrs.onClick);
          }
          


          А что до передачи в качестве аргументов объекта — это контекст, в котором выполняется выражение. Задача директивы, предоставить выражению контекст, содержащий данные. Это позволяет здорово изолировать систему и снизить связанность.

          Выражения в angular это такой безопасный eval, контекстом которого мы можем управлять (eval это плохо не потому что динамически выполняется что-то, а потому что мы не можем управлять контекстом и ограничивать сайд эффекты).


          1. th0r
            12.01.2016 17:18
            -1

            В общем, когда ребята из двух лагерей подружатся и общими усилиями заменят ангуляроские шаблоны+фильтры+директивы+контроллеры реактом на моей улице наступит праздник))


            1. Fesor
              12.01.2016 17:34
              +2

              на моей улице наступит праздник))


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

              А то что двум лагерям надо меньше враждовать и больше обсуждать схожие вещи и как можно улучшить дела — это да. Этого нехватает.


        1. aav
          12.01.2016 17:29

          «Это же простая итерация по массиву объектов! Зачем она вообще?» — спросит реактовод.
          «А затем, что иначе вы не сможете в значение option передать что-то кроме строки!»
          «Ну, знаете ли...»

          А как это будет выглядет в реакте?


          1. Fesor
            12.01.2016 17:32

            просто добавляем элементы в цикле, ничего особо сложного.


            1. aav
              12.01.2016 17:34

              В Angular тоже можем просто добавлять элементы в цикле, выбор то объекта вместо текстового значения как будет происходить?


  1. blackswanny
    12.01.2016 20:21

    Angular 2 не решил главную проблему — неконтролируемую связность директив\модулей, что по сути имеется в MV* архитектурах тоже. Даже опытные разработчики, смешивая директивы, используя scope и другую магию, вставляют новые «подпорки» для удовлетворения новых требований. Новый синтаксис лишь уменьшил возможность подобных манипуляций, но я чувствую, что Angular все еще испорчен.


    1. Fesor
      12.01.2016 20:58

      неконтролируемую связность директив\модулей, что по сути имеется в MV* архитектурах тоже


      Можете пояснить? Ибо я не особо понимаю о чем вы. Ибо никаких проблем с неконтролируемой связностью в angular2 нет, ну либо я уже отношусь к той категории людей которые не используют в ангуляре скоуп и «магию» со времен angular 1.2 и потому мне сложно понять о чем вы.


      1. blackswanny
        13.01.2016 00:23

        Angular 2 все еще поддерживает аттрибутивные директивы (а старый еще и классовые). В итоге можно было получить что-то типа

           <main-directive  sub-directive1 subdirective2><main-directive>
        

        и каждая примешивала свой функционал, что не разберешь, откуда что берется. Также старый Angular использовал $scope (или даже $rootScope), и люди ошибочно его использовали для связи компонентов между собой, в новом заменено на this, однако я не знаю, насколько они различны


        1. Vilyx
          13.01.2016 00:35

          Простите, но вы своё оружие не тем концом держите, много чего можно наворотить нарушив goodpractices. В голову даже не лезет зачем так делать как вы написали, в том смысле, какую от этого можно получить пользу.


          1. Fesor
            13.01.2016 00:42

            В голову даже не лезет зачем так делать как вы написали


            Вы писали на angular 1.0? в те времена подругому никак нельзя было. И в 1.1. В 1.2 уже можно было отказаться от скоупа в контроллерах, но в директивах еще было неудобно. в 1.3 уже можно было делать полноценные компоненты, а удобно это станет только с выходом 1.5.

            Ну и да — у Angular 1.x ужасная документация. Ну то есть 3-4 года назад это было ок, и на фоне всего остального было круто. Но сегодня… ужас дикий, причем официальная дока и примеры никак не придерживаются этим самым goodpractices. А потому людей сходу учили плохим вещам.

            Я очень рад что документация Angular2 в этом плане намного более качественнее сделана. Но примеры еще можно было бы порефакторить.


        1. Fesor
          13.01.2016 00:41

          все еще поддерживает аттрибутивные директивы


          Да, суть в простом разделении. Компоненты — существительные, сущности, отдельные UI компоненты. Атрибутивные директивы — прилагательные, декораторы для компонентов. Простой пример — компонет input и декоратор required добавляющий валидацию, расширяющий поведение компонента.

          Лично мне подобное разделение нравится, так как оно пораждает меньше сложности и позволяет удобно расширять функционал компонентов. Но не думаю что эту фичу будут часто юзать.

          Что до классовых директив — полностью согласен. Я к слову знаю пару людей которые расстроены тем что в angular2 так больше нельзя. Благо после пары холиваров они угомонились и приняли что это не ок вариант.

          что не разберешь, откуда что берется

          Если соблюдать правила (компонент — существительное, директива — прилагательное) и нормально именовывать директивы — то все довольно ок. Но да, я согласен что сделать плохо можно.

          Также старый Angular использовал $scope (или даже $rootScope), и люди ошибочно его использовали для связи компонентов между собой


          О да, и тут больше даже грех документации. Где-то с версии 1.3 angular можно вообще не использовать $scope (собственно я его не особо часто использую уже с версии 1.2 с введением controllerAs). Документация до сих пор преподносит инфу 2012-ого года как актуальную, хотя в best pratice и style guide хотя бы описано что это не ок идея. И что использовать $broadcast/$emit для общения своих директив это не ок говорится.

          Я полагаю, что если бы в 2013-ом году обновили документацию и PhoneCat туториал под существующие уже на тот момент бэст практисы, проблем было бы меньше. Все же большинство сразу после полу часа чтения документации (который описывает именно $scope и подход с контроллерами) идут писать приложеньки а бэст практис — ну их.

          В angular2 такой проблемы нет, все «плохое» спрятаны глубоко внутри.


          1. blackswanny
            13.01.2016 01:59
            +1

            Все это верно. Я собственно не спорю, а привел примеры, как можно испортить хороший фреймворк, если его плохо готовить. По-моему, многих разработчиков лучше ограничить изначально возможностями технологии, чем просить их следовать хорошим практикам (если есть кому просить). Хорошо, что в Angular 2 большая часть болезней вылечена, но бренд уже испортил себе репутацию. Есть еще и другие аспекты, которые мне не нравятся в Angular, но это уже на вкус и цвет. К примеру, декларативный язык выглядит приятнее с первого взгляда, но если в него положить много (кстати, сколько это), то читать его становится невозможно, плюс отладка тяжелая, что отмечается тут. Также у меня плохие отношения с двухсторонним биндингом. Опять же, это не явные недостатки, а вкусы


            1. Fesor
              13.01.2016 03:21

              но если в него положить много (кстати, сколько это)


              По этой причине мне нравятся инлайн шаблоны (с ES6 это удобно), так как проще контролировать разбухание. В целом большие шаблоны можно посчитать техническим долгом и зарефакторить позже. В некоторых случаях дробить UI на компоненты слишком дотошно может быть оверхэдом. Все зависит от контекста, за что мне ангуляр и нравится.

              Я стараюсь дробить компоненты таким образом, что бы они становились реально реюзабельными и помещались в один экран (с учетом ограничения по длине строки).

              плюс отладка тяжелая

              А вот тут статья немного лукавит. Как я отметил в статье, ангуляр теперь будет выкидывать ошибки при компиляции шаблонов и выполнении выражений (а не молчать как в 1.x затрудняя отладку). То есть выражение {{foobar.test}}' в случае если foobar не объект, будет падать с TypeError и т.д.

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

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

              Также у меня плохие отношения с двухсторонним биндингом


              В angular2 двустороннего биндинга нет, есть сахар для того что бы не писать постоянно проброс данных в обе стороны (иногда полезно, редко, но бывает).

              С учетом того что теперь помимо ngModel есть ngFormModel, то необходимости в использовании двустороннего биндинга почти не остается. Только для очень простых случаев для которых подобные вещи выглядят оверхэдом.


              1. vintage
                13.01.2016 09:43

                {{foobar.test()}}
                


                Если в test возникнет исключение, оно будет проглочено как в первой версии или всё же выведено в консоль?

                Интуиция мне подсказывает, что ngFormModel — не серебряная пуля.


                1. Fesor
                  13.01.2016 11:42

                  Если в test возникнет исключение, оно будет проглочено как в первой версии или всё же выведено в консоль?


                  выведет в консоль. Причем сначала ангуляр обернет исключение в свое (EXCEPTION: Error during evaluation of "click"), ну и выведет ORIGINAL EXCEPTION: Error: Your exception message.

                  что ngFormModel — не серебряная пуля.


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


                1. aav
                  13.01.2016 11:43
                  +1

                  Да в общем-то и там и там не проглатывает:
                  1.х: plnkr.co/edit/POgFi6FHZk1cyhf75NgA?p=preview
                  2.х: plnkr.co/edit/sQdCcD2tPrtEt4rBFst4?p=preview


                  1. Fesor
                    13.01.2016 11:50

                    единственное что для 2.x ошибка теперь чуть более детализирована. Появляется информация о контексте ошибки, ну мол у какого элемента это и все такое.


                  1. vintage
                    13.01.2016 16:50

                    Посыпаю голову пеплом, видимо не в ангуляре было дело.