Frontend Wolves

Rafał Brzoska

  • Senior Software Developer
  • JS, TS, Angular
  • Organizator Angular Silesia
  • Angular Trainer, Prelegent
  • Bottega IT Mind

Łukasz Max Kokoszka

  • Senior Software Developer
  •  JS & Frontend
  • Internal Trainer, Prelegent

Jak ogarnąć architekturę rozbudowanej wielomodułowej aplikacji dzięki

Angular'owi i NgRx

Z czego wzięła się cała nasza rozkmina?

Jeden produkt o spójnym UX

Różnorodność wymagań

Rozproszony development

Wymagania architektoniczne

Szkielet (Core)

Globalizacja (g11n)

Bezpieczeństwo

UX

Pozostałe elementy wspólne

Feature #1

Uwierzytelnienie, Autoryzacja, Dane

Lokalizacja, Internacjonalizacja

Wspólne komponenty i kontenery

Routing, Serwisy, Storage

Moduły biznesowe

Feature #2

Fantastyczna czwórka

Angular

TypeScript

Angular CLI

Components

Routing (Guards, Lazy Loading)

Angular Module (NgModule)

Angular Module (NgModule)

Angular Modules

Lazy Loaded FeatureModule

Services

Components

Directives

Eagery Loaded FeatureModule

Services

Components

Directives

App Module

Services

Components

Directives

Shared Module

Components

Directives

// src/app/app-routing.module.ts

// ...

const routes: Routes = [
  // ...

  {
    path: 'lazyLoading',
    canActivate: [ LoadTranslationsForFeatureGuard ],
    loadChildren: 'app/features/lazy-loading/lazy-loading.module#LazyLoadingModule'
  },

  {
    path: 'eagerlyLoading',
    canActivate: [ LoadTranslationsForFeatureGuard ],
    component: EagerComponent
  }
 
];

@NgModule({
  imports: [ RouterModule.forChild(routes) ],
  exports: [ RouterModule ]
})
export class AppRoutingModule { }

RxJS

Callback vs Promise vs Observable

Functional Programming

Reactive Programming

RxJS

Observable

Observer

Subscription

Operator

Subject

Redux

Store

Side effect

View

Action

Reducer

Action

NgRx

NgRx

Biblioteka Angular

@store, @effects, @router-store, @store-devtools, @schematics

ObservableStore

Silnie typowane Akcje

NgRx

subscription

Store

Effect

View

Action

Reducer

Action

Component

Dodatkowe wyzwania

Kontrola developmentu

Kontrola stanu

Deployment i budowanie

S jak Sandbox

Core

Store

Sandbox

Feature #1

Feature #3

Feature #2

Services

Shared components

Services

Services

Services

Components

Components

Components

Store

Store

Store

Sandbox

Sandbox

Sandbox

app.module.ts
app.sandbox.ts
app
store
core
features
feature1.module.ts
feature1.sandbox.ts
store
feature1
feature2.module.ts
feature2.sandbox.ts
store
feature2
actions
reducers
effects
selectors
states

Sandbox

Budowa Sandboxa

@Injectable()
class AppSandbox {

  constructor(
    private appState$: Store<AppState>,
    private actionsSubj: ActionsSubject,
    private childSandbox: ChildSandbox) {}

  dataStream$ = this.appState$.select(dataSelector);

}

Components

Store

Data streams

Dispatchers

Special  (observable) dispatchers

@Injectable()
class AppSandbox {

  constructor(
    private appState$: Store<AppState>,
    private actionsSubj: ActionsSubject,
    private childSandbox: ChildSandbox) {}

  dataStream$ = this.appState$.select(dataSelector);

  doSomething(newData) {
    this.appState$.dispatch(new DoSomethingAction(newData))
  }

}
@Injectable()
class AppSandbox {

  constructor(
    private appState$: Store<AppState>,
    private actionsSubj: ActionsSubject,
    private childSandbox: ChildSandbox) {}

  dataStream$ = this.appState$.select(dataSelector);

  doSomething(newData) {
    this.appState$.dispatch(new DoSomethingAction(newData))
  }

  doSomethingSpecial$$(): Observable<string> {
      return getDispatchObservable(
        this.actionsSubject,
        this.appState$,
        SampleActionTypes.DoSomethingSpecialSuccess,
        SampleActionTypes.DoSomethingSpecialFailure,
        new DoSomethingSpecialAction()
    );
  }
}
@Injectable()
class AppSandbox {

  constructor(
    private appState$: Store<AppState>,
    private actionsSubj: ActionsSubject,
    private childSandbox: ChildSandbox) {}

}

Services

Services

@Injectable()
class AppSandbox {

  constructor(
    private appState$: Store<AppState>,
    private actionsSubj: ActionsSubject,
    private childSandbox: ChildSandbox) {}

  dataStream$ = this.appState$.select(dataSelector);

