Creazione e restituzione di osservabile dal servizio Angular 2


132

Questa è più una domanda "best practice". Ci sono tre giocatori: a Component, a Servicee a Model. La Componentsta chiamando Serviceper ottenere dati da un database. Sta Serviceusando:

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

per restituire un Observable.

Il Componentpoteva semplicemente iscriversi al Observable:

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

Tuttavia, ciò che voglio davvero è Serviceche restituisca un Array of Modeloggetto creato dai dati Servicerecuperati dal database. Mi sono reso conto che Componentpotrebbe semplicemente creare questo array nel metodo di iscrizione, ma penso che sarebbe più pulito se il servizio lo facesse e lo rendesse disponibile al Component.

Come può Servicecreare un nuovo Observablecontenente quell'array e restituirlo?

Risposte:


159

AGGIORNAMENTO: 24/09/16 Angolare 2.0 stabile

Questa domanda ottiene ancora molto traffico, quindi volevo aggiornarla. Con la follia delle modifiche da Alpha, Beta e 7 candidati RC, ho smesso di aggiornare le mie risposte SO fino a quando non sono diventate stabili.

Questo è il caso perfetto per l'utilizzo di Soggetti e ReplaySubjects

Io personalmente preferisco usare ReplaySubject(1)in quanto consente l'ultimo valore memorizzato per essere passato quando i nuovi abbonati attaccano anche quando in ritardo:

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

Quindi, anche se allego in ritardo o devo caricare più tardi, posso sempre ricevere l'ultima chiamata e non preoccuparmi di perdere la richiamata.

Questo ti consente anche di utilizzare lo stesso flusso per spingere verso il basso su:

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

E se sei sicuro al 100%, che devi fare la chiamata una sola volta? Lasciare soggetti aperti e osservabili non va bene ma c'è sempre quel "What If?"

Ecco dove entra in gioco 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

Eccezionale! Anche se abbiamo chiuso l'argomento, ha comunque risposto con l'ultima cosa caricata.

Un'altra cosa è come ci siamo abbonati a quella chiamata http e gestito la risposta. La mappa è ottima per elaborare la risposta.

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

E se avessimo bisogno di annidare quelle chiamate? Sì, potresti usare soggetti con una funzione speciale:

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

Ma è molto e significa che hai bisogno di una funzione per farlo. Inserisci FlatMap :

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

Dolce, varè un osservabile che ottiene i dati dall'ultima chiamata http.

OK va benissimo ma voglio un servizio angular2!

Ti ho preso:

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

 }

Sono un grande fan di osservatori e osservabili, quindi spero che questo aggiornamento aiuti!

Risposta originale

Penso che questo sia un caso d'uso di utilizzo di un Soggetto Observable o Angular2la EventEmitter.

Nel tuo servizio crei un oggetto EventEmitterche ti consente di spingere i valori su di esso. In Alpha 45 devi convertirlo con toRx(), ma so che stavano lavorando per sbarazzarsene, quindi in Alpha 46 potresti essere in grado di restituire semplicemente il file EvenEmitter.

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

In questo modo c'è il singolo su EventEmittercui le diverse funzioni di servizio possono ora spingere.

Se volessi restituire un osservabile direttamente da una chiamata, potresti fare qualcosa del genere:

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

Ciò ti consentirebbe di farlo nel componente: peopleService.myHttpCall('path').subscribe(people => this.people = people);

E scherza con i risultati della chiamata nel tuo servizio.

Mi piace creare il EventEmitterflusso da solo nel caso avessi bisogno di accedervi da altri componenti, ma potrei vedere entrambi i modi di lavorare ...

Ecco un plunker che mostra un servizio di base con un emettitore di eventi: Plunkr


Ho provato questo approccio ma ho ottenuto "Impossibile usare 'nuovo' con un'espressione il cui tipo manca di una chiamata o costruisce la firma" -error. Qualcuno ha un'idea di cosa fare?
Spock,

3
@Spock sembra che le specifiche si siano aggiornate da questa domanda originale. Non hai più bisogno del "nuovo" per l'osservabile in quanto lo fa per te. Rimuovi semplicemente il nuovo e fammi sapere cosa succede. Sto scherzando con alcune cose ora, se funziona anche per te aggiornerò questa risposta
Dennis Smolek,

1
Utilizzando EventEmitterper qualsiasi cosa, ma @Output()è scoraggiato. Vedere anche stackoverflow.com/questions/34376854/...
Günter Zöchbauer

