Créer et renvoyer Observable à partir du service Angular 2

132

C'est plus une question de «meilleures pratiques». Il y a trois joueurs: a Component, a Serviceet a Model. Le Componentappelle le Servicepour obtenir des données d'une base de données. Le Serviceutilise:

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

pour retourner un fichier Observable.

Le Componentpourrait simplement s'abonner au Observable:

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

Cependant, ce que je veux vraiment, c'est que le Serviceretourne un Array of Modelobjet qui a été créé à partir des données Servicerécupérées dans la base de données. J'ai réalisé que le Componentpouvait simplement créer ce tableau dans la méthode d'abonnement, mais je pense que ce serait plus propre si le service le faisait et le rendait disponible pour le Component.

Comment Servicecréer un nouveau Observable, contenant ce tableau, et le renvoyer?

Joseph Genchik
la source

Réponses:

159

MISE À JOUR: 9/24/16 Angular 2.0 Stable

Cette question génère encore beaucoup de trafic, alors je voulais la mettre à jour. Avec la folie des changements apportés par les candidats Alpha, Beta et 7 RC, j'ai arrêté de mettre à jour mes réponses SO jusqu'à ce qu'elles se stabilisent.

C'est le cas parfait pour utiliser des sujets et des replaySubjects

Personnellement, je préfère utiliser ReplaySubject(1)car il permet à la dernière valeur stockée d'être transmise lorsque de nouveaux abonnés se connectent même en retard:

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

Ainsi, même si je me connecte en retard ou que je dois charger plus tard, je peux toujours recevoir le dernier appel et ne pas craindre de manquer le rappel.

Cela vous permet également d'utiliser le même flux pour pousser vers le bas:

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

Mais que faire si vous êtes sûr à 100% que vous n'avez besoin de faire l'appel qu'une seule fois? Laisser les sujets ouverts et les observables n'est pas bon mais il y a toujours ce "Et si?"

C'est là qu'AsyncSubject entre en jeu.

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

Impressionnant! Même si nous avons fermé le sujet, il a quand même répondu avec la dernière chose chargée.

Une autre chose est la façon dont nous nous sommes abonnés à cet appel http et avons géré la réponse. La carte est idéale pour traiter la réponse.

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

Mais que se passerait-il si nous devions imbriquer ces appels? Oui, vous pouvez utiliser des sujets avec une fonction spéciale:

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

Mais c'est beaucoup et cela signifie que vous avez besoin d'une fonction pour le faire. Entrez FlatMap :

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

Sweet, le varest un observable qui obtient les données de l'appel http final.

OK c'est super mais je veux un service angular2!

Je vous ai compris:

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

 }

Je suis un grand fan d'observateurs et d'observables alors j'espère que cette mise à jour vous aidera!

Réponse originale

Je pense que c'est un cas d'utilisation d'utilisation d'un sujet observable ou dans Angular2leEventEmitter .

Dans votre service, vous créez un EventEmitterqui vous permet d'y pousser des valeurs. Dans Alpha 45, vous devez le convertir avec toRx(), mais je sais qu'ils travaillaient pour s'en débarrasser, donc dans Alpha 46, vous pourrez peut-être simplement renvoyer le fichier EvenEmitter.

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

De cette façon, EventEmittervos différentes fonctions de service peuvent désormais pousser.

Si vous souhaitez renvoyer une observable directement à partir d'un appel, vous pouvez faire quelque chose comme ceci:

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

Cela vous permettrait de faire cela dans le composant: peopleService.myHttpCall('path').subscribe(people => this.people = people);

Et gâchez les résultats de l'appel dans votre service.

J'aime créer le EventEmitterflux seul au cas où j'aurais besoin d'y accéder à partir d'autres composants, mais je pouvais voir les deux façons fonctionner ...

Voici un plunker qui montre un service de base avec un émetteur d'événements: Plunkr

Dennis Smolek
la source
J'ai essayé cette approche mais j'ai obtenu "Impossible d'utiliser 'new' avec une expression dont le type n'a pas d'appel ou de signature de construction" -error. Quelqu'un a une idée de ce qu'il faut faire?
Spock
3
@Spock la spécification semble se mettre à jour depuis cette question originale. Vous n'avez plus besoin du «nouveau» pour l'observable car il le fait pour vous. Supprimez simplement le nouveau et dites-moi ce qui se passe. Je joue avec des trucs maintenant, si cela fonctionne pour vous aussi, je mettrai à jour cette réponse
Dennis Smolek
1
Utiliser EventEmitterpour n'importe quoi mais @Output()est déconseillé. Voir aussi stackoverflow.com/questions/34376854/…
Günter Zöchbauer
@ GünterZöchbauer, Oui, c'est maintenant ... À l'époque, ça allait être EventEmitters partout mais ils ont depuis standardisé sur Rx Observables. Mon exemple Observable fonctionne toujours mais si vous deviez utiliser l'exemple EventEmitter que j'ai donné, je suggère d'utiliser Subjects directement: github.com/Reactive-Extensions/RxJS/blob/master/doc/api/…
Dennis Smolek
1
@maxisam Merci pour la modification, même si la réponse était / est relative à l'Alpha, la suppression du "nouveau" pour l'Observable est correcte maintenant
Dennis Smolek
29

