Как создать игру на сопоставление карт с помощью Angular и RxJS

Опубликовано: 2022-03-10
Краткое резюме ↬ Эта статья посвящена разработчикам Angular, которые хотят использовать концепцию реактивного программирования. Это стиль программирования, который, проще говоря, имеет дело с обработкой асинхронных потоков данных.

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

Что мы строим?

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

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

Скриншот обучающей игры «Собери пары»
Краткий обзор игры, которую мы будем создавать сегодня

В этом уроке мы шаг за шагом создадим такую ​​обучающую игру. В первой части мы создадим компонент Angular, который просто показывает игровое поле игры. Наша цель состоит в том, чтобы компонент можно было настроить для различных вариантов использования и целевых групп — от викторины с животными до словарного тренера в приложении для изучения языка. Для этой цели Angular предлагает концепцию проецирования контента с помощью настраиваемых шаблонов, которые мы будем использовать. Чтобы проиллюстрировать принцип, я создам две версии игры («game1» и «game2») с разными макетами.

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

  • Требования
    Чтобы следовать этому руководству, необходимо установить Angular CLI.
  • Исходный код
    Исходный код этого руководства можно найти здесь (14 КБ).
Еще после прыжка! Продолжить чтение ниже ↓

1. Создание компонента Angular для обучающей игры

Как создать базовую структуру

Во-первых, давайте создадим новый проект под названием «learning-app». С Angular CLI вы можете сделать это с помощью команды ng new learning-app . В файле app.component.html я заменяю предварительно сгенерированный исходный код следующим образом:

 <div> <h1>Learning is fun!</h1> </div>

На следующем этапе создается компонент обучающей игры. Я назвал его «matching-game» и использовал команду ng generate component matching-game . Это создаст отдельную подпапку для игрового компонента с необходимыми файлами HTML, CSS и Typescript.

Как уже было сказано, обучающая игра должна быть настраиваемой для разных целей. Чтобы продемонстрировать это, я создаю два дополнительных компонента ( game1 и game2 ) с помощью той же команды. Я добавляю игровой компонент в качестве дочернего компонента, заменяя предварительно сгенерированный код в файле game1.component.html или game2.component.html следующим тегом:

 <app-matching-game></app-matching-game>

Сначала я использую только компонент game1 . Чтобы убедиться, что игра 1 отображается сразу после запуска приложения, я добавляю этот тег в файл app.component.html :

 <app-game1></app-game1>

При запуске приложения с помощью ng serve --open браузер отобразит сообщение «matching-gameworks». (В настоящее время это единственное содержимое match-game.component.html .)

Теперь нам нужно проверить данные. В папке /app я создаю файл с именем pair.ts, в котором определяю класс Pair :

 export class Pair { leftpart: string; rightpart: string; id: number; }

Парный объект содержит два связанных текста ( leftpart и rightpart ) и идентификатор.

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

В файле animals.ts я определяю массив с тестовыми данными:

 import { Pair } from './pair'; export const ANIMALS: Pair[] = [ { id: 1, leftpart: 'dog', rightpart: 'mammal'}, { id: 2, leftpart: 'blickbird', rightpart: 'bird'}, { id: 3, leftpart: 'spider', rightpart: 'insect'}, { id: 4, leftpart: 'turtle', rightpart: 'reptile' }, { id: 5, leftpart: 'guppy', rightpart: 'fish'}, ];

Компонент game1 нуждается в доступе к нашим тестовым данным. Они хранятся в собственности animals . Файл game1.component.ts теперь имеет следующее содержимое:

 import { Component, OnInit } from '@angular/core'; import { ANIMALS } from '../animals'; @Component({ selector: 'app-game1', templateUrl: './game1.component.html', styleUrls: ['./game1.component.css'] }) export class Game1Component implements OnInit { animals = ANIMALS; constructor() { } ngOnInit() { } }

Первая версия игрового компонента

Наша следующая цель: игровой компонент matching-game должен принимать игровые данные от родительского компонента (например game1 ) в качестве входных данных. Вход представляет собой массив «парных» объектов. Пользовательский интерфейс игры должен быть инициализирован переданными объектами при запуске приложения.

