Erstellen und Zurückgeben von Observable aus dem Angular 2-Dienst

132

Dies ist eher eine "Best Practices" -Frage. Es gibt drei Spieler: a Component, a Serviceund a Model. Das Componentruft das Serviceauf, um Daten aus einer Datenbank abzurufen. Das Serviceverwendet:

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

ein Observable.

Das Componentkönnte einfach abonnieren Observable:

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

Was ich jedoch wirklich möchte, ist Service, dass ein Array of ModelObjekt zurückgegeben wird, das aus den Daten erstellt wurde, die Serviceaus der Datenbank abgerufen wurden. Ich erkannte, dass das Componentnur dieses Array in der Subscribe-Methode erstellen könnte, aber ich denke, es wäre sauberer, wenn der Dienst dies tun und es dem zur Verfügung stellen würde Component.

Wie kann das Serviceein neues ObservableArray erstellen , das dieses Array enthält, und das zurückgeben?

Joseph Genchik
quelle

Antworten:

158

UPDATE: 24.09.16 Angular 2.0 Stable

Diese Frage hat immer noch viel Verkehr, deshalb wollte ich sie aktualisieren. Mit dem Wahnsinn der Änderungen von Alpha, Beta und 7 RC-Kandidaten hörte ich auf, meine SO-Antworten zu aktualisieren, bis sie stabil waren.

Dies ist der perfekte Fall für die Verwendung von Subjects und ReplaySubjects

Ich persönlich bevorzuge die Verwendung, ReplaySubject(1)da dadurch der zuletzt gespeicherte Wert übergeben werden kann, wenn neue Abonnenten auch dann zu spät anhängen:

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

Selbst wenn ich zu spät anhänge oder später laden muss, kann ich immer den neuesten Anruf erhalten und muss mir keine Sorgen machen, dass der Rückruf verpasst wird.

Auf diese Weise können Sie auch denselben Stream verwenden, um auf Folgendes zuzugreifen:

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

Aber was ist, wenn Sie zu 100% sicher sind, dass Sie den Anruf nur einmal tätigen müssen? Offene Themen und Observablen zu lassen ist nicht gut, aber es gibt immer das "Was wäre wenn?"

Das ist , wo AsyncSubject kommt.

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

Genial! Obwohl wir das Thema geschlossen haben, antwortete es immer noch mit dem letzten geladenen Element.

Eine andere Sache ist, wie wir diesen http-Aufruf abonniert und die Antwort verarbeitet haben. Map ist großartig, um die Antwort zu verarbeiten.

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

Aber was wäre, wenn wir diese Anrufe verschachteln müssten? Ja, Sie könnten Themen mit einer speziellen Funktion verwenden:

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();

Aber das ist viel und bedeutet, dass Sie eine Funktion benötigen, um dies zu tun. Geben Sie FlatMap ein :

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

Süß, das varist ein Observable, das die Daten vom letzten http-Aufruf erhält.

OK, das ist großartig, aber ich möchte einen Angular2-Service!

Ich habe dich:

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);
    }

 }

Ich bin ein großer Fan von Beobachtern und Observablen, also hoffe ich, dass dieses Update hilft!

Ursprüngliche Antwort

Ich denke, dies ist ein Anwendungsfall für die Verwendung eines beobachtbaren Subjekts oder in Angular2der EventEmitter.

In Ihrem Dienst erstellen Sie eine EventEmitter, mit der Sie Werte darauf übertragen können. In Alpha 45 müssen Sie es konvertieren toRx(), aber ich weiß, dass sie daran gearbeitet haben, dies zu beseitigen. In Alpha 46 können Sie das möglicherweise einfach zurückgeben EvenEmitter.

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

Auf diese Weise haben Sie die Single EventEmitter, auf die Ihre verschiedenen Servicefunktionen jetzt zugreifen können.

Wenn Sie ein Observable direkt von einem Anruf zurückgeben möchten, können Sie Folgendes tun:

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();
        });
    });
}

Damit können Sie dies in der Komponente tun: peopleService.myHttpCall('path').subscribe(people => this.people = people);

Und verwirren Sie die Ergebnisse des Anrufs in Ihrem Dienst.

Ich mag es, den EventEmitterStream selbst zu erstellen, falls ich von anderen Komponenten aus darauf zugreifen muss, aber ich konnte sehen, dass beide Möglichkeiten funktionieren ...

Hier ist ein Plunker, der einen Basisdienst mit einem Ereignisemitter zeigt: Plunkr

Dennis Smolek
quelle
Ich habe diesen Ansatz ausprobiert, aber den Fehler "Kann 'new' nicht mit einem Ausdruck verwenden, dessen Typ keinen Aufruf oder keine Konstruktionssignatur enthält" erhalten. Hat jemand eine Idee was zu tun ist?
Spock
3
@Spock Die Spezifikation scheint seit dieser ursprünglichen Frage aktualisiert zu werden. Sie brauchen das "Neue" für das Beobachtbare nicht mehr, wie es dies für Sie tut. Entfernen Sie einfach das neue und lassen Sie mich wissen, was passiert. Ich spiele jetzt mit ein paar Sachen, wenn es auch für dich funktioniert, werde ich diese Antwort aktualisieren
Dennis Smolek
1
Verwenden EventEmitterfür alles andere @Output()als entmutigt. Siehe auch stackoverflow.com/questions/34376854/…
Günter Zöchbauer
@ GünterZöchbauer, Ja, das ist es jetzt ... Zu der Zeit waren es überall EventEmitter, aber seitdem sind sie auf Rx Observables standardisiert. Mein Observable-Beispiel funktioniert immer noch, aber wenn Sie das von mir angegebene EventEmitter-Beispiel verwenden möchten, schlage ich vor, Subjects direkt zu verwenden: github.com/Reactive-Extensions/RxJS/blob/master/doc/api/…
Dennis Smolek
1
@maxisam Vielen Dank für die Bearbeitung, obwohl die Antwort relativ zum Alpha war / ist, ist das Entfernen von "neu" für das Observable jetzt korrekt
Dennis Smolek
29

