Tworzenie i zwracanie Observable z usługi Angular 2

136

To bardziej kwestia „sprawdzonych metod”. Jest trzech graczy: a Component, a Servicei a Model. ComponentJest wywołanie Serviceaby pobrać dane z bazy danych. ServiceJest za pomocą:

this.people = http.get('api/people.json').map(res => res.json());

zwrócić plik Observable.

ComponentMoże po prostu zapisać się do Observable:

    peopleService.people
        .subscribe(people => this.people = people);
      }

Jednak to, czego naprawdę chcę, to Servicezwrócenie Array of Modelobiektów, które zostały utworzone na podstawie danych Servicepobranych z bazy danych. Zdałem sobie sprawę, że Componentmoże po prostu utworzyć tę tablicę w metodzie subskrybowania, ale myślę, że byłoby czystsze, gdyby usługa to zrobiła i udostępniła ją Component.

W jaki sposób można Serviceutworzyć nową Observable, zawierającą tę tablicę i zwrócić ją?

Joseph Genchik
źródło

Odpowiedzi:

160

AKTUALIZACJA: 9/24/16 Angular 2.0 Stable

To pytanie nadal generuje duży ruch, więc chciałem je zaktualizować. Z powodu szaleństwa zmian w kandydatach do wersji Alpha, Beta i 7 RC, przestałem aktualizować odpowiedzi SO, dopóki nie ustabilizowały się.

Jest to idealny przypadek do używania Subjects i ReplaySubjects

Ja osobiście wolę używać ReplaySubject(1), ponieważ pozwala ostatnia wartość przechowywane mają być przekazane, gdy nowi abonenci dołączyć nawet jeśli późno:

let project = new ReplaySubject(1);

//subscribe
project.subscribe(result => console.log('Subscription Streaming:', result));

http.get('path/to/whatever/projects/1234').subscribe(result => {
    //push onto subject
    project.next(result));

    //add delayed subscription AFTER loaded
    setTimeout(()=> project.subscribe(result => console.log('Delayed Stream:', result)), 3000);
});

//Output
//Subscription Streaming: 1234
//*After load and delay*
//Delayed Stream: 1234

Więc nawet jeśli połączę się późno lub muszę załadować później, zawsze mogę uzyskać najnowsze połączenie i nie martwić się, że przegapię oddzwonienie.

Umożliwia to również użycie tego samego strumienia do skierowania w dół:

project.next(5678);
//output
//Subscription Streaming: 5678

Ale co, jeśli masz 100% pewności, że telefon wystarczy wykonać tylko raz? Pozostawianie otwartych tematów i obserwowalnych nie jest dobre, ale zawsze pojawia się pytanie „Co jeśli?”

W tym miejscu pojawia się AsyncSubject .

let project = new AsyncSubject();

//subscribe
project.subscribe(result => console.log('Subscription Streaming:', result),
                  err => console.log(err),
                  () => console.log('Completed'));

http.get('path/to/whatever/projects/1234').subscribe(result => {
    //push onto subject and complete
    project.next(result));
    project.complete();

    //add a subscription even though completed
    setTimeout(() => project.subscribe(project => console.log('Delayed Sub:', project)), 2000);
});

//Output
//Subscription Streaming: 1234
//Completed
//*After delay and completed*
//Delayed Sub: 1234

Niesamowite! Mimo że zamknęliśmy temat, nadal odpowiadał, podając ostatnią załadowaną rzecz.

Inną rzeczą jest to, jak zasubskrybowaliśmy to wywołanie http i obsłużyliśmy odpowiedź. Mapa świetnie nadaje się do przetwarzania odpowiedzi.

public call = http.get(whatever).map(res => res.json())

Ale co by było, gdybyśmy musieli zagnieździć te połączenia? Tak, możesz użyć przedmiotów ze specjalną funkcją:

getThing() {
    resultSubject = new ReplaySubject(1);

    http.get('path').subscribe(result1 => {
        http.get('other/path/' + result1).get.subscribe(response2 => {
            http.get('another/' + response2).subscribe(res3 => resultSubject.next(res3))
        })
    })
    return resultSubject;
}
var myThing = getThing();

Ale to dużo i oznacza, że ​​potrzebujesz do tego funkcji. Wpisz FlatMap :

var myThing = http.get('path').flatMap(result1 => 
                    http.get('other/' + result1).flatMap(response2 => 
                        http.get('another/' + response2)));

Świetnie, varjest to obserwowalne, które pobiera dane z końcowego wywołania http.

OK, to świetnie, ale chcę usługę angular2!

Mam cię:

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { ReplaySubject } from 'rxjs';

@Injectable()
export class ProjectService {

  public activeProject:ReplaySubject<any> = new ReplaySubject(1);

  constructor(private http: Http) {}

  //load the project
  public load(projectId) {
    console.log('Loading Project:' + projectId, Date.now());
    this.http.get('/projects/' + projectId).subscribe(res => this.activeProject.next(res));
    return this.activeProject;
  }

 }

 //component