Скриншот обучающей игры «Собери пары»

Для этого нам нужно действовать следующим образом:

  1. Добавьте pairs свойств в игровой компонент с помощью декоратора @Input .
  2. Добавьте solvedPairs и unsolvedPairs как дополнительные приватные свойства компонента. (Необходимо различать уже «решенные» и «еще не решенные» пары.)
  3. Когда приложение запускается (см. функцию ngOnInit ), все пары остаются «нерешенными» и поэтому перемещаются в массив unsolvedPairs .
 import { Component, OnInit, Input } from '@angular/core'; import { Pair } from '../pair'; @Component({ selector: 'app-matching-game', templateUrl: './matching-game.component.html', styleUrls: ['./matching-game.component.css'] }) export class MatchingGameComponent implements OnInit { @Input() pairs: Pair[]; private solvedPairs: Pair[] = []; private unsolvedPairs: Pair[] = []; constructor() { } ngOnInit() { for(let i=0; i<this.pairs.length; i++){ this.unsolvedPairs.push(this.pairs[i]); } } }

Кроме того, я определяю HTML-шаблон компонента matching-game . Есть контейнеры для нерешенных и решенных пар. Директива ngIf гарантирует, что соответствующий контейнер отображается только в том случае, если существует хотя бы одна нерешенная или решенная пара.

В контейнере для нерешенных пар (класс container unsolved ) сначала перечислены все left (см. левый кадр в GIF выше), а затем все right (см. правый кадр в GIF) компоненты пар. (Для отображения пар я использую директиву ngFor .) На данный момент в качестве шаблона достаточно простой кнопки.

С выражением шаблона {{{pair.leftpart}} и { {{pair.rightpart}}} значения свойств leftpart и rightpart отдельных парных объектов запрашиваются при повторении массива pair . Они используются в качестве меток для сгенерированных кнопок.

Назначенные пары перечислены во втором контейнере ( container solved контейнер класса). Зеленая полоса ( connector класса) указывает на то, что они принадлежат друг другу.

Соответствующий код CSS файла matching-game.component.css можно найти в исходном коде в начале статьи.

 <div> <div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.leftpart}} </button> </div> <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.rightpart}} </button> </div> </div> <div class="container solved" *ngIf="solvedPairs.length>0"> <div *ngFor="let pair of solvedPairs" class="pair"> <button>{{pair.leftpart}}</button> <div class="connector"></div> <button>{{pair.rightpart}}</button> </div> </div> </div>

В компоненте game1 массив animals теперь привязан к свойству pairs компонента matching-game (односторонняя привязка данных).

 <app-matching-game [pairs]="animals"></app-matching-game>

Результат показан на изображении ниже.

Текущее состояние пользовательского интерфейса
Текущее состояние пользовательского интерфейса

Очевидно, что наша игра на соответствие еще не слишком сложна, потому что левая и правая части пар прямо противоположны друг другу. Чтобы спаривание не было слишком тривиальным, следует смешать нужные части. Я решаю проблему с самоопределяемым shuffle канала, которое я применяю к массиву unsolvedPairs с правой стороны ( test параметра необходим позже, чтобы принудительно обновить канал):

 ... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...

Исходный код пайпа хранится в файле shuffle.pipe.ts в папке приложения (см. исходники в начале статьи). Также обратите внимание на файл app.module.ts , где канал должен быть импортирован и указан в объявлениях модуля. Теперь нужный вид появится в браузере.

Расширенная версия: использование настраиваемых шаблонов для создания индивидуального дизайна игры

Вместо кнопки должна быть возможность указать произвольные фрагменты шаблона для настройки игры. В файле Matching-game.component.html я заменяю шаблон кнопки для левой и правой стороны игры тегом ng-template . Затем я присваиваю имя ссылки на шаблон свойству ngTemplateOutlet . Это дает мне два заполнителя, которые заменяются содержимым соответствующей ссылки на шаблон при рендеринге представления.

Здесь мы имеем дело с концепцией проекции контента : определенные части шаблона компонента задаются извне и «проецируются» в шаблон в отмеченных позициях.

