Учебное пособие по Ngrx и Angular 2: создание реактивного приложения

Опубликовано: 2022-03-11

Мы много говорим о реактивном программировании в сфере Angular. Реактивное программирование и Angular 2, кажется, идут рука об руку. Однако для тех, кто не знаком с обеими технологиями, может оказаться довольно сложной задачей понять, о чем идет речь.

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

Ngrx — это группа библиотек Angular для реактивных расширений. Ngrx/Store реализует шаблон Redux с использованием хорошо известных наблюдаемых объектов RxJS Angular 2. Он обеспечивает несколько преимуществ, упрощая состояние вашего приложения до простых объектов, обеспечивая однонаправленный поток данных и многое другое. Библиотека Ngrx/Effects позволяет приложению взаимодействовать с внешним миром, запуская побочные эффекты.

Что такое реактивное программирование?

Реактивное программирование — это термин, который вы часто слышите в наши дни, но что он на самом деле означает?

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

Как вы, возможно, знаете, отличным инструментом для реактивного программирования является RxJS.

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

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

Несколько слов о Ngrx

Чтобы понять приложение, которое вы создадите с помощью этого руководства, вы должны быстро погрузиться в основные концепции Redux.

Магазин

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

Это единственное, что вы изменяете, когда следуете шаблону Redux и модифицируете, отправляя ему действия.

Редуктор

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

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

Действия

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

Диспетчер

Диспетчеры — это просто точка входа для отправки вашего действия. В Ngrx есть метод отправки прямо в магазин.

ПО промежуточного слоя

Промежуточное ПО — это некоторые функции, которые будут перехватывать каждое отправляемое действие для создания побочных эффектов, даже если вы не будете использовать их в этой статье. Они реализованы в библиотеке Ngrx/Effect, и есть большая вероятность, что они понадобятся вам при создании реальных приложений.

Зачем использовать Ngrx?

Сложность

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

Инструменты

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

Архитектурная простота

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

Хотя вам не нужно думать о том, что вы делаете, добавление некоторых вещей, таких как аналитика, во все ваши приложения становится тривиальным, поскольку вы можете отслеживать все отправленные действия.

Небольшая кривая обучения

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

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

Создание приложения с помощью Ngrx

Сила Ngrx проявляется больше всего, когда у вас есть внешние данные, которые передаются в наше приложение в режиме реального времени. Имея это в виду, давайте создадим простую сетку фрилансеров, которая показывает онлайн-фрилансеров и позволяет вам фильтровать их.

Настройка проекта

Angular CLI — отличный инструмент, который значительно упрощает процесс настройки. Вы можете не использовать его, но имейте в виду, что он будет использоваться в остальной части этой статьи.

 npm install -g @angular/cli

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

 ng new toptal-freelancers npm install ngrx --save

Редуктор фрилансеров

Редьюсеры являются основной частью архитектуры Redux, так почему бы не начать с них в первую очередь при создании приложения?

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

фрилансер-сетка/freelancers.reducer.ts

 import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, thumbnail: string } export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); default: return state; } }

Итак, вот наш редуктор фрилансеров.

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

Здесь важно отметить, что если будет возвращена старая ссылка на состояние, то состояние будет считаться неизменным. Это означает, что если вы state.push(something) , состояние не будет считаться измененным. Помните об этом, выполняя функции редуктора.

Состояния неизменны. Новое состояние должно возвращаться каждый раз, когда оно изменяется.

Компонент сетки фрилансера

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

 ng generate component freelancer-grid

Поместите следующее в freelancer-grid.component.ts

 import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component.html', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; constructor(private store: Store<AppState>) { this.freelancers = store.select('freelancers'); } }

И следующее в freelancer-grid.component.html :

 <span class="count">Number of freelancers online: {{(freelancers | async).length}}</span> <div class="freelancer fade thumbail" *ngFor="let freelancer of freelancers | async"> <button type="button" class="close" aria-label="Close" (click)="delete(freelancer)"><span aria-hidden="true">&times;</span></button><br> <img class="img-circle center-block" src="{{freelancer.thumbnail}}" /><br> <div class="info"><span><strong>Name: </strong>{{freelancer.name}}</span> <span><strong>Email: </strong>{{freelancer.email}}</span></div> <a class="btn btn-default">Hire {{freelancer.name}}</a> </div>

