Статьи

Изучение Angular2

Команда Angular в настоящее время сокращает свое отставание, обещая бета-версию «очень скоро сейчас» ™. Тем временем я играл с их альфа-релизами. Да, это так больно, как кажется . Тем не менее, это дало мне четкое понимание того, куда концептуально движется структура. Посмотрите код , а затем вернитесь к такому необходимому контексту!

Пример приложения предоставляет альтернативный интерфейс для YouTube. Вы можете искать живую музыку, указав имя исполнителя. Результаты поиска могут быть либо сохранены, либо воспроизведены напрямую. Плейлист поддерживается местным хранилищем. Это не большое приложение, но и не игрушечное приложение. Это ‘YouTube Live!’ В бою:

Машинопись

Первое, что вы заметите, глядя на код, это то, что YouTube работает! написано в TypeScript. Зачем? Ну, во-первых, я думаю, что TypeScript — это огромное улучшение по сравнению с простым JavaScript. Посмотрите мой доклад о TypeScript, чтобы понять почему. Этот пост предполагает некоторое знакомство с ES6 и TypeScript. Опять же, смотрите разговор, если вы хотите освежить в памяти это. В нем много живого кодирования, поэтому я обещаю, что вам не будет скучно.

Основная причина написания приложений Angular2 на TypeScript заключается в том, что сам Angular2 написан на TypeScript. Вы по-прежнему можете писать приложения Angular2 на простом JavaScript (ES5 или ES6), но вы упустите некоторые синтаксические тонкости. И пропустите полную проверку типов во время компиляции вашего клиентского приложения. Поверь мне, это большое дело.

После клонирования репо вы можете установить и запустить приложение с помощью npm install && npm start. Макет приложения выглядит следующим образом:

src/
├── index.html
├── tsconfig.json
├── node_modules/
├── lib/
└── ytlive/
    ├── playlist/
    │   ├── PlaylistBackend.ts
    │   ├── PlaylistComponents.ts
    │   ├── playlist.html
    │   └── playlistentry.html
    ├── search/
    │   ├── SearchComponents.ts
    │   ├── YTLiveBackend.ts
    │   ├── search.html
    │   └── searchresult.html
    ├── ytlive.html
    └── ytlive.ts

Код хорошо модульный с использованием модулей ES6, поддерживаемых TypeScript. Angular2 покончил с собственной системой модулей. Это решает неловкие проблемы определения дубликатов модулей при объединении AngularJS 1.x с загрузчиками модулей, такими как require.js. Обратите внимание, что Angular2 распространяется в виде пакета npm, поэтому установка (и последующее обновление) очень проста.

Составные части

Когда вы открываете исходные файлы, сразу становится ясно, что Angular2 — это совершенно другой фреймворк из AngularJS 1.x. Концептуально вы узнаете некоторые вещи, но на техническом уровне это чистый лист. Если это вас немного пугает, я искренне сочувствую. Тем не менее, компонентный подход Angular2 определенно является шагом вперед. Начать сначала было смелым шагом команды Angular, и это окупается, о чем свидетельствуют некоторые предварительные показатели производительности .

Напоминает React, все ваше приложение построено в виде дерева компонентов:

Компоненты Angular2 заменяют целый ряд абстракций, известных нам из AngularJS 1.x. Это существенно объединяет сервисы, контроллеры и директивы. Компонент — это аннотированный класс, который может ссылаться на связанный шаблон HTML:

PlaylistComponents.ts :

import { Component, View, NgFor } from 'angular2/angular2';
import { LocalStoragePlayList } from './PlaylistBackend';

// PlaylistEntryComponent class definition omitted for brevity.

@Component({
  selector: 'playlist',
  providers: [LocalStoragePlayList]
})
@View({
  templateUrl: "ytlive/playlist/playlist.html",
  directives: [NgFor, PlaylistEntryComponent]
})
export class PlaylistComponent {

  constructor(private playlistService: LocalStoragePlayList) { }

  get entries(): ConcertSummary[] {
    return this.playlistService.getPlaylist();
  }

}

В частности, аннотация @View содержит ссылку на этот шаблон:

playlist.html :

<div class="playlist row">
  <div *ng-for="#entry of entries">
    <playlist-entry [entry]="entry"></playlist-entry>
  </div>
</div>

Вместе шаблон и класс компонента образуют единое целое. Через свойство селектора в аннотации @Component мы контролируем, как этот компонент может быть создан в шаблонах. В приведенном выше шаблоне мы аналогичным образом используем этот playlist-entryэлемент для создания экземпляров вложенных компонентов для каждого из entryнас в PlaylistComponent. Эти записи поступают из метода получения entries()этого компонента. Используя [entry]="entry"синтаксис, мы передаем текущую запись в итерации entryсвойству экземпляра вложенного компонента (который является простым элементом класса в классе PlaylistEntryComponent).