@ GünterZöchbauer, Sì, lo è adesso ... All'epoca sarebbe stato EventEmitters dappertutto ma da allora si sono standardizzati su Rx Observables. Il mio esempio osservabile funziona ancora, ma se avessi intenzione di usare l'esempio EventEmitter che ho dato, ti suggerisco di usare direttamente i Soggetti: github.com/Reactive-Extensions/RxJS/blob/master/doc/api/…
Dennis Smolek,

1
@maxisam Grazie per la modifica, anche se la risposta era / è relativa all'Alfa che rimuove "nuovo" per l'Osservabile ora è corretto
Dennis Smolek,

29

Questo è un esempio dei documenti di Angular2 su come è possibile creare e utilizzare i propri osservabili:

Il servizio

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

Il componente

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

Esempio completo e funzionante può essere trovato qui: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service


18

Vorrei aggiungere che se l'oggetto creato è statico e non proviene da http, si può fare qualcosa del genere:

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

Modifica: per Angular 7.xx è necessario eseguire la mappatura utilizzando pipe () come descritto qui ( 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
      }
    }

dalla risposta alla mia domanda su osservatori e dati statici: https://stackoverflow.com/a/35219772/986160


17

Sono un po 'in ritardo alla festa, ma penso che il mio approccio abbia il vantaggio che manca l'uso di EventEmitters e soggetti.

Quindi, ecco il mio approccio. Non possiamo allontanarci da iscriviti () e non vogliamo. In tal senso, il nostro servizio restituirà un Observable<T>con un osservatore che ha il nostro prezioso carico. Dal chiamante inizializzeremo una variabile Observable<T>e otterrà il servizio Observable<T>. Successivamente, ci iscriveremo a questo oggetto. Finalmente ottieni la tua "T"! dal vostro servizio.

Innanzitutto, il nostro servizio clienti, ma il tuo non passa i parametri, è più realistico:

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, come puoi vedere, stiamo restituendo un Observabletipo di "persone". La firma del metodo, lo dice anche! Inseriamo l' _peopleoggetto nel nostro osservatore. Accederemo a questo tipo dal nostro chiamante nel Componente, il prossimo!

Nel componente:

private _peopleObservable: Observable<people>;

constructor(private peopleService: PeopleService){}

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

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

Inizializziamo il nostro _peopleObservablerestituendolo Observable<people>dal nostro PeopleService. Quindi, ci iscriviamo a questa proprietà. Infine, abbiamo impostato la this.peoplenostra peoplerisposta data ( ).

L'architettura del servizio in questo modo ha un grande vantaggio rispetto al servizio tipico: mappa (...) e componente: modello "iscriviti (...)". Nel mondo reale, dobbiamo mappare il json alle nostre proprietà nella nostra classe e, a volte, facciamo alcune cose personalizzate lì. Quindi questa mappatura può avvenire nel nostro servizio. E, in genere, poiché la nostra chiamata di servizio verrà utilizzata non una volta, ma, probabilmente, in altri punti del nostro codice, non è necessario eseguire nuovamente tale mapping in alcuni componenti. Inoltre, se aggiungessimo un nuovo campo alle persone? ....


Sono d'accordo che la formattazione dovrebbe essere nel servizio e ho pubblicato anche un metodo osservabile standard, ma il vantaggio dei soggetti in un servizio è che altre funzioni possono attivarsi su di esso. Se hai sempre bisogno solo della chiamata http diretta, allora userei il metodo Osservabile ..
Dennis Smolek,

9

Nel file service.ts -

un. import "of" da osservabile / di
b. creare un elenco json
c. restituisce l'oggetto json usando Observable.of ()
Es. -

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

Nel componente in cui stiamo chiamando la funzione get del servizio -

this.clientListService.getClientList().subscribe(res => this.clientList = res);

Buon lavoro @Anirban, potrebbe anche restituire solo (this.clientList);
foo-baar

7

Nota che stai usando la mappa # osservabile per convertire l' Responseoggetto grezzo che la tua osservabile di base emette in una rappresentazione analizzata della risposta JSON.

Se ti ho capito bene, lo vuoi di mapnuovo. Ma questa volta, convertendo quel JSON grezzo in istanze del tuo Model. Quindi faresti qualcosa del tipo:

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

Quindi, hai iniziato con un osservabile che emette un Responseoggetto , lo hai trasformato in un osservabile che emette un oggetto del JSON analizzato di quella risposta, e poi lo hai trasformato in un altro osservabile che ha trasformato quel JSON grezzo in una matrice dei tuoi modelli.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.