Skip to content

State Management (Akita)

Frontend Narmak ERP использует Akita — реактивную библиотеку управления состоянием на основе RxJS и паттерна Store/Query.

Версия: @datorama/akita ^8.0.1
Dev tools: @datorama/akita-ngdevtools
Router store: @datorama/akita-ng-router-store

Концепция Akita

  • Store — хранилище состояния (иммутабельный объект)
  • Query — реактивные селекторы для чтения состояния
  • Service — методы для обновления стора через API-запросы

Карта всех Stores

Глобальные (shared между приложениями)

StoreФайлСодержимое
MainStorelibs/main-state/src/lib/main.store.tsКонтрагенты, организации, ассортимент, документы, справочники
CurrentUserStorelibs/shared/state/current-user.store.tsТекущий пользователь, права доступа

Feature stores (библиотеки)

StoreФайлСодержимое
OpenDocumentsStorelibs/layout/main-menu/state/open-documents.store.tsОткрытые вкладки документов
FileAttachmentStorelibs/file-attachment/src/lib/state/file-attachment-counterparty.store.tsФайлы контрагента
ContactStorelibs/contact-components/src/lib/state/contact.store.tsКонтакты в форме
PaySheetStateStorelibs/manufacture/salary-report/.../PaySheetState.store.tsЗарплатные ведомости

App-specific stores (main-office)

StoreФайлСодержимое
TaskStoreapps/main-office/.../task/state/task.store.tsЗадачи и фильтры
OzonStoreapps/main-office/.../ozon/state/ozon.store.tsЗаказы и товары Ozon
DocumentFilterStoreapps/main-office/.../document-filter/state/document-filter.store.tsАктивные фильтры документов
DocumentStoreapps/main-office/.../document-detail/store/document.store.tsДетали открытого документа

App-specific stores (другие приложения)

StoreПриложениеСодержимое
MainWarehouseStorewarehouse-workerЗадания склада, текущий пользователь
WowXMainStorewow-x-officeОсновное состояние WoW-X
OzonProductsStorenarmak-web-siteПродукты Ozon на сайте
CartStore (narmak)narmak-web-siteКорзина покупателя
CartStore (counterparty)counterparty-officeКорзина контрагента

Паттерн Store/Query

Store (EntityStore)

typescript
// libs/main-state/src/lib/main.store.ts
export interface MainState {
  counterparties: CounterParty[]
  organizations: Organization[]
  assortiment: Assortiment[]
  currentOrganization: Organization | null
  isLoading: boolean
}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'main' })
export class MainStore extends Store<MainState> {
  constructor() {
    super(createInitialState())
  }
}

Query

typescript
// libs/main-state/src/lib/main.query.ts
@Injectable({ providedIn: 'root' })
export class MainQuery extends Query<MainState> {
  // Синхронное значение
  counterparties$ = this.select('counterparties')

  // Вычисляемый селектор
  activeOrganization$ = this.select(state =>
    state.organizations.find(o => o.id === state.currentOrganization?.id)
  )

  constructor(protected store: MainStore) {
    super(store)
  }
}

Service (обновление стора)

typescript
// Типичный паттерн сервиса
@Injectable({ providedIn: 'root' })
export class CounterpartyService {
  constructor(
    private store: MainStore,
    private apiClient: ApiClientService,
  ) {}

  loadCounterparties(): Observable<CounterParty[]> {
    this.store.setLoading(true)
    return this.apiClient.getCounterparties().pipe(
      tap(counterparties => {
        this.store.update({ counterparties })
        this.store.setLoading(false)
      }),
      catchError(err => {
        this.store.setError(err)
        return EMPTY
      })
    )
  }
}

Использование в компоненте

typescript
@Component({...})
export class DocumentListComponent {
  counterparties$ = this.mainQuery.counterparties$
  isLoading$ = this.mainQuery.selectLoading()

  constructor(
    private mainQuery: MainQuery,
    private counterpartyService: CounterpartyService,
  ) {}

  ngOnInit() {
    this.counterpartyService.loadCounterparties().subscribe()
  }
}

Инициализация данных

При загрузке приложения init-data.service.ts загружает начальные данные через WebSocket:

typescript
// libs/websocket/src/lib/init-data-root.service.ts
@Injectable({ providedIn: 'root' })
export class InitDataRootService {
  constructor(
    private wsService: WebsocketService,
    private store: MainStore,
  ) {}

  init() {
    this.wsService.connect()
    this.wsService.getInitialData().subscribe(data => {
      this.store.update({
        counterparties: data.counterparties,
        organizations: data.organizations,
        assortiment: data.assortiment,
        // ...
      })
    })
  }
}

DevTools

В development-режиме доступны Akita DevTools (через Redux DevTools расширение в Chrome):

  • Просмотр текущего состояния всех сторов
  • История actions
  • Time-travel debugging
typescript
// apps/main-office/src/main.ts
if (!environment.production) {
  enableAkitaProdMode()
  AkitaNgDevtools.init()
}

Связанные разделы