Так что ты только что сделал?

Во-первых, вы создали новый компонент под названием freelancer-grid .

Компонент содержит свойство с именем freelancers , которое является частью состояния приложения, содержащегося в хранилище Ngrx. Используя оператор выбора, вы выбираете, чтобы свойство freelancers уведомляло вас только об общем состоянии приложения. Итак, теперь каждый раз, когда свойство freelancers состояния приложения изменяется, ваш наблюдаемый объект будет уведомлен.

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

В части шаблона вы не сделали ничего слишком сложного. Обратите внимание на использование асинхронного канала в *ngFor . Наблюдаемые freelancers не являются итерируемыми напрямую, но благодаря Angular у нас есть инструменты для его развертывания и привязки dom к его значению с помощью асинхронного канала. Это значительно упрощает работу с наблюдаемым.

Добавление функции удаления фрилансеров

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

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

В данном случае это только редуктор freelancers :

 export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }

Здесь действительно важно создать новый массив из старого, чтобы иметь новое неизменяемое состояние.

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

 delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }

Разве это не выглядит просто?

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

А что, если вы добавите в приложение еще один компонент, чтобы увидеть, как они могут взаимодействовать друг с другом через магазин?

Редуктор фильтра

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

 import { Action } from '@ngrx/store'; export interface IFilter { name: string, email: string, } export const ACTIONS = { UPDATE_FITLER: 'UPDATE_FITLER', CLEAR_FITLER: 'CLEAR_FITLER', } const initialState = { name: '', email: '' }; export function filterReducer( state: IFilter = initialState, action: Action): IFilter { switch (action.type) { case ACTIONS.UPDATE_FITLER: // Create a new state from payload return Object.assign({}, action.payload); case ACTIONS.CLEAR_FITLER: // Create a new state from initial state return Object.assign({}, initialState); default: return state; } }

Компонент фильтра

 import { Component, OnInit } from '@angular/core'; import { IFilter, ACTIONS as FilterACTIONS } from './filter-reducer'; import { Store } from '@ngrx/store'; import { FormGroup, FormControl } from '@angular/forms'; import * as Rx from 'RxJS'; @Component({ selector: 'app-filter', template: '<form class="filter">'+ '<label>Name</label>'+ '<input type="text" [formControl]="name" name="name"/>'+ '<label>Email</label>'+ '<input type="text" [formControl]="email" name="email"/>'+ '<a (click)="clearFilter()" class="btn btn-default">Clear Filter</a>'+ '</form>', styleUrls: ['./filter.component.scss'], }) export class FilterComponent implements OnInit { public name = new FormControl(); public email = new FormControl(); constructor(private store: Store<any>) { store.select('filter').subscribe((filter: IFilter) => { this.name.setValue(filter.name); this.email.setValue(filter.email); }) Rx.Observable.merge(this.name.valueChanges, this.email.valueChanges).debounceTime(1000).subscribe(() => this.filter()); } ngOnInit() { } filter() { this.store.dispatch({ type: FilterACTIONS.UPDATE_FITLER, payload: { name: this.name.value, email: this.email.value, } }); } clearFilter() { this.store.dispatch({ type: FilterACTIONS.CLEAR_FITLER, }) } }

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

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

Что хорошо в Angular 2, так это то, что он предоставляет вам множество инструментов для взаимодействия с наблюдаемыми.

Вы видели асинхронный канал ранее, а теперь вы видите класс formControl , который позволяет вам иметь наблюдаемое значение на входе. Это позволяет причудливые вещи, подобные тому, что вы сделали в компоненте фильтра.

Как вы можете видеть, вы используете Rx.observable.merge для объединения двух наблюдаемых, предоставленных вашим formControls , а затем вы отменяете этот новый наблюдаемый перед запуском функции filter .

Проще говоря, вы ждете одну секунду после изменения имени или адреса электронной почты formControl , а затем вызываете функцию filter .

Разве это не потрясающе?

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

Теперь давайте перейдем к этой функции фильтра. Что оно делает?

Он просто отправляет действие UPDATE_FILTER со значением имени и электронной почты, а редьюсер заботится об изменении состояния с этой информацией.

Давайте перейдем к чему-то более интересному.

Как заставить этот фильтр взаимодействовать с ранее созданной сеткой фрилансеров?

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

 import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import { IFilter, ACTIONS as FilterACTIONS } from './../filter/filter-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; public filter: Rx.Observable<IFilter>; constructor(private store: Store<AppState>) { this.freelancers = Rx.Observable.combineLatest(store.select('freelancers'), store.select('filter'), this.applyFilter); } applyFilter(freelancers: Array<IFreelancer>, filter: IFilter): Array<IFreelancer> { return freelancers .filter(x => !filter.name || x.name.toLowerCase().indexOf(filter.name.toLowerCase()) !== -1) .filter(x => !filter.email || x.email.toLowerCase().indexOf(filter.email.toLowerCase()) !== -1) } ngOnInit() { } delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) } }

Это не сложнее, чем это.

Вы снова использовали мощь RxJS, чтобы объединить фильтр и состояние фрилансера.

На самом деле, combineLatest сработает, если сработает одна из двух наблюдаемых, а затем объединит каждое состояние с помощью функции applyFilter . Он возвращает новую наблюдаемую, которая делает это. Нам не нужно изменять какие-либо другие строки кода.

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

Заставляя это сиять

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

Представляем freelancers-service .

 ng generate service freelancer

Сервис фрилансера будет имитировать работу с данными в реальном времени и должен выглядеть следующим образом.

 import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-grid/freelancer-reducer'; import { Http, Response } from '@angular/http'; @Injectable() export class RealtimeFreelancersService { private USER_API_URL = 'https://randomuser.me/api/?results=' constructor(private store: Store<AppState>, private http: Http) { } private toFreelancer(value: any) { return { name: value.name.first + ' ' + value.name.last, email: value.email, thumbail: value.picture.large, } } private random(y) { return Math.floor(Math.random() * y); } public run() { this.http.get(`${this.USER_API_URL}51`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.FREELANCERS_LOADED, payload: response.json().results.map(this.toFreelancer) }) }) setInterval(() => { this.store.select('freelancers').first().subscribe((freelancers: Array<IFreelancer>) => { let getDeletedIndex = () => { return this.random(freelancers.length - 1) } this.http.get(`${this.USER_API_URL}${this.random(10)}`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.INCOMMING_DATA, payload: { ADD: response.json().results.map(this.toFreelancer), DELETE: new Array(this.random(6)).fill(0).map(() => getDeletedIndex()), } }); this.addFadeClassToNewElements(); }); }); }, 10000); } private addFadeClassToNewElements() { let elements = window.document.getElementsByClassName('freelancer'); for (let i = 0; i < elements.length; i++) { if (elements.item(i).className.indexOf('fade') === -1) { elements.item(i).classList.add('fade'); } } } }

Этот сервис не идеален, но он делает то, что делает, и в демонстрационных целях позволяет нам продемонстрировать несколько вещей.

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

Если мы хотим, чтобы наш редьюсер мог с этим справиться, нам нужно изменить его:

 import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, } export const ACTIONS = { LOAD_FREELANCERS: 'LOAD_FREELANCERS', INCOMMING_DATA: 'INCOMMING_DATA', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.INCOMMING_DATA: action.payload.DELETE.forEach((index) => { state.splice(state.indexOf(action.payload), 1); }) return Array.prototype.concat(action.payload.ADD, state); case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }

Теперь мы можем обрабатывать такие операции.

Одна вещь, которая демонстрируется в этом сервисе, заключается в том, что из всего процесса изменения состояния, выполняемого синхронно, очень важно заметить это. Если применение состояния было асинхронным, вызов this.addFadeClassToNewElements(); не будет работать, поскольку элемент DOM не будет создан при вызове этой функции.

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

Создание приложений реактивным способом

С помощью этого руководства вы создали реактивное приложение с использованием Ngrx, RxJS и Angular 2.

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

Реактивная парадигма

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

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

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

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

Связанный: Все льготы, никаких хлопот: учебник по Angular 9