Обратите внимание, что мы используем два пользовательских элемента в шаблоне: ng-forи playlist-entry. Глядя на класс PlaylistComponent, вы видите, что они явно перечислены directivesв аннотации @View. Больше не нужно гадать, откуда берутся «волшебные» элементы! Это прямо там. И не только как строки, но и должным образом импортированные и ссылающиеся из файла, в котором они определены. В этом случае ng-for родом из самого Angular2, а PlaylistEntryComponent определен ранее в том же файле (опущено выше). Возможно, вас интересует слегка прикольный синтаксис со звездочками и скобками. К счастью, есть метод безумия. Прочтите этот пост для более глубокого изучения синтаксиса шаблона Angular2. И да, это на 100% правильный синтаксис HTML-атрибута , если вам интересно.

Одно справедливое предупреждение при работе с компонентами: порядок объявления компонентов в одном исходном файле имеет значение. Сначала я определил PlaylistComponent, а PlaylistEntryComponent позже в файле. Это казалось таким логичным, но оно сломалось во время выполнения. Есть прямая ссылка на класс, который еще не существует в directivesсвойстве PlaylistComponent. Это делает для некоторых хороших сообщений об ошибках и трассировки стека в консоли, я могу вам сказать. (для неудачливого гуглера, который страдает от этой проблемы: «ИСКЛЮЧЕНИЕ: неожиданное значение директивы« неопределенное »в представлении компонента« PlaylistComponent »было ошибкой с Angular2.alpha45 и более ранними версиями)

Мораль истории: определите (или импортируйте) свои компоненты, прежде чем ссылаться на них в других компонентах. Или прибегнуть к уродливым обходным путям .

Взаимодействие компонентов

Таким образом, у нас есть дерево компонентов, компоненты инкапсулируют данные и могут визуализировать себя изначально. Следующий вопрос: как что-то сделать? Как компоненты взаимодействуют с пользователем и друг с другом?

С AngularJS 1.x вы по умолчанию используете двухстороннее связывание данных. В Angular2 по умолчанию данные передаются однонаправленно, от корневого компонента до дочерних. Мы уже видели пример передачи данных дочерним компонентам через их атрибуты, которые заканчиваются на членах класса компонентов. Здесь одностороннее движение. У вас есть два основных способа взаимодействия между произвольными неиерархическими компонентами: события и общие компоненты.

В этом примере используются общие компоненты. Также возможно определить пользовательские события и запустить поведение по всему дереву компонентов. Однако, не все пользовательские события распространяются правильно в альфа-версиях, с которыми я работал. Вы не найдете пример использования пользовательских событий в YouTube Live !, но вы можете найти больше информации в этом посте .

Примером общих компонентов в действии является воспроизведение видео на YouTube в прямом эфире. Видео можно запустить как из записей плейлиста, так и из результатов поиска. Эта общая функциональность может быть достигнута простым созданием класса VideoPlayer с соответствующими методами и состоянием:

export class VideoPlayer {
  public isPlaying = false;
  public currentVideoUrl: string

  public playConcert(id: string) {
    this.isPlaying = true;
    this.currentVideoUrl = this.concertIdToEmbedUrl(id);
  }

  public stop() {
    this.isPlaying = false;
    this.currentVideoUrl = undefined;
  }

  private concertIdToEmbedUrl(id: string): string {
    return yt_embed + id + '?showinfo=0&autoplay=1';
  }
}

Это просто простой класс, никаких специальных ангулярных аннотаций не требуется. Нет представления прилагается. Одно предостережение: если бы мы хотели внедрить другие компоненты в этот класс, аннотация @Injectable была бы необходима. Мы можем внедрить этот класс VideoPlayer в существующие компоненты через их конструкторы. Это немного похоже на сервисы в AngularJS 1.x.

Возьмем, к примеру, компонент SearchResult, показывающий внедрение конструктора:

@Component({
  selector: 'search-result',
  properties: ["concert"],
  providers: [LocalStoragePlayList]
})
@View({
  templateUrl: "ytlive/search/searchresult.html",
  directives: []
})
class SearchResultComponent {
  concert: ytbackend.ConcertSummary

  constructor(private playlistService: LocalStoragePlayList,
     private videoPlayer: ytbackend.VideoPlayer) {}

  addToPlaylist(concert: ytbackend.ConcertSummary) {
    this.playlistService.addConcert(concert);
  }

  playConcert(id: string) {
    this.videoPlayer.playConcert(id);
  }
}

