Как создать игру на сопоставление карт с помощью Angular и RxJS
Опубликовано: 2022-03-10Сегодня я хотел бы сосредоточиться на потоках данных, возникающих в результате кликов в пользовательском интерфейсе. Обработка таких потоков кликов особенно полезна для приложений с интенсивным взаимодействием с пользователем, где необходимо обрабатывать множество событий. Я также хотел бы немного больше познакомить вас с 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
) в качестве входных данных. Вход представляет собой массив «парных» объектов. Пользовательский интерфейс игры должен быть инициализирован переданными объектами при запуске приложения.
Для этого нам нужно действовать следующим образом:
- Добавьте
pairs
свойств в игровой компонент с помощью декоратора@Input
. - Добавьте
solvedPairs
иunsolvedPairs
как дополнительные приватные свойства компонента. (Необходимо различать уже «решенные» и «еще не решенные» пары.) - Когда приложение запускается (см. функцию
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 при запуске приложения появляется вторая версия игры:
Возможности дизайна теперь практически безграничны. Например, можно было бы определить подкласс Pair
, содержащий дополнительные свойства. Например, адреса изображения могут быть сохранены для левой и/или правой частей. Изображения могут отображаться в шаблоне вместе с текстом или вместо текста.
2. Контроль взаимодействия пользователя с RxJS
Преимущества реактивного программирования с RxJS
Чтобы превратить приложение в интерактивную игру, необходимо обработать события (например, события щелчка мышью), которые запускаются в пользовательском интерфейсе. В реактивном программировании рассматриваются непрерывные последовательности событий, так называемые «потоки». Поток можно наблюдать (это «наблюдаемый»), т.е. может быть один или несколько «наблюдателей» или «подписчиков», подписанных на поток. Они уведомляются (обычно асинхронно) о каждом новом значении в потоке и могут реагировать на него определенным образом.
При таком подходе может быть достигнут низкий уровень связи между частями приложения. Существующие наблюдатели и наблюдаемые независимы друг от друга, и их связь может изменяться во время выполнения.
Библиотека JavaScript RxJS обеспечивает зрелую реализацию шаблона проектирования Observer. Кроме того, RxJS содержит множество операторов для преобразования потоков (например, filter, map) или их объединения в новые потоки (например, merge, concat). Операторы являются «чистыми функциями» в смысле функционального программирования: они не производят побочных эффектов и не зависят от состояния вне функции. Логика программы, состоящая только из вызовов чистых функций, не нуждается в глобальных или локальных вспомогательных переменных для хранения промежуточных состояний. Это, в свою очередь, способствует созданию не имеющих состояния и слабо связанных блоков кода. Поэтому желательно реализовать большую часть обработки событий с помощью продуманной комбинации операторов потока. Примеры этого приведены в разделе после следующего, основанном на нашей игре на соответствие.
Интеграция RxJS в обработку событий компонента Angular
Фреймворк Angular работает с классами библиотеки RxJS. Таким образом, RxJS автоматически устанавливается при установке Angular.
На изображении ниже показаны основные классы и функции, которые играют роль в наших рассуждениях:
Имя класса | Функция |
---|---|
Наблюдаемый (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