AngularInDepth уходит от Medium. Эта статья, ее обновления и более свежие статьи размещены на новой платформе inDepth.dev

Государственное управление и модель потока - горячие темы в веб-разработке сегодня. Все ведущие фреймворки имеют вариации с React с использованием Redux, Angular с использованием NgRx, VueJS с использованием Vuex и другими. В частности, NgRx стал очень популярным в сообществе Angular. Если вы в настоящее время подумываете о добавлении NgRx в свой проект или просто хотите научиться, эта статья расскажет об основах паттерна потока и о том, как его использовать. Я собираюсь провести пошаговое руководство по изменению приложения Angular, не использующего служебные вызовы к NgRx.

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

В своих примерах я также буду использовать функции создания NgRx, которые были выпущены с версией 8. Они великолепны, потому что они значительно сокращают шаблонный код, который традиционно использовался в реализациях NgRx. Для более подробного представления о том, как работают функции создания, я настоятельно рекомендую статью Тима Дешривера« NgRx Creator Functions 101 здесь».

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

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

Проэкт

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

Проект to-do-with-ngrx просто использует localStorage вашего браузера для создания, редактирования и удаления набора to-do элементов. Приложение очень простое и имеет всего один компонент с одной службой to-do.

«Задачи» выглядят следующим образом:

import { Injectable } from '@angular/core';
import { Item } from '../models/item';
@Injectable({
  providedIn: 'root'
})
export class ToDoService {
  constructor() {}
getItems() {
    let items = JSON.parse(window.localStorage.getItem('items'));
    if (items === null) {
      items = [];
    }
    return items;
  }
addItem(addItem: string) {
    const itemsStored = window.localStorage.getItem('items');
    let items = [];
    if (itemsStored !== null) {
      items = JSON.parse(itemsStored);
    }
    const item: Item = {
      id: items.length + 1,
      name: addItem
    };
    items.push(item);
    window.localStorage.setItem('items', JSON.stringify(items));
  }
deleteItem(deleteItem) {
    const items = JSON.parse(window.localStorage.getItem('items'));
    console.log(items);
    console.log(deleteItem);
    const saved = items.filter(item => {
      return item.id !== deleteItem.id;
    });
    window.localStorage.setItem('items', JSON.stringify(saved));
  }
}

Как видите, методы здесь просто используют localStorage браузера для сохранения «дел». Без NgRx приложение использует эти методы через прямые вызовы в компоненте. Мы собираемся изменить приложение, чтобы оно использовало те же методы обслуживания с помощью действий, редукторов, селекторов и эффектов . Это упростит обслуживание приложения и позволит вам более легко управлять его состоянием.

Предварительный просмотр NgRx

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

Я позаимствовал эту диаграмму из статьи Как начать летать с Angular и NgRx для обсуждения.

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

Это также помогает понять паттерн потока, если вы рассматриваете изменение в терминах изменяемого и неизменного.

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

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

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

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

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

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

Установка NgRx и формирование приложения

Итак, как я сказал во вступлении, я собираюсь пошагово добавить NgRx в приложение Angular. NgRx - это версия Redux для Angular, пользующаяся большой поддержкой сообщества.

Для начала нам нужно сделать git clone моего пробного проекта. Я сохранил версию проекта, в которой нет NgRx, в ветке before-ngrx. git clone этой ветки можно сделать следующим образом:

git clone --branch before-ngrx https://github.com/andrewevans0102/to-do-with-ngrx.git

Вы также можете перейти к полной версии приложения, выполнив git clone --branch after-ngrx« https://github.com/andrewevans0102/to-do-with-ngrx.git »

После клонирования проекта откройте каталог в своем терминале и выполните стандартный npm install, а затем запустите npm run serve, чтобы запустить его локально.

Чтобы добавить NgRx в этот проект, мы собираемся установить следующее:

Для их установки сделайте следующее:

npm install @ngrx/store @ngrx/effects @ngrx/store-devtools --save