В конструктор вставляются две вещи: LocalStoragePlayList (чтобы мы могли сохранять результаты поиска) и VideoPlayer (чтобы мы могли воспроизводить результаты поиска). В аннотации @Component вы можете видеть, что внедрение LocalStoragePlaylist настроено в providersсвойстве. Но VideoPlayer там не упоминается. Как придешь? Когда вы определяете провайдера, это также уровень, на котором создается компонент, который должен быть внедрен. Этот экземпляр затем становится доступным для компонента и всех его дочерних компонентов для внедрения. Поэтому провайдер VideoPlayer настроен в корнеYTLiveComponent, Таким образом, один и тот же экземпляр VideoPlayer внедряется во все компоненты, которые запрашивают его в своих конструкторах. Это хорошо, потому что есть только одна область просмотра для видео. Одно видео может быть воспроизведено одновременно, что делает VideoPlayer общим ресурсом, который используется несколькими другими компонентами.

Воспроизведение концерта так же просто, как вызов playConcertметода для SearchResultComponent из шаблона searchresult:

searchresult.html :

<!-- lots of stuff omitted -->
<button title="Play now" class="play btn btn-success">
    <span (click)="playConcert(concert.id)" class="glyphicon glyphicon-play-circle"></span>
</button>

Он связывает событие click в этом промежутке с playConcertметодом в SearchResultComponent, который, в свою очередь, вызывает общий компонент VideoPlayer. Такая привязка к DOM-событиям является основным средством взаимодействия с пользователем. Очевидно, что компоненты более высокого уровня доступны для простой интеграции компонентов ввода и так далее.

Состояние экземпляра VideoPlayer отслеживается в другом компоненте / шаблоне:

search.html :

<!-- lots of stuff omitted -->
<div *ng-if="playing" id="concerts" class="row">
  <iframe width="100%" height="100%" [src]="embedUrl" frameborder="0" allowfullscreen></iframe>
</div>

[src]Синтаксис связывает Src свойства фрейма к embedUrlсвойству компонента для этого шаблона. Если embedUrl изменяется, src iframe автоматически обновляется (но не наоборот).

Сервис Http

Angular — это больше, чем просто инфраструктура компонентов. В AngularJS 1.x была служба $ http для выполнения внутренних вызовов. То же самое относится и к Angular2. Вместо того чтобы возвращать (свой собственный вариант) Promises, как в 1.x, новый компонент Http возвращает RX Observables . Angular2 принимает RxJ в качестве основной зависимости, вы видите, что это появляется в нескольких API. Требуется некоторое привыкание, но RxJs — это проверенная библиотека, предлагающая отличный способ для создания асинхронных потоков данных.

В YouTube live !, мы используем встроенный компонент Http для вызовов API YouTube:

@Injectable()
export class ConcertService {

  private concerts: ConcertSummary[];

  constructor(private http: Http) { }

  public findConcerts(artist: string, duration = Duration.FULLCONCERT): any {
    var ytDuration: string;

    // .. snipped for brevity ..

    var searchString = yt_search + ytDuration + '&q=' + encodeURIComponent('live ' + artist);

    return this.http.get(searchString).map((res: any) => {
      var ytResults: {items: YTSearchResult[] } = res.json();
      var transformedResults = ytResults.items.map(this.toConcertSummary)
      this.concerts = transformedResults;
      return transformedResults;
    });
  }
}

Опять же, мы видим компонент без обзора, но на этот раз с аннотацией @Injectable, поскольку нам нужно, чтобы Angular вставлял компонент Http в конструктор. После выполнения getвызова результат трансформируется с помощью mapнаблюдаемого. Это возвращает другое наблюдаемое, теперь содержащее данные в формате, который мы можем использовать. Небольшое раздражение вызывает то, что Http.get возвращает anyтекущее определение типа Angular2. Было бы неплохо использовать определения типов RxJS для Observables, так что мы можем получить здесь некоторую здравомыслие во время компиляции.

Результирующая Observable используется в searchConcertsметоде SearchComponent:

export class SearchComponent {

  private concerts: ytbackend.ConcertSummary[] = [];

  constructor(private concertService: ytbackend.ConcertService,
      private videoPlayer: ytbackend.VideoPlayer) { }

  searchConcerts(): void {
    this.videoPlayer.stop();
    this.concertService
      .findConcerts(this.searchTerm)
      .subscribe((results: ytbackend.ConcertSummary[]) => this.concerts = results);
  }
}

Поскольку ConcertService возвращает наблюдаемое, мы не можем назначить его непосредственно члену класса типа ConcertSummary[]. Вместо этого мы подписываемся на наблюдаемое и назначаем результат, как только наш подписчик вызывается, когда результаты доступны. Шаблон автоматически обнаруживает изменения concertsи отображает новые результаты вызова API. Было бы неплохо, если бы эта «распаковка» из Observables не понадобилась.

Завершение

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

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

Поиграйте с кодом для YouTube в прямом эфире и дайте мне знать, что вы думаете!