Digamos que se enfrenta a la tarea de escribir una aplicación frontend. Hay una tarea técnica con una descripción de la funcionalidad, tickets en el rastreador de errores. Pero la elección de una arquitectura específica depende de usted.
, , . . , ? - , . — .
.
: .
. , , - . HTTP API.
. , . , . , , .
, .
—
—
:
:
—
—
—
NgRx
NgRx
NgRx
NgRx
NgRx
—
, ( app-table) ( app-filters). , . — , ( ), ( ).
, . :
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common';
@Component({
selector: 'app-root',
template: `
<app-filters (selectedChange)="onFilterChange($event)" [filtersData]="filtersData$ | async"></app-filters>
<app-table [data]="data$ | async"></app-table>
`,
})
export class VerySimpleAppComponent implements OnInit {
data$: Observable<Item[]>;
filtersData$: Observable<any>;
constructor(public http: HttpClient) {
this.data$ = this.http.get<Item[]>('/api/data');
this.filtersData$ = this.http.get<any>('/api/filtersData');
}
onFilterChange(filterValue: any) {
this.data = this.filterData(filterValue);
}
private filterData(filterValue): Item[] {
//
}
}
—
: . , . :
@Component({
selector: 'app-root',
template: `
<app-filters (selectedChange)="onFilterChange($event)" [filtersData]="filtersData"></app-filters>
<router-outlet></router-outlet> <!-- -->
`,
})
export class AppComponent {
}
. .
, -? , - . layout :
@Component({
selector: 'app-root',
template: `
<router-outlet></router-outlet> <!-- -->
`,
})
export class AppComponent {
}
//
@Component({
selector: 'app-first-page',
template: `
<!-- -->
<app-filters-common (selectedChange)="onFilterChange($event)" [filtersData]="filtersData"></app-filters-common>
<!-- , -->
<app-filters-for-first-table><app-filters-for-first-table>
<app-table-first [data]="data"></app-table-first>
`,
})
export class FirstPageComponent {}
@Component({
selector: 'app-second-page',
template: `
<!-- -->
<app-filters-common (selectedChange)="onFilterChange($event)" [filtersData]="filtersData"></app-filters-common>
<!-- , -->
<app-filters-for-second-table><app-filters-for-second-table>
<app-table-second [data]="data"></app-table-second>
`,
})
export class SecondPageComponent {}
, : , . : ( , ) . , . , . app-root , - .
, , . :
, , . Angular . , , . , ( ; , ), — . , (, ), . , (, , ), . , , .
. , Angular .
, , , . , . ?
:
, : . , Angular , ChangeDetectionStrategy.Default, (, - , ).
, , : . , , .
:
, - . — RxJS.
Observable
-:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class StorageService {
//
// data = 'initial value';
//
private dataSubject: BehaviorSubject<string> = new BehaviorSubject('initial value');
data$: Observable<string> = this.dataSubject.asObservable();
setData(newValue: string) {
this.dataSubject.next(newValue);
}
}
BehaviorSubject. Subject (. . Observable
, Observer
), , . data$
. .
RxJS :
@Component({
selector: 'my-app',
template: `{{ data }}`,
})
export class AppComponent implements OnDestroy {
data: any;
subscription: Subscription = null;
constructor(public storage: StorageService) {
this.subscription = this.storage.data$.subscribe(data => this.data = data);
}
ngOnDestroy(): void {
// !
if (this.subscription !== null) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
}
Angular , Promise
Observable
, — async. :
@Component({
selector: 'my-app',
template: `{{ data$ | async }}`, //
})
export class AppComponent {
data$: Observable<any>;
constructor(public storage: StorageService) {
this.data$ = this.storage.data$;
}
}
, , . ? :
-
OnPush
- , , . async . - .
—
.
— : , . .
Observable-, :
export class BehaviorSubjectItem<T> {
readonly subject: BehaviorSubject<T>;
readonly value$: Observable<T>;
// : `BehaviorSubject`, .
get value(): T {
return this.subject.value;
}
set value(value: T) {
this.subject.next(value);
}
constructor(initialValue: T) {
this.subject = new BehaviorSubject(initialValue);
this.value$ = this.subject.asObservable();
}
}
:
@Injectable()
export class FiltersStore {
private apiUrl = '/path/to/api';
readonly filterData: BehaviorSubjectItem<{ value: string; text: string }[]> = new BehaviorSubjectItem([]);
readonly selectedFilters: BehaviorSubjectItem<string[]> = new BehaviorSubjectItem([]);
constructor(private http: HttpClient) {
this.fetch(); // -
}
fetch() {
this.http.get(this.apiUrl).subscribe(data => this.filterData.value = data);
}
setSelectedFilters(value: { value: string; text: string }[]) {
this.selectedFilters.value = value;
}
}
:
@Injectable()
export class TableStore {
private apiUrl = '/path/to/api';
tableData: BehaviorSubjectItem<any[]>;
loading: BehaviorSubjectItem<boolean> = new BehaviorSubjectItem(false);
constructor(private filterStore: FilterStore, private http: HttpClient) {
this.fetch();
}
fetch() {
this.tableData$ = this.filterStore.selectedFilters.value$.pipe(
tap(() => this.loading.value = true),
switchMap(selectedFilters => this.http.get(this.apiUrl, { params: selectedFilters })),
tap(() => this.loading.value = false),
share(),
);
}
}
(, ), combineLatest:
this.tableData$ = combineLatest(firstSource$, secondSource$).pipe(
// start loading
switchMap([firstData, secondData] => /* ... */)
// end loading
);
:
@Component({
selector: 'first-table-page',
template: `
<app-filters (selectedChange)="onFilterChange($event)" [filterData]="filterData$ | async"></app-filters>
<app-table-first [data]="tableData$ | async" [loading]="loading$ | async"></app-table-first>
`,
})
export class FirstTablePageComponent {
filterData$: Observable<FilterData[]>;
tableData$: Observable<TableItem[]>;
loading$: Observable<boolean>;
constructor(private filtersStore: FiltersStore, private tableStore: FirstTableStore) {
this.filterData$ = filtersStore.filterData.value$;
this.tableData$ = tableStore.tableData.value$;
this.loading$ = tableStore.loading.value$;
}
onFilterChange(selectedFilters) {
this.filtersStore.setSelectedFilters(selectedFilters)
// ,
this.filtersStore.selected.value = selectedFilters;
}
}
:
- - ;
- - (, ) ;
- ;
- : , .
—
, store- : Single Responsibility, , . , :
- . , - , .
- . , 5–10 store-.
- . , Foo Bar, , Foo, , Foo Bar, . .
- , . , . . , - (, localStorage), . .
, . , : . , .
-, HTTP API , , . . — .
-, store- , :
export interface FilterItem<T> {
value: T;
text: string;
}
export interface FilterState<T> {
data: FilterItem<T>[];
selected: string[];
}
type ColorsFilterState = FilterState<string>;
type SizesFilterState = FilterState<int>;
export interface FiltersState {
colors: ColorsFilterState;
sizes: SizesFilterState;
}
const FILTERS_INITIAL_STATE: FiltersState = {
colors: {
data: [],
selected: [],
},
sizes: {
data: [],
selected: [],
},
};
@Injectable()
export class FiltersStore {
filtersState: BehaviorSubjectItem<FiltersState> = new BehaviorSubjectItem(FILTERS_INITIAL_STATE);
}
-, :
@Injectable()
export class FiltersStore {
filtersState: BehaviorSubjectItem<FiltersState> = = new BehaviorSubjectItem({/* ... */});
setColorsData(data: FilterItem<string>[]) {
const oldState = this.filtersState.value;
this.filtersState.value = {
...oldState,
colors: {
...oldState.colors,
data,
},
};
}
setColotsSelected(selected: string[]) {
const oldState = this.filtersState.value;
this.filtersState.value = {
...oldState,
colors: {
...oldState.colors,
selected,
},
};
}
}
-, :
@Injectable()
export class FiltersStore {
filtersState: BehaviorSubjectItem<FiltersState> = = new BehaviorSubjectItem({/* ... */});
setColorsData(data: FilterItem<string>[]) {/* ... */}
setColotsSelected(selected: string[]) {/* ... */}
colorsFilter$: Observable<ColorsFilterState> = this.filtersState.value$.pipe(
map(filtersState => filtersState.colors), // pluck('colors')
);
colorsFilterData$: Observable<FilterItem<string>[]> = this.colorsFilter$.pipe(
map(colorsFilter => colorsFilter.data),
);
colorsFilterSelected$: Observable<string[]> = this.colorsFilter$.pipe(
map(colorsFilter => colorsFilter.selected),
);
}
—
. : . .
. . observable-, : , . , ( ). Flux, Facebook.
. :
- - ;
- ;
- .
- . , , DI:
export interface AppState {/* ... */}
export const INITIAL_STATE: InjectionToken<AppState> = new InjectionToken('InitialState');
// - AppModule
const appInitialState: AppState = {/* ... */};
@NgModule({
providers: [
Store,
{ provide: INITIAL_STATE, useValue: appInitialState },
]
})
export class AppModule {}
— init
, APP_INITIALIZER:
@Injectable()
export class Store<AppState> {
state: BehaviorSubjectItem<AppState>;
init(initialState: AppState) {
this.state = new BehaviorSubjectItem(initialState);
}
}
const appInitialState: AppState = {/* ... */};
export function initStore(store: Store<AppState>) {
return () => store.init();
}
@NgModule({
providers: [
Store,
{
provide: APP_INITIALIZER,
useFactory: initStore,
deps: [Store],
multi: true,
}
]
})
export class AppModule {}
, . — , .
. :
// :
export interface ColorsFilterState {
data: FilterItem<string>[];
selected: string[];
}
: . - , , . , , :
interface ChangeColorsDataAction {
type: 'changeData';
payload: FilterItem<string>[];
}
interface ChangeColorsSelectedAction {
type: 'changeSelected';
payload: string[];
}
type ColorsActions = ChangeColorsDataAction | ChangeColorsSelectedAction;
, :
export const changeColorsDataState: (oldState: ColorsFilterState, data: FilterItem<string>[]) => FilterState = (oldState, data) => ({ ...oldState, data });
export const changeColorsSelectedState: (oldState: ColorsFilterState, selected: string[]) => FilterState = (oldState, selected) => ({ ...oldState, selected });
. , , :
export const changeColorsState: (oldState: ColorsFilterState, action: ColorsActions) => ColorsFilterState = (state, action) => {
if (action.type === 'changeData') {
return changeColorsDataState(oldState, action.payload);
}
if (action.type === 'changeSelected') {
return changeColorsSelectedState(oldState, action.payload);
}
return oldState;
}
? , , ColorsFilterState
:
@Injectable()
export class Store<ColorsFilterState> {
state: BehaviorSubjectItem<ColorsFilterState>;
init(
state: ColorsFilterState,
changeStateFunction: (oldState: ColorsFilterState, action: ColorsActions) => ColorsFilterState, // <- ,
) {/* ... */}
changeState(action: ColorsActions) {
this.state.value = this.changeStateFunction(this.state.value, action);
}
}
//
this.store.changeState({ type: 'changeSelected', payload: ['red', 'orange'] });
, , :
- ;
- ;
- , ;
- ,
.
. . , changeStateFunction
, . . - , :
// :
export interface FiltersState {
colors: ColorsFilterState;
sizes: SizesFilterState;
}
//
export const changeColorsState = (state, action) => {/* ... */};
//
export const changeSizeState = (state, action) => {/* ... */};
export const changeFunctionsMap = {
colors: changeColorsState,
sizes: changeSizeState,
};
export function combineChangeStateFunction(fnsMap) {
return function (state, action) {
const nextState = {};
Object.entries(fnsMap).forEach(([key, changeFunction]) => {
nextState[key] = changeFunction(state[key], action);
});
return nextState;
}
}
- EventBus:
- — ;
changeStateFunction
;- , ,
- .
, , state manager'. - , , . , :
- NgRx;
- NGXS;
- MobX + MobX Angular (
, ).
, .
NgRx
NgRx
NgRx Redux Angular. Redux , react-.
Redux NgRx . . — store. , . , «» — , ( ) . «» — , .
API - (, ), Middleware ( NgRx — Effect).
, Redux NgRx:
- view - ( , , XHR- . .).
- .
- , . , Middleware (Effect), API.
- , .
- View .
- API ( ):
- ;
- 4 5.
NgRx
@ngrx/store
: . — Store
. . Observable
, — RxJS async
-. , . , select. map
pluck.
, . dispatch
store
.
export interface FilterItem<T> {
key: string;
value: T;
}
//
export enum FiltersActionTypes {
Change = '[Filter] Change',
Reset = '[Filter] Reset',
}
export class ChangeFilterAction implements Action {
readonly type = FiltersActionTypes.Change;
constructor(public payload: { filterItem: FilterItem<string> }) {}
}
export class ResetFilterAction implements Action {
readonly type = FiltersActionTypes.Reset;
constructor() {}
}
export type FiltersActions = ChangeFilterAction | ResetFilterAction;
//
export interface FiltersState {
filterData: FilterItem<string>[];
selected: FilterItem<string>;
}
export const FILTERS_INIT_STATE: FiltersState = {
filterData: [
{ key: '-', value: '---' },
{ key: 'red', value: '' },
{ key: 'green', value: '' },
{ key: 'blue', value: '' },
],
selected: { key: '-', value: '---' },
};
// ,
export function filtersReducer(state: FiltersState = FILTERS_INIT_STATE, action: FiltersActions) {
switch (action.type) {
case FiltersActionTypes.Change:
return {
...state,
selected: action.payload.filterItem,
};
case FiltersActionTypes.Reset:
return {
...state,
selected: { key: '-', value: '---' },
};
default:
return state;
}
}
// -, select
// , ,
export const selectedFilterSelector = createSelector(state => state.filters.seleced);
//
export interface AppState {
filters: FiltersState;
}
export const reducers: ActionReducerMap<AppState> = {
filters: filtersReducer,
}
@NgModule({
imports: [
StoreModule.forRoot(reducers),
],
// ...
})
export class AppModule {}
//
@Component({
selector: 'app-root',
template: `
<app-filters [data]="filtersData$ | async"
[selected]="selectedFilter$ | async"
(change)="onChange($event)"
(reset)="onReset()">
`,
})
export class AppComponent {
filtersData$: Observable<FilterItem<string>[]>;
selectedFilter$: Observable<FilterItem<string>>;
constructor(private store: Store<AppState>) {
this.filtersData$ = this.store.pipe(select('filters', 'data'));
// -
this.selectedFilter$ = this.store.pipe(select(selectedFilterSelector));
}
onChange(filterItem: FilterItem) {
this.store.dispatch(new ChangeFilterAction({ filterItem }));
}
onReset() {
this.store.dispatch(new ResetFilterAction());
}
}
@ngrx/effects
: -, . , , . , . @ngrx/store
«» .
- : API, , . Redux Middleware, NgRx — . — @Effect
.
, Effect
, , . Observable<Action>
. . Actions , - : , API . . - .
@Injectable()
export class AppEffects {
//
@Effect({ dispatch: false })
loginSuccess$ = this.actions$.pipe(
ofType(LoginActionTypes.Success),
tap(() => {
this.logger.log('Login success')
}),
);
// API
//
// FiltersActionTypes.LoadStarted API
// FiltersDataLoadSuccess
// FiltersDataLoadFailure,
@Effect()
loadFilterData$ = this.actions$.pipe(
ofType(FiltersActionTypes.LoadStarted),
switchMap(() => this.filtersBackendService.fetch()),
map(filtersData => new FiltersDataLoadSuccess({ filtersData })),
catchError(error => new FiltersDataLoadFailure({ error })),
);
// Actions — Observable @ngrx/store
//
constructor(private actions$: Actions, private filtersBackendService: FiltersBackendService) {}
}
, -:
@NgModule({
imports: [
EffectsModule.forRoot([ AppEffects ]),
],
})
export class AppModule {}
@ngrx/store-devtools
Redux DevTools NgRx. . :
- ;
- ;
- - .
— @ngrx/store-devtools
. — , — :
@ngrx/router-store
NgRx Angular. :
- ;
- ;
- NgRx.
, . «» , DevTools . URL , , .
NgRx
NgRx — . , . , .
, , :
export interface FiltersState {/* ... */}
export const FILTERS_INIT_STATE: FiltersState = {/* ... */};
export function filtersReducer(state, action) {/* ... */}
export interface AppState {
filters: FiltersState;
}
export const reducers: ActionReducerMap<AppState> = {
filters: filtersReducer,
}
,
:
- .
- .
- .
- .
AppState :
export interface FirstTableState {/* ... */}
export const TABLE_INIT_STATE: FirstTableState = {/* ... */};
export function firstTableReducer(state, action) {/* ... */}
export const firstTableDataSelector = state => state.firstTable.data;
export const firstTableLoadingSelector = state => state.firstTable.loading;
export interface AppState {
filters: FiltersState;
firstTable: FirstTableState;
}
export const reducers: ActionReducerMap<AppState> = {
filters: filtersReducer,
firstTable: firstTableReducer,
}
, lazy loading . , ? , . , . , . , , , .
, NgRx. , . , . feature- . , :
@NgModule({
imports: [
StoreModule.forFeature('auth', authReducer),
StoreModule.forFeature('user', userReducer),
StoreModule.forFeature('config', configReducer),
EffectsModule.forFeature([ AuthEffects, UserEffects, ConfigEffects ]),
],
})
export class LibModule {}
API . , API .
LibModule
. API, .
NgRx
, . , NgRx .
NgRx . , . NgRx : , , .
. . , (, withLatestFrom RxJS). , .
. , . :
// qux foo bar
// { foo: { qux: 1 }, bar: { baz: 2 } } => { foo: {}, bar: { qux: 1, baz: 2 } }
//
export const fooReducer = (state, action) => {/* ... */}
export const quxSelector = state => state.foo.qux;
//
export const barReducer = (state, action) => {/* ... */}
export const quxSelector = state => state.bar.qux;
export const reducers = {
foo: fooReducer,
bar: barReducer,
};
//
@Component({})
export class AppComponent {
constructor(private store: Store<AppState>) {
this.qux$ = this.store.pipe(select(quxSelector));
}
}
. NgRx :
@Injectable()
export class AppEffects {
@Effect({ dispatch: false })
log$ = this.actions$.pipe(tap(action => this.logger.log(action)));
constructor(private actions$: Actions, private logger: Logger) {}
}
, . Actions , , :
@Injectable()
export class AppEffects {
@Effect({ dispatch: false })
log$ = this.actions$.pipe(tap(action => this.logger.log(action)));
constructor(private actions$: Actions, private logger: Logger) {}
}
.
, NgRx .
. , .
. NgRx - . , ag-Grid. , , . «» ag-Grid . , - . ag-Grid , . , (, , ) , . , ag-Grid NgRx, ag-Grid . API, NgRx . : , , NgRx.
. , . Angular, . .
, , Angular.
, Angular 2 . .
NgRx . - ( , ) .
:
- , — .
- , — .
- , , — , :
- , , HTTP API, ( backend-). backend- , store-.
- : «» «». «» «» . «» , . «»
@Input
@Output
. «» , «» . - , . , .
- , , , NgRx.
- NgRx, . NgRx. NgRx, — , .
- , , , , NgRx. .