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

Мы будем реализовывать решение по статье Юре Байта.

Для реализации этого решения необходимо добавить в наш проект следующий пакет: rxjs-observable-store

Готовый проект можно найти здесь:



Наш пример магазина позволит нам добавлять и удалять людей из комнат:

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

import {Observable, BehaviorSubject} from 'rxjs';
export class Store<T> {
    state$: Observable<T>;
    private _state$: BehaviorSubject<T>;
protected constructor (initialState: T) {
        this._state$ = new BehaviorSubject(initialState);
        this.state$ = this._state$.asObservable();
    }
get state (): T {
        return this._state$.getValue();
    }
setState (nextState: T): void {
        this._state$.next(nextState);
    }
}

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

Создание магазина

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

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

export interface Employee {
    title: string;
    name: string;
}

Чтобы в нашем приложении было хранилище, мы расширим класс хранилища типом OfficeState из нашего файла состояния.

@Injectable({providedIn: 'root'})
export class OfficeStore extends Store<OfficeState> {
    constructor(){
      super(new OfficeState())
    }
}

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

@Injectable({providedIn: 'root'})
export class OfficeStore extends Store<OfficeState> {
    constructor(){
      super(new OfficeState())
    }

    addEmployee(person: Employee, location): void {
      this.setState({
        ...this.state,
        [location]: [...this.state[location], person]
      });
    }

    removeEmployee(person: Employee, location): void {
      this.setState({
        ...this.state,
        [location]: this.state[location].filter(item => item.name !== person.name)
      });
    }
}

Как видите, у нас есть действие для _3 _, _ 4_, и каждое из этих действий выполняет только одну задачу.

  • Наша addEmployee task только добавляет сотрудника к указанному свойству (которое мы указываем динамически) состояния.
  • Наша removeEmployee задача только удаляет сотрудника в указанное состояние

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

Подписка на наш Магазин

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

Мы можем использовать довольно интересную функцию Angular - асинхронный конвейер. По сути, это позволяет нам подписаться на наблюдаемое состояние и иметь доступ к результату нашего наблюдаемого. Но самое замечательное в асинхронном конвейере - это то, что нам не нужно беспокоиться о том, чтобы отписаться от нашей наблюдаемой. Кроме того, мы также используем синтаксис Angular «as» для установки результата нашего наблюдаемого в локальную переменную, к которой мы можем получить доступ в нашей разметке.

Наш компонентный файл будет обрабатывать обновление нашего состояния:

removeEmployee(employee) {    
  this.store.removeEmployee(employee, this.location)
  this.store.addEmployee(employee, 'employees');  
}

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

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

export class AddPersonComponent implements OnInit {

  @Input() location: string;
  showList = false;
  // ngUnsubscribe = new Subject();
  // employees: Employee[];

  constructor(public store: OfficeStore) { }

  // for cleaning up subscriptions
  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

  ngOnInit() {
    // subscription to the store
    this.store.state$
      .pipe(
        takeUntil(this.ngUnsubscribe),
        map(state => state[this.location]))
      .subscribe(data => {
        this.employees = data;
      })
  }

  addEmployee(employee): void {
    this.store.addEmployee(employee, this.location);
    this.store.removeEmployee(employee, 'employees');
  }
}

В этом случае мы внедряем наш магазин и подписываемся на наблюдаемый объект state$. Мы также убеждаемся, что отказываемся от подписки на эту наблюдаемую через takeUntil и subject , которую мы уничтожаем при удалении компонента. С нашим state у нас также есть возможность отображать, какие свойства состояния мы наблюдаем. Чтобы увидеть несколько значений, мы могли бы использовать map следующим образом:

this.store.state$
      .pipe(
        takeUntil(this.ngUnsubscribe),
        map(state => ({employees: state.employees, bossOffice: state.bossOffice})))
      .subscribe(data => {
        this.employees = data.employees;
      })

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

Выводы

Итак, где мы разместим наши магазины в нашем приложении? Что ж, мы можем поместить их в следующее:

app
  -core
    -store-name (would place here if global store)
  -feature
    -store-name (would place here for a feature specific store, store should only be injected within this feature)

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