При создании представления Angular должен вставить игровые данные в шаблон. Параметром ngTemplateOutletContext я сообщаю Angular, что внутри шаблона используется переменная contextPair , которой должно быть присвоено текущее значение переменной pair из директивы ngFor .

В следующем листинге показана замена контейнера unsolved . В solved контейнере кнопки также должны быть заменены тегами ng-template .

 <div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <div *ngFor="let pair of unsolvedPairs" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> <div class="pair_items right"> <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> </div> ...

В файле matching-game.component.ts должны быть объявлены переменные обеих ссылок на шаблоны ( leftpart_temp и rightpart_temp ). Декоратор @ContentChild указывает, что это проекция контента, т.е. Angular теперь ожидает, что два фрагмента шаблона с соответствующим селектором ( leftpart или rightpart ) предоставлены в родительском компоненте между тегами <app-matching-game></app-matching-game> элемента host (см. @ViewChild ).

 @ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;

Не забывайте: типы ContentChild и TemplateRef должны быть импортированы из основного пакета.

В родительском компоненте game1 теперь вставлены два обязательных фрагмента шаблона с селекторами leftpart и rightpart .

Для простоты я снова буду использовать кнопки здесь:

 <app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <button>{{animalPair.leftpart}}</button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button>{{animalPair.rightpart}}</button> </ng-template> </app-matching-game>

Атрибут let-animalPair="contextPair" используется для указания того, что переменная контекста contextPair используется во фрагменте шаблона с именем animalPair .

Фрагменты шаблона теперь можно изменить по своему вкусу. Чтобы продемонстрировать это, я использую компонент game2 . Файл game2.component.ts получает то же содержимое, что и game1.component.ts . В game2.component.html я использую индивидуально разработанный элемент div вместо кнопки. Классы CSS хранятся в файле game2.component.css .

 <app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <div class="myAnimal left">{{animalPair.leftpart}}</div> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <div class="myAnimal right">{{animalPair.rightpart}}</div> </ng-template> </app-matching-game>

После добавления тегов <app-game2></app-game2> на главную страницу app.component.html при запуске приложения появляется вторая версия игры:

Альтернативный вид игры в компоненте game2
Альтернативный вид игры в компоненте game2

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

2. Контроль взаимодействия пользователя с RxJS

Преимущества реактивного программирования с RxJS

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

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

Библиотека JavaScript RxJS обеспечивает зрелую реализацию шаблона проектирования Observer. Кроме того, RxJS содержит множество операторов для преобразования потоков (например, filter, map) или их объединения в новые потоки (например, merge, concat). Операторы являются «чистыми функциями» в смысле функционального программирования: они не производят побочных эффектов и не зависят от состояния вне функции. Логика программы, состоящая только из вызовов чистых функций, не нуждается в глобальных или локальных вспомогательных переменных для хранения промежуточных состояний. Это, в свою очередь, способствует созданию не имеющих состояния и слабо связанных блоков кода. Поэтому желательно реализовать большую часть обработки событий с помощью продуманной комбинации операторов потока. Примеры этого приведены в разделе после следующего, основанном на нашей игре на соответствие.

Интеграция RxJS в обработку событий компонента Angular

Фреймворк Angular работает с классами библиотеки RxJS. Таким образом, RxJS автоматически устанавливается при установке Angular.

На изображении ниже показаны основные классы и функции, которые играют роль в наших рассуждениях:

Модель основных классов для обработки событий в Angular/RxJS.
Модель основных классов для обработки событий в Angular/RxJS.
Имя класса Функция
Наблюдаемый (RxJS) Базовый класс, представляющий поток; другими словами, непрерывная последовательность данных. На наблюдаемое можно подписаться. Функция pipe используется для применения одной или нескольких операторных функций к наблюдаемому экземпляру.
Тема (RxJS) Подкласс observable предоставляет следующую функцию для публикации новых данных в потоке.
Эмиттер событий (угловой) Это специфичный для angular подкласс, который обычно используется только в сочетании с декоратором @Output для определения вывода компонента. Как и следующая функция, функция emit используется для отправки данных подписчикам.
Подписка (RxJS) Функция subscribe наблюдаемого объекта возвращает экземпляр подписки. Требуется отменить подписку после использования компонента.

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

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

