Управление состояниями в Angular с использованием Akita
В этой статье мы расскажем о концепции управления состоянием и о том, как Akita помогает нам упростить поток данных в приложении, управляя им.
Конечный автомат - это любое устройство, которое хранит состояние чего-либо в определенный момент времени и может работать на входе, чтобы изменять состояние и / или вызывать действие или выход для любого данного изменения. - State Machine
Проще говоря, он хранит состояние любой формы данных и управляет переходом состояния на основе текущего состояния и действия.
Почему управление состояниями важно?
Веб-приложения становятся все богаче и сложнее. Управлять состоянием экспоненциально сложнее, чем раньше.
Различные части приложения имеют разные обязанности, и эти компоненты (компоненты, директивы и т.д.) Разделены по многим различным файлам, но все они должны отражать одно и то же основное состояние.
Библиотека управления состоянием дает вам удобный способ:
- Смоделировать состояние вашего приложения
- Получить вычисленные значения
- Следить за изменениями
Это дает нам много преимуществ, таких как обработка нормализованных данных (можно избежать избыточных моделей), неизменность и организованный переход состояний, способность путешествовать во времени и т.д.
Введение в Акиту
Akita - это шаблон управления состоянием, построенный на основе RxJS и основанный на принципах объектно-ориентированного проектирования.
Акита поощряет простоту. Это избавляет вас от необходимости создавать шаблонный код и предлагает мощные инструменты с умеренной кривой обучения, подходящие как для опытных, так и для неопытных разработчиков.
Прежде чем мы углубимся, давайте рассмотрим строительные блоки Акиты:
Model
Общее представление модели данных, которой будет управлять магазин.
Store
Store - это как склад, где будет храниться модель данных. Вы можете выполнять все DML (Data Manipulation Language) с помощью встроенных методов add()
, update()
, delete()
, setActive()
, setError()
и т.д.
Query
Просто запросы к базе данных, которые помогают запрашивать store. Результаты запроса доступны в двух формах - реактивные запросы и синхронизация запросов.
Вы можете запустить все (Definition Language Data) DDL с помощью встроенных методов запросов как select()
, selectAll()
, selectEntity()
, selectCount()
, selectActive()
и т.д.
Два типа хранилищ
Базовое хранилище - когда наша модель не представляет коллекцию сущностей, например, сессию, состояния пользовательского интерфейса и т.д.
Хранилище сущностей - когда нам нужно поддерживать набор сущностей, например, модель домена, сотрудника, студента и т.д.
Давайте сделаем простое приложение
Мы будем строить дашборд панель успеваемости студентов с помощью Akita и узнаем как легко создать управляемые хранилища данных.
Структура папок
Вот как Акита рекомендует структурировать проект ради поддержания модульности и порядка.
Дополнительная служба данных рекомендуется только для корпоративных приложений.
Обратите внимание, что Akita предлагает инструмент или схемы CLI, позволяющие быстро создавать хранилища на основе спецификаций.
Хранилще сессий
Чтобы поддерживать активного пользователя, нам нужно создать таблицу session
, т.е. хранилище.
Обычно модель данных и функции создания фабрики являются частью объявления файла модели (например, session.model.ts), так как мы используем базовое хранилище, нам не нужен отдельный файл модели, мы сохраняем нашу модель вместе с файлом хранилища.
// session.store.ts import { ID, Store, StoreConfig } from '@datorama/akita'; export interface User { firstName: string; lastName: string; token: string; } export interface SessionState { user: User | null; } export function createInitialState(): SessionState { return { user: null }; } export function createSession(user: User) { return { ...user }; } @StoreConfig({ name: 'session' }) export class SessionStore extends Store{ constructor() { super(createInitialState()); } login(data: User) { const user = createSession(data); this.update({ user }); } logout() { this.update(createInitialState()); } }
Как упоминалось ранее, вы можете увидеть некоторые из операций DML, которые выполняются как часть хранилища.
Давайте создадим запрос сеанса:
// session.query.ts import { Query, toBoolean } from '@datorama/akita'; import { filter, map } from 'rxjs/operators'; import { SessionStore, SessionState } from './session.store'; export class SessionQuery extends Query{ isLoggedIn$ = this.select(({ user }) => toBoolean(user)); loggedInUser$ = this.select().pipe( filter(({ user }) => toBoolean(user)), map(({ user: { firstName: f, lastName: l } }) => `${f} ${l}`) ); constructor(protected store: SessionStore) { super(store); } isLoggedIn() { return toBoolean(this.getSnapshot().user); } }
Эти методы select()
выбирают кусочек из стора. Мы можем видеть, как мы можем инкапсулировать более сложные запросы внутри класса Query
, оставляя наш компонент не осведомленным об источнике данных.
Класс Query
обрабатывает операции DDL. В приведенном выше коде мы можем увидеть пример реактивного запроса isLoggedIn$
и синхронизировать запрос isLoggedIn()
.
Давайте представим текущее состояние с помощью инструментов разработчика Акиты:
Хранилище списка студентов
Поскольку модель Student
является доменной, мы знаем, что нам нужно поддерживать набор сущностей, поэтому выбор хранилища здесь - EntityStore .
// student.model.ts import { ID, guid } from '@datorama/akita'; export interface Student { id: ID; name: string; sex: 'Male' | 'Female'; standard: number; quarterlyScore: number; halfyearlyScore: number; annualScore: number; } export function createStudent({ name = '', standard = null, sex = null, quarterlyScore = 0, halfyearlyScore = 0, annualScore = 0 }: Partial): Student { return { id: guid(), name, sex, standard, quarterlyScore, halfyearlyScore, annualScore }; }
Мы начнем с определения интерфейса Student
и фабричной функции, которая знает, как создать студента. Мы используем метод guid()
для генерации уникального идентификатора.
// student.store.ts import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; import { Student } from './student.model'; export interface StudentState extends EntityState{ } @StoreConfig({ name: 'students' }) export class StudentStore extends EntityStore { constructor() { super(); } }
Для создания EntityStore
нам нужно определить интерфейс хранилища. В нашем случае мы можем обойтись без расширения от Акиты EntityState
, предоставив ему тип сущности Student
.
Давайте создадим запрос студента:
// student.query.ts import { QueryEntity } from '@datorama/akita'; export class StudentQuery extends QueryEntity{ studentsGraphData$ = this.selectAll().pipe( map(this.getStudentGraphData.bind(this)) ); constructor(protected store: StudentStore) { super(store); } getStudentGraphData(students: Array ): { [key: string]: Array } { return students.reduce(( { names: nArray, quarterly: qArray, halfyearly: hArray, annual: aArray }, { name, quarterlyScore, halfyearlyScore, annualScore }) => { return { names: [...nArray, name], quarterly: [...qArray, quarterlyScore], halfyearly: [...hArray, halfyearlyScore], annual: [...aArray, annualScore] }; }, { names: [], quarterly: [], halfyearly: [], annual: [] }); } }
Давайте создадим сервис для студентов:
// student.service.ts import { noop, ID } from '@datorama/akita'; // importing respective dependencies here // ... export class StudentService { constructor( private studentDataService: StudentDataService, private studentStore: StudentStore, private studentQuery: StudentQuery ) { } getStudents(): Observable> { const request = this.studentDataService.getStudents().pipe( tap(s => this.studentStore.set(s)) ); return this.studentQuery.isPristine ? request : noop(); // request } deleteStudent(id: ID) { this.studentStore.remove(id); } updateStudent(student: Student) { this.studentStore.createOrReplace(student.id, { ...student }); } }
Асинхронная логика и вызовы обновления должны быть инкапсулированы в сервисах.
Компоненты могут использовать StudentService
и StudentQuery
для обработки всех действия DML и DDL, необходимые для модели.
// dashboard.component.ts import { ID } from '@datorama/akita'; import { createStudent, StudentService, StudentQuery, Student } from './state/index'; @Component({ template: `...`, ... }) export class DashboardComponent implements OnInit { formData: Student; students$: Observable>; constructor( private studentService: StudentService, public studentQuery: StudentQuery ) { } ngOnInit() { this.studentService.getStudents().subscribe(); // настройка хранилища с данными учеников this.students$ = this.studentQuery.selectAll(); } onAdd() { this.formData = createStudent({}); } onEdit(id: ID) { this.formData = this.studentQuery.getEntity(id) } onDelete(id: ID) { if (confirm('Are you sure to delete?')) { this.studentService.deleteStudent(id); } } updateformData(student: Student) { this.studentService.updateStudent(student); } }
Вот окончательное состояние приложения:
Вывод
Короче говоря, Акита поможет управлять всеми вашими данными в одном месте и даст вам сильную абстракцию при обработке каждой операции - операции DML или DDL, которую мы думали выполнить над данными.
Используйте Акиту, это облегчит вашу жизнь.