@Component({
    selector: 'nav',
    template: `<div>{{project?.name}}<a (click)="load('1234')">Load 1234</a></div>`
})
 export class navComponent implements OnInit {
    public project:any;

    constructor(private projectService:ProjectService) {}

    ngOnInit() {
        this.projectService.activeProject.subscribe(active => this.project = active);
    }

    public load(projectId:string) {
        this.projectService.load(projectId);
    }

 }

Jestem wielkim fanem obserwatorów i obserwatorów, więc mam nadzieję, że ta aktualizacja pomoże!

Oryginalna odpowiedź

Myślę, że jest to przypadek użycia korzystania z obserwowalnych Temat lub w .Angular2EventEmitter

W swojej usłudze tworzysz obiekt, EventEmitterktóry pozwala na wpychanie do niego wartości. W Alpha 45 musisz go przekonwertować toRx(), ale wiem, że pracowali nad pozbyciem się tego, więc w Alpha 46 możesz po prostu zwrócić plik EvenEmitter.

class EventService {
  _emitter: EventEmitter = new EventEmitter();
  rxEmitter: any;
  constructor() {
    this.rxEmitter = this._emitter.toRx();
  }
  doSomething(data){
    this.rxEmitter.next(data);
  }
}

W ten sposób jest to jedno, do EventEmitterktórego mogą teraz kierować różne funkcje usługowe.

Jeśli chcesz zwrócić obserwowalny bezpośrednio z połączenia, możesz zrobić coś takiego:

myHttpCall(path) {
    return Observable.create(observer => {
        http.get(path).map(res => res.json()).subscribe((result) => {
            //do something with result. 
            var newResultArray = mySpecialArrayFunction(result);
            observer.next(newResultArray);
            //call complete if you want to close this stream (like a promise)
            observer.complete();
        });
    });
}

To pozwoliłoby ci to zrobić w komponencie: peopleService.myHttpCall('path').subscribe(people => this.people = people);

I bałagan z wynikami rozmowy w twojej usłudze.

Lubię EventEmittersamodzielnie tworzyć strumień na wypadek, gdyby potrzebował uzyskać do niego dostęp z innych komponentów, ale widziałem działanie obu sposobów ...

Oto plunker, który pokazuje podstawową usługę z emiterem zdarzeń: Plunkr

Dennis Smolek
źródło
Wypróbowałem to podejście, ale otrzymałem „Nie można użyć„ nowego ”z wyrażeniem, którego typ nie zawiera sygnatury wywołania lub konstrukcji” -error. Czy ktoś ma pomysł, co robić?
Spock
3
@Spock Wydaje się, że specyfikacja została zaktualizowana od czasu tego pierwotnego pytania. Nie potrzebujesz już „nowego” dla tego, co obserwowalne, ponieważ robi to dla ciebie. Po prostu usuń nowe i daj mi znać, co się stanie. Mam teraz trochę rzeczy, jeśli to zadziała również dla ciebie, zaktualizuję tę odpowiedź
Dennis Smolek
1
Używanie EventEmitterdo niczego, ale @Output()jest odradzane. Zobacz także stackoverflow.com/questions/34376854/…
Günter Zöchbauer
@ GünterZöchbauer, tak, teraz ... W tamtym czasie miało to być EventEmitters, ale od tego czasu ustandaryzowali się na Rx Observables. Mój obserwowalny przykład nadal działa, ale jeśli zamierzasz użyć przykładu EventEmitter, podałem propozycję bezpośredniego użycia Subjects: github.com/Reactive-Extensions/RxJS/blob/master/doc/api/ ...
Dennis Smolek
1
@maxisam Dzięki za edycję, chociaż odpowiedź brzmiała / jest w stosunku do alfy, usunięcie "nowego" dla Observable jest teraz poprawne
Dennis Smolek
30

To jest przykład z dokumentacji Angular2, w jaki sposób możesz tworzyć i używać własnych Observables:

Serwis

import {Injectable} from 'angular2/core'
import {Subject}    from 'rxjs/Subject';
@Injectable()
export class MissionService {
  private _missionAnnouncedSource = new Subject<string>();
  missionAnnounced$ = this._missionAnnouncedSource.asObservable();

  announceMission(mission: string) {
    this._missionAnnouncedSource.next(mission)
  }
}

Składnik

    import {Component}          from 'angular2/core';
    import {MissionService}     from './mission.service';

    export class MissionControlComponent {
      mission: string;

      constructor(private missionService: MissionService) {

        missionService.missionAnnounced$.subscribe(
          mission => {
            this.mission = mission;
          })
      }

      announce() {
        this.missionService.announceMission('some mission name');
      }
    }

Pełny i działający przykład można znaleźć tutaj: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

tibbus
źródło
18

Dodam, że jeśli tworzony obiekt jest statyczny i nie przechodzi przez http, to można to zrobić:

public fetchModel(uuid: string = undefined): Observable<string> {
      if(!uuid) { //static data
        return Observable.of(new TestModel()).map(o => JSON.stringify(o));
      }
      else {
        return this.http.get("http://localhost:8080/myapp/api/model/" + uuid)
                .map(res => res.text());
      }
    }