Для этого я определяю четыре выходных значения типа EventEmitter в файле matching-game.component.ts . Типы Output и EventEmitter должны быть импортированы из основного пакета.

 @Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();

В шаблоне Matching-game.component.html я реагирую на событие mousedown с левой и правой стороны, а затем отправляю ID выбранного элемента всем получателям.

 <div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)">

В нашем случае получателями являются компоненты game1 и game2 . Теперь вы можете определить обработку событий для событий leftpartSelected , rightpartSelected , leftpartUnselected и rightpartUnselected . Переменная $event представляет выдаваемое выходное значение, в нашем случае идентификатор. Ниже вы можете увидеть листинг для game1.component.html , для game2.component.html применяются те же изменения.

 <app-matching-game [pairs]="animals" (leftpartSelected)="onLeftpartSelected($event)" (rightpartSelected)="onRightpartSelected($event)" (leftpartUnselected)="onLeftpartUnselected()" (rightpartUnselected)="onRightpartUnselected()"> <ng-template #leftpart let-animalPair="contextPair"> <button [class.selected]="leftpartSelectedId==animalPair.id"> {{animalPair.leftpart}} </button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button [class.selected]="rightpartSelectedId==animalPair.id"> {{animalPair.rightpart}} </button> </ng-template> </app-matching-game>

В game1.component.ts (и аналогично в game2.component.ts ) теперь реализованы функции event событий. Я сохраняю идентификаторы выбранных элементов. В шаблоне HTML (см. выше) этим элементам назначается класс selected . Файл CSS game1.component.css определяет, какие визуальные изменения вызовет этот класс (например, изменения цвета или шрифта). Сброс выбора (отмена выбора) основан на предположении, что парные объекты всегда имеют положительные идентификаторы.

 onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }

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

Я хотел бы сформулировать логику оценки с помощью операторов RxJS (см. следующий раздел). Для подготовки я создаю тематический поток assignmentStream в Matching-game.component.ts . Он должен излучать элементы, выбранные пользователем, слева или справа. Цель состоит в том, чтобы использовать операторы RxJS для изменения и разделения потока таким образом, чтобы я получил два новых потока: один solvedStream , который предоставляет правильно назначенные пары, и второй поток failedStream , который предоставляет неправильные назначения. Я хотел бы подписаться на эти два потока с помощью subscribe , чтобы иметь возможность выполнять соответствующую обработку событий в каждом случае.

Также мне нужна ссылка на созданные объекты подписки, чтобы я мог отменить подписки с помощью «отписаться» при выходе из игры (см. ngOnDestroy ). Классы Subject и Subscription должны быть импортированы из пакета «rxjs».

 private assignmentStream = new Subject<{pair:Pair, side:string}>(); private solvedStream = new Observable<Pair>(); private failedStream = new Observable<string>(); private s_Subscription: Subscription; private f_Subscription: Subscription; ngOnInit(){ ... //TODO: apply stream-operators on //assignmentStream this.s_Subscription = this.solvedStream.subscribe(pair => handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(() => handleFailedAssignment()); } ngOnDestroy() { this.s_Subscription.unsubscribe(); this.f_Subscription.unsubscribe(); }

Если задание выполнено правильно, выполняются следующие действия:

  • Назначенная пара перемещается в контейнер для решенных пар.
  • События leftpartUnselected и rightpartUnselected отправляются родительскому компоненту.

Ни одна пара не перемещается, если присвоение неверно. Если слева направо было выполнено неправильное присваивание ( side1 имеет значение left ), выделение следует отменить для элемента с левой стороны (см. GIF в начале статьи). Если присваивание выполняется справа налево, выбор отменяется для элемента с правой стороны. Это означает, что последний элемент, по которому был сделан щелчок, остается в выбранном состоянии.

Для обоих случаев я готовлю соответствующие функции-обработчики handleSolvedAssignment и handleFailedAssignment (функция удаления: см. исходный код в конце этой статьи):

 private handleSolvedAssignment(pair: Pair):void{ this.solvedPairs.push(pair); this.remove(this.unsolvedPairs, pair); this.leftpartUnselected.emit(); this.rightpartUnselected.emit(); //workaround to force update of the shuffle pipe this.test = Math.random() * 10; } private handleFailedAssignment(side1: string):void{ if(side1=="left"){ this.leftpartUnselected.emit(); }else{ this.rightpartUnselected.emit(); } }

Теперь нам нужно изменить точку зрения с потребителя, который подписывается на данные, на производителя, который генерирует данные. В файле Matching-game.component.html я удостоверяюсь, что при нажатии на элемент связанный с ним парный объект помещается в assignmentStream . Имеет смысл использовать общий поток для левой и правой стороны, потому что порядок присваивания для нас не важен.

 <div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'left'})"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'right'})">