Voici un exemple tiré de la documentation Angular2 de la façon dont vous pouvez créer et utiliser vos propres observables:

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

Le composant

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

Un exemple complet et fonctionnel peut être trouvé ici: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

tibbus
la source
18

Je voudrais ajouter que si l'objet créé est statique et ne passe pas par http, quelque chose comme ça peut être fait:

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

Edit: Pour Angular 7.xx, le mappage doit être effectué à l'aide de pipe () comme décrit ici ( 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
      }
    }

de la réponse à ma question sur les observateurs et les données statiques: https://stackoverflow.com/a/35219772/986160

Michail Michailidis
la source
17

Je suis un peu en retard à la fête, mais je pense que mon approche a l'avantage de ne pas utiliser EventEmitters et Subjects.

Alors, voici mon approche. Nous ne pouvons pas nous passer de subscribe (), et nous ne voulons pas. Dans cette veine, notre service retournera un Observable<T>avec un observateur qui a notre précieuse cargaison. De l'appelant, nous initialiserons une variable Observable<T>, et il obtiendra le serviceObservable<T> . Ensuite, nous nous abonnerons à cet objet. Enfin, vous obtenez votre "T"! de votre service.

Premièrement, notre service aux personnes, mais le vôtre ne passe pas de paramètres, c'est plus réaliste:

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, comme vous pouvez le voir, nous retournons un Observabletype "personnes". La signature de la méthode le dit même! Nous rentrons l' _peopleobjet dans notre observateur. Nous accèderons à ce type depuis notre appelant dans le composant, ensuite!

Dans le composant:

private _peopleObservable: Observable<people>;

constructor(private peopleService: PeopleService){}

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

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

Nous initialisons notre _peopleObservableen renvoyant celui Observable<people>de notre PeopleService. Ensuite, nous souscrivons à cette propriété. Enfin, nous définissons this.peoplenotre peopleréponse data ( ).

L'architecture du service de cette manière présente un avantage majeur par rapport au service typique: la carte (...) et le composant: le modèle "s'abonner (...)". Dans le monde réel, nous devons mapper le json à nos propriétés dans notre classe et, parfois, nous y faisons des choses personnalisées. Cette cartographie peut donc se produire dans notre service. Et, généralement, parce que notre appel de service ne sera pas utilisé une fois, mais probablement à d'autres endroits de notre code, nous n'avons pas à effectuer ce mappage dans certains composants, à nouveau. De plus, que se passerait-il si nous ajoutions un nouveau champ aux personnes? ....

GrandTeckel
la source
Je suis d'accord que le formatage doit être dans le service et j'ai également posté une méthode Observable standard mais l'avantage des sujets dans un service est que d'autres fonctions peuvent se déclencher dessus. Si vous n'avez toujours besoin que de l'appel http direct, j'utiliserais la méthode Observable ..
Dennis Smolek
9

Dans le fichier service.ts -

une. importation «de» à partir d'observable / de
b. créer une liste json
c. retourne l'objet json en utilisant Observable.of ()
Ex. -

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

Dans le composant où nous appelons la fonction get du service -

this.clientListService.getClientList().subscribe(res => this.clientList = res);
Anirban Bhadra
la source
Bon travail @Anirban, ne pouvait aussi retourner que (this.clientList);
foo-baar
7

Notez que vous utilisez Observable # map pour convertir l' Responseobjet brut émis par votre Observable de base en une représentation analysée de la réponse JSON.

Si je vous ai bien compris, vous voulez mapencore. Mais cette fois, convertir ce JSON brut en instances de votre Model. Donc vous feriez quelque chose comme:

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

Donc, vous avez commencé avec un observable qui émet un Responseobjet , vous l'avez transformé en observable qui émet un objet du JSON analysé de cette réponse, puis vous l'avez transformé en un autre observable qui a transformé ce JSON brut en un tableau de vos modèles.

julioolvr
la source