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 | Файл | Содержимое |
|---|---|---|
| MainStore | libs/main-state/src/lib/main.store.ts | Контрагенты, организации, ассортимент, документы, справочники |
| CurrentUserStore | libs/shared/state/current-user.store.ts | Текущий пользователь, права доступа |
Feature stores (библиотеки)
| Store | Файл | Содержимое |
|---|---|---|
| OpenDocumentsStore | libs/layout/main-menu/state/open-documents.store.ts | Открытые вкладки документов |
| FileAttachmentStore | libs/file-attachment/src/lib/state/file-attachment-counterparty.store.ts | Файлы контрагента |
| ContactStore | libs/contact-components/src/lib/state/contact.store.ts | Контакты в форме |
| PaySheetStateStore | libs/manufacture/salary-report/.../PaySheetState.store.ts | Зарплатные ведомости |
App-specific stores (main-office)
| Store | Файл | Содержимое |
|---|---|---|
| TaskStore | apps/main-office/.../task/state/task.store.ts | Задачи и фильтры |
| OzonStore | apps/main-office/.../ozon/state/ozon.store.ts | Заказы и товары Ozon |
| DocumentFilterStore | apps/main-office/.../document-filter/state/document-filter.store.ts | Активные фильтры документов |
| DocumentStore | apps/main-office/.../document-detail/store/document.store.ts | Детали открытого документа |
App-specific stores (другие приложения)
| Store | Приложение | Содержимое |
|---|---|---|
| MainWarehouseStore | warehouse-worker | Задания склада, текущий пользователь |
| WowXMainStore | wow-x-office | Основное состояние WoW-X |
| OzonProductsStore | narmak-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()
}Связанные разделы
- WebSocket — инициализация данных
- Приложения Frontend — overview приложений