Dies ist ein Beispiel aus Angular2-Dokumenten, wie Sie Ihre eigenen Observables erstellen und verwenden können:

Der Service

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)
  }
}

Die Komponente

    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');
      }
    }

Das vollständige und funktionierende Beispiel finden Sie hier: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

Tibbus
quelle
18

Ich möchte hinzufügen, dass, wenn das erstellte Objekt statisch ist und nicht über http kommt, so etwas getan werden kann:

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());
      }
    }

Bearbeiten: Für Angular 7.xx muss die Zuordnung mit pipe () erfolgen, wie hier beschrieben ( 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
      }
    }

Von der Antwort auf meine Frage zu Beobachtern und statischen Daten: https://stackoverflow.com/a/35219772/986160

Michail Michailidis
quelle
17

Ich bin etwas spät zur Party, aber ich denke, mein Ansatz hat den Vorteil, dass EventEmitters und Subjects nicht verwendet werden.

Also, hier ist mein Ansatz. Wir kommen nicht von subscribe () weg und wollen es auch nicht. In diesem Sinne wird unser Service Observable<T>mit einem Beobachter zurückkehren, der unsere kostbare Fracht hat. Vom Aufrufer aus initialisieren wir eine Variable Observable<T>und sie erhält die Dienste Observable<T>. Als nächstes abonnieren wir dieses Objekt. Endlich bekommst du dein "T"! von Ihrem Dienst.

Erstens, unser Personaldienst, aber Ihr Mitarbeiter gibt keine Parameter weiter, das ist realistischer:

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, wie Sie sehen können, geben wir einen ObservableTyp "Menschen" zurück. Die Signatur der Methode sagt es sogar! Wir stecken das _peopleObjekt in unseren Beobachter. Wir werden als nächstes von unserem Anrufer in der Komponente auf diesen Typ zugreifen!

In der Komponente:

private _peopleObservable: Observable<people>;

constructor(private peopleService: PeopleService){}

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

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

Wir initialisieren unsere, _peopleObservableindem wir das Observable<people>von unserer zurückgeben PeopleService. Dann abonnieren wir diese Eigenschaft. Schließlich setzen wir this.peopleauf unsere data ( people) Antwort.

Die Architektur des Dienstes auf diese Weise hat einen großen Vorteil gegenüber dem typischen Dienst: map (...) und component: "subscribe (...)" - Muster. In der realen Welt müssen wir den JSON unseren Eigenschaften in unserer Klasse zuordnen, und manchmal machen wir dort einige benutzerdefinierte Dinge. Diese Zuordnung kann also in unserem Service erfolgen. Und normalerweise müssen wir diese Zuordnung in einigen Komponenten nicht erneut durchführen, da unser Serviceabruf nicht einmal verwendet wird, sondern wahrscheinlich an anderen Stellen in unserem Code. Was ist außerdem, wenn wir den Menschen ein neues Feld hinzufügen?

Großer Dackel
quelle
Ich bin damit einverstanden, dass die Formatierung im Dienst enthalten sein sollte, und habe auch eine Standard-Observable-Methode veröffentlicht. Der Vorteil von Subjects in einem Dienst besteht jedoch darin, dass andere Funktionen darauf ausgelöst werden können. Wenn Sie immer nur den direkten http-Aufruf benötigen, würde ich die Observable-Methode verwenden.
Dennis Smolek
9

In der Datei service.ts -

ein. Import 'von' aus beobachtbar / von
b. Erstellen einer JSON-Liste
c. Geben Sie das json-Objekt mit Observable.of () zurück
. - -

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);
    }
};

In der Komponente, in der wir die get-Funktion des Dienstes aufrufen -

this.clientListService.getClientList().subscribe(res => this.clientList = res);
Anirban Bhadra
quelle
Gute Arbeit @Anirban, konnte auch nur von (this.clientList) zurückkehren;
Foo-Baar
7

Beachten Sie, dass Sie Observable # map verwenden , um das von ResponseIhrer Basis-Observable ausgegebene Rohobjekt in eine analysierte Darstellung der JSON-Antwort zu konvertieren .

Wenn ich dich richtig verstanden habe, willst du es mapwieder. Aber dieses Mal konvertieren Sie diesen rohen JSON in Instanzen von Ihnen Model. Sie würden also so etwas tun wie:

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

Sie haben also mit einer Observable begonnen, die ein ResponseObjekt ausgibt, diese in eine Observable umgewandelt, die ein Objekt des analysierten JSON dieser Antwort ausgibt, und diese dann in eine weitere Observable umgewandelt, die diese rohe JSON in ein Array Ihrer Modelle verwandelt hat.

julioolvr
quelle