  doSomething(newData) {
    this.appState$.dispatch(new DoSomethingAction(newData))
  }

  doSomethingSpecial$$(): Observable<string> {
      return getDispatchObservable(
        this.actionsSubject,
        this.appState$,
        SampleActionTypes.DoSomethingSpecialSuccess,
        SampleActionTypes.DoSomethingSpecialFailure,
        new DoSomethingSpecialAction()
    );
  }
  
  doSomethingElse(data) {
    this.childSandbox.doSomethingElse(data);
  }

}

Re-exports

// src/app/component/sample.component.ts
// ...
@Component({
  selector: 'sample',
  templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss']
})
export class SampleComponent implement OnInit, OnDestroy {
   
  dataStream$: Observable<any>;

  constructor(private appSandbox: AppSandbox, private childSandbox: ChildSandbox) {
      this.dataStream$ = this.appSandbox.dataStream$;
  }

}
// src/app/component/sample.component.ts
// ...
@Component({
  selector: 'sample',
  templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss']
})
export class SampleComponent implement OnInit, OnDestroy {

  dataStream$: Observable<any>;

  constructor(private appSandbox: AppSandbox, private childSandbox: ChildSandbox) {
      this.dataStream$ = this.appSandbox.dataStream$;
  }

  ngOnInit() {
    this.routerSubscription = this.appSandbox.routeParams$.pipe(take(1))
        .subscribe((p: Params) => {}, () => {});
  }
}
// src/app/component/sample.component.ts
// ...
@Component({
  selector: 'sample',
  templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss']
})
export class SampleComponent implement OnInit, OnDestroy {

  dataStream$: Observable<any>;

  constructor(private appSandbox: AppSandbox, private childSandbox: ChildSandbox) {
      this.dataStream$ = this.appSandbox.dataStream$;
  }

  ngOnInit() {
    this.routerSubscription = this.appSandbox.routeParams$.pipe(take(1))
        .subscribe((p: Params) => this.appSandbox.doSomething(p.param1), () => {});
  }

  buttonClicked() {
    this.childSandbox.doSomethingElse();
  }

  ngOnDestroy() {
    this.routerSubscription.unsubscribe();
  }
}
// src/app/component/sample.component.ts
// ...
@Component({
  selector: 'sample',
  templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss']
})
export class SampleComponent implement OnInit, OnDestroy {
  
  dataStream$: Observable<any>;

  constructor(private appSandbox: AppSandbox, private childSandbox: ChildSandbox) {
      this.dataStream$ = this.appSandbox.dataStream$;
  }

  ngOnInit() {
    this.routerSubscription = this.appSandbox.routeParams$.pipe(take(1))
        .subscribe((p: Params) => this.appSandbox.doSomething(p.param1), () => {});
  }
}
// src/app/component/sample.component.ts
// ...
@Component({
  selector: 'sample',
  templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss']
})
export class SampleComponent implement OnInit, OnDestroy {
  
  dataStream$: Observable<any>;

  constructor(private appSandbox: AppSandbox, private childSandbox: ChildSandbox) {
      this.dataStream$ = this.appSandbox.dataStream$;
  }

  ngOnInit() {
    this.routerSubscription = this.appSandbox.routeParams$.pipe(take(1))
        .subscribe((p: Params) => this.appSandbox.doSomething(p.param1), () => {});
  }

  buttonClicked() {
    this.childSandbox.doSomethingElse();
  }
}

Gratisy

Mało zależności w komponentach 

Agregacja zależności

Uproszczone testowanie

Problemy

Duży przyrost kodu NgRx

Ograniczone wsparcie ze strony narzędzi

Puchnący Sandbox

Ciężkie wejście dla nowych osób w projekcie

Pułapki RxJS i NgRx

// src/app/store/reducers/example.reducer.ts
// ...

export function sampleReducer(
  state = initialState,
  action: SampleActions
): SampleState {

  switch (action.type) {
      case SampleActionTypes.DoSomethingSpecialSuccess:
        return {
          ...state,
          exampleStateSlice: action.data
        };
      }
  }
}
// src/app/store/effects/example.effects.ts
// ...

@Injectable()
export class SampleEffects {

  constructor(
    private actions$: Actions, private dataRepository: DataRepository
  ) {}

  @Effect()
  doSomething$ = this.actions$.pipe(

    ofType(SampleActionTypes.DoSomethingSpecialSuccess),

    switchMap((action: DoSomethingAction) =>
      this.dataRepository.getDataFromServer(action.payload.id, action.payload.params).pipe(
        map((data) => new DoSomethingSpecialSuccess(data)),
        catchError((error: string) => of(new DoSomethingSpecialFailure(error)))
      )
    )
  );
}

Go & code! :-)

linkedin.com/in/lukasz-max-kokoszka

twitter.com/rafal_brzoska

linkedin.com/in/rafalbrzoska

Made with Slides.com