Дизайн игрового взаимодействия с операторами RxJS

Остается только преобразовать assignmentStream в solvedStream и failedStream . Я последовательно применяю следующие операторы:

pairwise

В задании всегда две пары. pairwise оператор выбирает данные парами из потока. Текущее значение и предыдущее значение объединяются в пару.

Из следующего потока…

 „{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“

…результаты этого нового потока:

 „({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“

Например, мы получаем комбинацию ({pair1, left}, {pair3, right}) когда пользователь выбирает dog (id=1) слева и insect (id=3) справа (см. массив ANIMALS в начало статьи). Эти и другие комбинации являются результатом игровой последовательности, показанной на GIF-файле выше.

filter

Вы должны удалить из потока все комбинации, которые были составлены на одной стороне игрового поля, например ({pair1, left}, {pair1, left}) или ({pair1, left}, {pair4, left}) .

Таким образом, условием фильтрации для комбинации comb является comb[0].side != comb[1].side .

partition

Этот оператор берет поток и условие и создает из них два потока. Первый поток содержит данные, соответствующие условию, а второй поток содержит оставшиеся данные. В нашем случае потоки должны содержать правильные или неправильные назначения. Таким образом, условием для комбинированного comb является comb[0].pair===comb[1].pair .

Пример приводит к «правильному» потоку с

 ({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})

и «неправильный» поток с

 ({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})

map

Для дальнейшей обработки правильного присваивания, например, pair2 , требуется только отдельный объект пары. Оператор map может использоваться для выражения того, что комбинация comb должна быть сопоставлена ​​с comb[0].pair . Если присвоение неверно, комбинация comb сопоставляется со строкой comb[0].side потому что выбор должен быть сброшен на стороне, указанной side .

Функция pipe используется для объединения вышеуказанных операторов. Операторы pairwise , filter , partition , map должны быть импортированы из пакета rxjs/operators .

 ngOnInit() { ... const stream = this.assignmentStream.pipe( pairwise(), filter(comb => comb[0].side != comb[1].side) ); //pipe notation leads to an error message (Angular 8.2.2, RxJS 6.4.0) const [stream1, stream2] = partition(comb => comb[0].pair === comb[1].pair)(stream); this.solvedStream = stream1.pipe( map(comb => comb[0].pair) ); this.failedStream = stream2.pipe( map(comb => comb[0].side) ); this.s_Subscription = this.solvedStream.subscribe(pair => this.handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(side => this.handleFailedAssignment(side)); }

Теперь игра уже работает!

Скриншот обучающей игры «Собери пары»
Конечный результат

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

Заключение

На примере простой обучающей игры мы протестировали использование RxJS в компоненте Angular. Реактивный подход хорошо подходит для обработки событий, происходящих в пользовательском интерфейсе. С RxJS данные, необходимые для обработки событий, можно удобно организовать в виде потоков. Для преобразования потоков доступны многочисленные операторы, такие как filter , map или partition . Результирующие потоки содержат данные, подготовленные в окончательном виде, и на них можно подписаться напрямую. Требуется небольшой навык и опыт, чтобы выбрать подходящих операторов для соответствующего случая и эффективно связать их. Эта статья должна дать введение в это.

Дополнительные ресурсы

  • «Введение в реактивное программирование, которое вы пропустили», написанное Андре Стальцем.

Связанное Чтение на SmashingMag:

  • Управление точками останова изображения с помощью Angular
  • Стилизация приложения Angular с помощью Bootstrap
  • Как создать и развернуть приложение Angular Material