Edycja: w przypadku mapowania Angular 7.xx należy wykonać za pomocą pipe (), jak opisano tutaj ( https://stackoverflow.com/a/54085359/986160 ):

import {of,  Observable } from 'rxjs';
import { map } from 'rxjs/operators';
[...]
public fetchModel(uuid: string = undefined): Observable<string> {
      if(!uuid) { //static data
        return of(new TestModel());
      }
      else {
        return this.http.get("http://localhost:8080/myapp/api/model/" + uuid)
                .pipe(map((res:any) => res)) //already contains json
      }
    }

z odpowiedzi na moje pytanie dotyczące obserwatorów i danych statycznych: https://stackoverflow.com/a/35219772/986160

Michail Michailidis
źródło
17

Trochę się spóźniłem na imprezę, ale myślę, że moje podejście ma tę zaletę, że brakuje w nim użycia EventEmitterów i Subjects.

Oto moje podejście. Nie możemy uciec od subscribe () i nie chcemy. W tym duchu nasza usługa zwróci Observable<T>obserwatora, który ma nasz cenny ładunek. Od wywołującego zainicjujemy zmienną, Observable<T>która otrzyma wartość usługi Observable<T>. Następnie zasubskrybujemy ten obiekt. Wreszcie otrzymujesz swoje „T”! z Twojej usługi.

Po pierwsze, nasi ludzie obsługują, ale Twój nie przekazuje parametrów, to bardziej realistyczne:

people(hairColor: string): Observable<People> {
   this.url = "api/" + hairColor + "/people.json";

   return Observable.create(observer => {
      http.get(this.url)
          .map(res => res.json())
          .subscribe((data) => {
             this._people = data

             observer.next(this._people);
             observer.complete();


          });
   });
}

Ok, jak widać, zwracamy Observabletyp „ludzie”. Podpis metody nawet tak mówi! Wsuwamy _peopleobiekt do naszego obserwatora. Następnie uzyskamy dostęp do tego typu od naszego wywołującego w komponencie!

W komponencie:

private _peopleObservable: Observable<people>;

constructor(private peopleService: PeopleService){}

getPeople(hairColor:string) {
   this._peopleObservable = this.peopleService.people(hairColor);

   this._peopleObservable.subscribe((data) => {
      this.people = data;
   });
}

Inicjalizujemy nasze _peopleObservable, zwracając to Observable<people>z naszego PeopleService. Następnie subskrybujemy tę właściwość. Na koniec ustawiliśmy this.peoplenaszą odpowiedź data ( people).

Architektura usługi w ten sposób ma jedną, zasadniczą przewagę nad typową usługą: mapa (...) i komponent: wzorzec „subskrybuj (...)”. W prawdziwym świecie musimy mapować json do naszych właściwości w naszej klasie i czasami robimy tam niestandardowe rzeczy. Więc to mapowanie może wystąpić w naszej usłudze. I zazwyczaj dlatego, że nasze zgłoszenie serwisowe zostanie użyte nie raz, ale prawdopodobnie w innych miejscach naszego kodu nie musimy ponownie wykonywać tego mapowania w jakimś komponencie. Co więcej, co jeśli dodamy nowe pole do ludzi? ....

Duży jamnik
źródło
Zgadzam się, że formatowanie powinno być w usłudze i opublikowałem również standardową metodę Observable, ale zaletą Subjects w usłudze jest to, że inne funkcje mogą się na niej wyzwalać. Jeśli zawsze potrzebujesz tylko bezpośredniego wywołania http, użyłbym metody Observable ..
Dennis Smolek
11

W pliku service.ts -

za. import „of” from obsable / of
b. utworzyć listę json
c. zwraca obiekt json przy użyciu Observable.of ()
Np. -

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';

@Injectable()
export class ClientListService {
    private clientList;

    constructor() {
        this.clientList = [
            {name: 'abc', address: 'Railpar'},
            {name: 'def', address: 'Railpar 2'},
            {name: 'ghi', address: 'Panagarh'},
            {name: 'jkl', address: 'Panagarh 2'},
        ];
    }

    getClientList () {
        return Observable.of(this.clientList);
    }
};

W komponencie, w którym wywołujemy funkcję get usługi -

this.clientListService.getClientList().subscribe(res => this.clientList = res);
Anirban Bhadra
źródło
Dobra robota @Anirban, może również zwrócić tylko (this.clientList);
foo-baar
7

Zauważ, że używasz Observable # map do konwersji surowego Responseobiektu, który emituje Twoja podstawowa Observable, na przeanalizowaną reprezentację odpowiedzi JSON.

Jeśli dobrze cię zrozumiałem, mapznowu chcesz . Ale tym razem konwertowanie tego surowego JSON na wystąpienia twojego Model. Więc zrobiłbyś coś takiego:

http.get('api/people.json')
  .map(res => res.json())
  .map(peopleData => peopleData.map(personData => new Person(personData)))

Zacząłeś więc od Observable, który emituje Responseobiekt, przekształcił go w obserwowalny, który emituje obiekt przeanalizowanego kodu JSON tej odpowiedzi, a następnie przekształcił go w kolejny obserwowalny, który przekształcił ten surowy JSON w tablicę twoich modeli.

julioolvr
źródło