Теперь, когда они установлены, в папке src/app создайте следующие файлы:

  • ToDoActions.ts
  • ToDoEffects.ts
  • ToDoReducers.ts

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

Затем подключите это к файлу app.module, изменив его так, как вы видите здесь:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
import { EffectsModule } from '@ngrx/effects';
import { ToDoEffect } from './ToDoEffects';
import { StoreModule } from '@ngrx/store';
import { ToDoReducer } from './ToDoReducers';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    StoreModule.forRoot({ toDo: ToDoReducer }),
    EffectsModule.forRoot([ToDoEffect]),
    StoreDevtoolsModule.instrument({
      maxAge: 25,
      logOnly: environment.production
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Если вы заметили, в частности, следующее:

StoreModule.forRoot({ toDo: ToDoReducer }),
EffectsModule.forRoot([ToDoEffect]),
StoreDevtoolsModule.instrument({
  maxAge: 25,
  logOnly: environment.production
})

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

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

Действия

Итак, в нашем приложении у нас есть основные потребности, которые включают:

  • Загрузка элементов для страницы
  • Добавление дела
  • Удаление задачи
  • возврат ошибки при ее возникновении

Скопируйте и вставьте в файл ToDoActions.ts следующее:

import { createAction, props } from '@ngrx/store';
import { Item } from './models/item';
export const getItems = createAction('[to-do] get items');
export const loadItems = createAction(
  '[to-do] load items',
  props<{ items: Item[] }>()
);
export const addItem = createAction(
  '[to-do] add item',
  props<{ name: string }>()
);
export const deleteItem = createAction(
  '[to-do] delete item',
  props<{ item: Item }>()
);
export const errorItem = createAction(
  '[to-do] error item',
  props<{ message: string }>()
);

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

Как я уже упоминал во вступлении, для более подробного изучения этого синтаксиса я настоятельно рекомендую Здесь можно прочитать сообщение Тима Дешрайвера« NgRx Creator Functions 101 ».

Редукторы

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

Скопируйте и вставьте в файл ToDoReducers.ts следующее:

import { loadItems, errorItem } from './ToDoActions';
import { on, createReducer } from '@ngrx/store';
import { Item } from './models/item';
export interface State {
  toDo: { items: Item[]; error: string };
}
export const initialState: State = {
  toDo: { items: [], error: '' }
};
export const ToDoReducer = createReducer(
  initialState,
  on(loadItems, (state, action) => ({
    ...state,
    items: action.items
  })),
  on(errorItem, (state, action) => ({
    ...state,
    error: action.message
  }))
);
export const selectItems = (state: State) => state.toDo.items;
export const selectError = (state: State) => state.toDo.error;

Итак, здесь мы делаем несколько вещей. Основная цель редукторов - настроить поведение при работе с состоянием.

Мы определяем глобальный объект состояния с помощью:

export interface State {
  toDo: { items: Item[]; error: string };
}
export const initialState: State = {
  toDo: { items: [], error: '' }
};

Затем мы определяем редукторы для действий [to-do] load items и [to-do] error item с помощью:

export const ToDoReducer = createReducer(
  initialState,
  on(loadItems, (state, action) => ({
    ...state,
    items: action.items
  })),
  on(errorItem, (state, action) => ({
    ...state,
    error: action.message
  }))
);

Затем в последнем разделе мы определяем selectors, который позволит нам получить фрагмент состояния с помощью:

export const selectItems = (state: State) => state.toDo.items;
export const selectError = (state: State) => state.toDo.error;

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

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

Эффекты

Определив действия и редукторы, нам нужно определить события, которые происходят в результате действий. В NgRx (и redux) это называется effects.

Скопируйте и вставьте в ToDoEffects.ts следующее:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { addItem, getItems, deleteItem } from './ToDoActions';
import { switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { ToDoService } from './services/to-do.service';
@Injectable()
export class ToDoEffect {
  loadItems$ = createEffect(() =>
    this.actions$.pipe(
      ofType(getItems),
      switchMap(action => {
        const itemsLoaded = this.toDoService.getItems();
        return of({ 
          type: '[to-do] load items', items: itemsLoaded 
        });
      }),
      catchError(error => of({ 
        type: '[to-do] error item', message: error 
      }))
    )
  );
addItem$ = createEffect(() =>
    this.actions$.pipe(
      ofType(addItem),
      switchMap(action => {
        this.toDoService.addItem(action.name);
        const itemsLoaded = this.toDoService.getItems();
        return of({ 
          type: '[to-do] load items', items: itemsLoaded 
        });
      }),
      catchError(error => of({ 
        type: '[to-do] error item', message: error 
      }))
    )
  );
deleteItem$ = createEffect(() =>
    this.actions$.pipe(
      ofType(deleteItem),
      switchMap(action => {
        this.toDoService.deleteItem(action.item);
        const itemsLoaded = this.toDoService.getItems();
        return of({ 
          type: '[to-do] load items', items: itemsLoaded 
        });
      }),
      catchError(error => of({ 
        type: '[to-do] error item', message: error 
      }))
    )
  );
constructor(
    private actions$: Actions, 
    private toDoService: ToDoService
  ) {}
}

Если вы заметили, мы только что создали эффекты для:

  • loadItems$ = createEffect(() = вызывает «службу дел» для получения элементов
  • addItem$ = createEffect(() = вызывает «службу дел» для добавления элемента, затем вызывает службу «дел» для извлечения элементов (в конечном итоге обновляет состояние)
  • deleteItem$ = createEffect(() = вызывает «службу дел» для удаления элемента, затем вызывает службу «дел» для извлечения элементов (в конечном итоге обновляет состояние)

Как я уже упоминал в разделе обзора, это считается «побочными эффектами» NgRx. Вы можете увидеть это по действию [to-do] add item. Когда это действие отправляется (или запускается), связанный эффект вызывает службу «to-do», а затем генерирует связанные изменения состояния с возвращением [to-do] load items, как вы видите здесь:

addItem$ = createEffect(() =>
  this.actions$.pipe(
    ofType(addItem),
    switchMap(action => {
      this.toDoService.addItem(action.name);
      const itemsLoaded = this.toDoService.getItems();
      return of({
        type: '[to-do] load items',
        items: itemsLoaded
      });
    }),
    catchError(error =>
      of({
        type: '[to-do] error item',
        message: error
      })
    )
  )
);

Возвращаемое [to-do] load items затем вызывает «ToDoReducer» с on(loadItems…, и вычисляется новое состояние. Любые компоненты, которые подписаны на магазин, получают новое состояние с помощью селекторов.

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

Изменение компонента приложения

Итак, на этом этапе все части определены. Однако нам нужно будет изменить компонент приложения, чтобы использовать определенные нами действия и селекторы.

Измените src/application.component.ts, чтобы он выглядел так:

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { Item } from './models/item';
import { Store, select } from '@ngrx/store';
import { getItems, addItem, deleteItem } from './ToDoActions';
import { Observable } from 'rxjs';
import { selectItems, selectError } from './ToDoReducers';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  toDoForm = new FormGroup({
    name: new FormControl('')
  });
  items$: Observable<any>;
  error$: Observable<any>;
constructor(private store: Store<{ toDo: { items: Item[] } }>) {
    this.store.dispatch(getItems());
    this.items$ = this.store.pipe(select(selectItems));
    this.error$ = this.store.pipe(select(selectError));
  }
onSubmit() {
    this.store.dispatch(addItem({ name: this.toDoForm.controls.name.value }));
    this.toDoForm.controls.name.reset();
  }
deleteItem(deleted: Item) {
    this.store.dispatch(deleteItem({ item: deleted }));
  }
}

Обратите внимание на импорт NgRx:

import { Store, select } from '@ngrx/store';
import { getItems, addItem, deleteItem } from './ToDoActions';
import { Observable } from 'rxjs';
import { selectItems, selectError } from './ToDoReducers';

Это задействует определенные ранее действия и селекторы.

Обратите внимание также на наблюдаемые:

items$: Observable<any>;
error$: Observable<any>;

Они подпишутся на магазин, чтобы получать любые изменения состояния.

Также обратите внимание на конструктор:

constructor(private store: Store<{ toDo: { items: Item[] } }>) {
    this.store.dispatch(getItems());
    this.items$ = this.store.pipe(select(selectItems));
    this.error$ = this.store.pipe(select(selectError));
  }

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

Наконец, обратите внимание на изменения в onSubmit и deleteItem здесь:

onSubmit() {
  this.store.dispatch(addItem({ 
    name: this.toDoForm.controls.name.value }));
  this.toDoForm.controls.name.reset();
}
deleteItem(deleted: Item) {
  this.store.dispatch(deleteItem({ item: deleted }));
}

Оба этих метода раньше выполняли прямые вызовы служб, теперь они отправляют действия. В обоих случаях действия отправляются, затем «побочные эффекты» вызывают методы службы «to-do» и создают новое состояние в хранилище с запуском [to-do] load items действий.

После изменения компонента последний шаг - изменить шаблон HTML для получения обновлений из магазина:

<header class="c-header">
  <h1 class="c-header__title">To-Do list with NgRx</h1>
  <p class="c-header__subtitle">Learn how to use NgRx with a to-do list</p>
</header>
<section class="c-error" *ngIf="error$ | async as error">
  <h1>{{ error }}</h1>
</section>
<form class="c-form" [formGroup]="toDoForm" (ngSubmit)="onSubmit()">
  <input
    class="c-form__input"
    type="text"
    formControlName="name"
    placeholder="to-do item"
    required
  />
  <button class="c-form__button" type="submit" [disabled]="!toDoForm.valid">
    Create Item
  </button>
</form>
<section class="c-list" *ngIf="items$ | async as items">
  <ul *ngFor="let item of items">
    <li class="c-list__item">
      <button class="c-list__button" (click)="deleteItem(item)">X</button>
      <span class="c-list__item-label">{{ item.name }}</span>
    </li>
  </ul>
</section>

Сначала обратите внимание на раздел, который прослушивает значения из наблюдаемых элементов:

<section class="c-list" *ngIf="items$ | async as items">
  <ul *ngFor="let item of items">
    <li class="c-list__item">
      <button class="c-list__button" (click)="deleteItem(item)">X</button>
      <span class="c-list__item-label">{{ item.name }}</span>
    </li>
  </ul>
</section>

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

<section class="c-error" *ngIf="error$ | async as error">
  <h1>{{ error }}</h1>
</section>

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

Общий расход

Итак, когда все части определены, вы можете сделать npm run serve и увидеть приложение в действии. Потоки следуют аналогичным шаблонам, давайте посмотрим на поток «добавления элемента», который мы обсуждали в первых разделах.

Вместо вызова метода службы непосредственно в компоненте при добавлении элементов происходит следующее:

  1. Пользователь вводит текст для добавляемого элемента
  2. Пользователь нажимает кнопку «Создать элемент».
  3. Компонент отправляет [to-do] add item действие
  4. Возникает эффект: значение, переданное в [to-do] add item реквизитах, отправляется в localStorage через вызов «службы дел». эффект возвращает [to-do] load items действие.
  5. Когда действие [to-do] load items отправляется, оно создает новое состояние в магазине, и все это передается в шаблон через селекторы.

Все это демонстрирует различные части картины потока в действии. Чтобы увидеть это визуально, я также рекомендую установить Chrome’s Redux Devtools Extension, чтобы видеть различные происходящие события.

Если у вас возникли проблемы с его запуском, вы также можете перейти к полной версии приложения, выполнив git clone - branch after-ngrx« https://github.com/andrewevans0102/to-do-with- ngrx.git »

Заключение

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

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

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

Спасибо за прочтение! Подписывайтесь на меня в твиттере на @ AndrewEvans0102!