Bidirectional Service – komunikacja komponentów poprzez serwis
Najczęściej nasze komponenty komunikują się przy użyciu dekoratorów Input() oraz Output(). Niestety niesie to za sobą pewne ograniczenia. Zarówno Input oraz Output, służą do komunikacji komponentów w bezpośredniej relacji rodzic-dziecko. Co jeśli chcemy komunikować komponenty niezależnie od relacji?
BIDIRECTIONAL SERVICE
Pomoże nam w tym dwukierunkowy serwis, który będzie współdzielony przez rodzic-komponent oraz jego dzieci.
Zasięg instancji serwisu dotyczy tylko danego drzewa komponentów, wszystkie komponenty, które nie należą do danego drzewa, nie mają dostępu do serwisu.
Na potrzeby artykułu, załączam Live Example.
Jakie wątki poruszymy w tym artykule?
- napiszemy bi-directional service, który będzie zawierał metody do komunikacji naszych komponentów
- wyślemy dane do komponentu trzy levele niżej w drzewie komponentów
- komponent najniżej zagnieżdżony skomunikuje się z komponentem-rodzicem
W przedstawieniu graficznym, wykonamy następujące interakcje:
TWORZYMY SERWIS
Jeśli zapoznałeś się już ze plikami z live example na plunkerze, to skupimy się teraz na naszym dwu-kierunkowym serwisie (user-shared.service.ts):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Injectable() export class UserSharedService { private userSource = new Subject<User>(); private messageSource = new Subject<string>(); userContent$ = this.userSource.asObservable(); messageSent$ = this.messageSource.asObservable(); shareUser(user: User): void { this.userSource.next(user); } tellSomethingToParent(message: string): void { this.messageSource.next(message); } } |
Rozłóżmy kod na czynniki:
1 |
private userSource = new Subject<User>(); |
Subject jest klasą, która pochodzi z biblioteki RxJs, która implementuje reaktywne programowanie. Jest to skomplikowany i mocno rozbudowany koncept, stąd na potrzeby tego artykułu, nie będę wdawać się w szczegóły RxJs. W przypadku zainteresowania tematem, załączam linka do Subject w dokumentacji RxJs.
Zatem krótko o Subject:
- Subject jest strumieniem, jest to jednocześnie Observable oraz Observer
- posiada między innymi metodę next(value), która powoduje emisję przekazanej wartości.
1 |
userContent$ = this.userSource.asObservable(); |
Publiczne pole userContent$ posłuży nam do odbioru informacji w komponentach. Po subskrypcji do tego pola, będziemy mieli dostęp do danych.
1 2 3 |
shareUser(user: User) { this.userSource.next(user); } |
Metoda shareUser posłuży nam do emisji danych usera z poziomu UserDetailComponent.
Analogicznie z messageSource, które wykorzystamy do powiadomienia UserDetailComponent z poziomu ChildChildChildComponent.
WYSYŁAMY DANE DO KOMPONENTU 3 POZIOMY NIŻEJ
Serwis gotowy, więc czas przekazać dane Usera z komponentu UserDetailComponent do ChildChildChildComponent. W pliku user-detail.component.ts, zaczynamy od wstrzyknięcia naszego serwisu UserSharedService w konstruktorze oraz dodania go do providers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Component({ ... providers: [UserSharedService] }) constructor(private userSharedService: UserSharedService, private usersService: UsersService, private route: ActivatedRoute) { this.route.params .switchMap((params: Params) => this.usersService.getUser(+params['id'])) .subscribe(user => { this.user = user this.userSharedService.shareUser(user); }); } |
W powyższym kodzie, odebraliśmy konkretnego usera, metoda subscribe udostępnia nam jego dane. Podepniemy się w tym miejscu z wywołaniem metody shareUser naszego dwu-kierunkowego serwisu UserSharedService, który wyemituje przekazane dane. Pozostaje nam tylko teraz wykonać subskrypcję do $userContent, który umożliwi nam dostęp do wyemitowanych danych.
Przechodzimy do ChildChildChildComponent (child-child-child.component.ts)
1 2 3 4 5 6 7 8 9 10 |
subscription: Subscription; constructor(private userSharedService: UserSharedService) { this.subscription = this.userSharedService.userContent$.subscribe((user) => { this.user = user; }); } ngOnDestroy() { this.subscription.unsubscribe(); } |
Zaczynamy od stworzenia subscription, które pochodzi z biblioteki RxJs. Subscription to obiekt, który reprezentuje zasoby w postaci Observables. Ma jedną bardzo ważną metodę – unsubscribe(), która umożliwia nam się pozbycia zasobów trzymanych w subscription, służy to przede wszystkim zapobieganiu wyciekom pamięci.
Generalnie w naszym przykładzie użycie subscription i metoda unsubscribe() nie ma znaczenia, ponieważ cykl życia ChildChildChildComponent jest identyczny jak UserComponent, także wycieki pamięci nie mają prawa bytu. Jednak w bardziej złożonych aplikacjach, warto pamiętać o użyciu unsubscribe(). Użyłem subscription wyłącznie demonstracyjnie.
Powyższy kod, zapewnił nam odebranie w ChildChildChildComponent danych usera z UserComponent. To jest idea dwukierunkowego serwisu w pigułce. W analogiczny sposób, ChildChildChildComponent da znać UserComponent, że odebrał dane, emitując wiadomość poprzez metodę tellSomethingToParent(), która zostanie wywołana na kliknięcie buttona.
PODSUMOWANIE
Idea bidirectional service jest bardzo przydatna, zwłaszcza jak chcemy wysyłać dane między komponentami, które nie są między sobą bezpośrednio połączone. Pamiętaj, że dodanie takiego serwisu do providers danego komponentu, tworzy Ci nową instancję tego serwisu. Jeśli planujesz użyć takiego serwisu, np. do wystawienia danych zalogowanego użytkownika w całej aplikacji, musisz ten serwis dodać do providers w głównym ngModule aplikacji, dzięki temu nasz serwis będzie singletonem (jedna instancja serwisu na całą aplikację).
A nie lepiej zamiast Subject, dać EventEmitter ?
EventEmitter dziedziczy po Subject, więc mogłoby się wydawać, że też będzie OK.
Tym niemniej Angular Team, zaleza używanie EventEmittera wyłącznie z @Output w dyrektywach i komponentach.
Pingback: RxJS w Angular – co wypada wiedzieć – Angular.love
Twój live example nie śmiga https://www.dropbox.com/s/cu9eeoziuufn82s/Zrzut%20ekranu%202018-07-09%2010.19.35.png?dl=0
dzięki, poprawione 🙂
Tomku, a dlaczego 'Subject’ jest „schowany” i nie jest bezpośrednio wywoływna na nim metoda 'next()’ do emitowania kolejne wartości strumienia?
hej Jarek, przeoczyłem Twoje pytanie 🙂 dlatego, że dzięki temu pozwalasz na wołanie next tylko poprzezz Twoją metodę z serwisu, a strumien robi się tylko do odczytu. Dzięki temu nikt w kodzie nie zrobi subject$.next(tutaj byle co), takie zabezpieczenie.