Crear y devolver Observable desde el Servicio Angular 2

132

Esta es más una pregunta de "mejores prácticas". Hay tres jugadores: un Component, una Servicey una Model. El Componentestá llamando al Servicepara obtener datos de una base de datos. El Serviceestá usando:

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

para devolver un Observable.

El Componentpodría simplemente suscribirse a Observable:

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

Sin embargo, lo que realmente quiero es Serviceque devuelva un Array of Modelobjeto creado a partir de los datos que se Servicerecuperaron de la base de datos. Me di cuenta de que Componentpodría crear esta matriz en el método de suscripción, pero creo que sería más limpio si el servicio lo hace y lo pone a disposición del Component.

¿Cómo puede Servicecrear una nueva Observableque contenga esa matriz y devolverla?

Joseph Genchik
fuente

Respuestas:

159

ACTUALIZACIÓN: 9/24/16 Angular 2.0 Estable

Esta pregunta todavía tiene mucho tráfico, así que quería actualizarla. Con la locura de los cambios de Alpha, Beta y 7 candidatos RC, dejé de actualizar mis respuestas SO hasta que se estabilizaron.

Este es el caso perfecto para usar Sujetos y ReplaySubjects

Yo personalmente prefiero usar ReplaySubject(1)ya que permite que el último valor almacenado que se pasa cuando se unen nuevos suscriptores incluso cuando tarde:

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

Entonces, incluso si adjunto tarde o necesito cargar más tarde, siempre puedo recibir la última llamada y no preocuparme por perder la devolución de llamada.

Esto también le permite usar la misma secuencia para presionar hacia abajo:

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

Pero, ¿qué sucede si está 100% seguro de que solo necesita hacer la llamada una vez? Dejar temas abiertos y observables no es bueno, pero siempre está ese "¿Y si?"

Ahí es donde entra 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

¡Increíble! Aunque cerramos el tema, todavía respondió con lo último que cargó.

Otra cosa es cómo nos suscribimos a esa llamada http y manejamos la respuesta. El mapa es excelente para procesar la respuesta.

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

Pero, ¿y si necesitáramos anidar esas llamadas? Sí, podría usar sujetos con una función especial:

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

Pero eso es mucho y significa que necesita una función para hacerlo. Ingrese FlatMap :

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

Dulce, vares un observable que obtiene los datos de la última llamada http.

OK, eso es genial, pero quiero un servicio angular2.

Te tengo:

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

 }

Soy un gran admirador de los observadores y observables, ¡así que espero que esta actualización ayude!

Respuesta original

Creo que este es un caso de uso del uso de un sujeto observable o en Angular2el EventEmitter.

En su servicio, crea uno EventEmitterque le permite insertar valores en él. En Alpha 45 tienes que convertirlo toRx(), pero sé que estaban trabajando para deshacerse de eso, por lo que en Alpha 46 puedes simplemente devolver el EvenEmitter.

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

De esta manera tiene el único EventEmitterque sus diferentes funciones de servicio ahora pueden empujar.

Si desea devolver un observable directamente de una llamada, puede hacer algo como esto:

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

Eso le permitiría hacer esto en el componente: peopleService.myHttpCall('path').subscribe(people => this.people = people);

Y meterse con los resultados de la llamada en su servicio.

Me gusta crear la EventEmittertransmisión por sí sola en caso de que necesite acceder a ella desde otros componentes, pero pude ver que ambas formas funcionan ...

Aquí hay un plunker que muestra un servicio básico con un emisor de eventos: Plunkr

Dennis Smolek
fuente
Intenté este enfoque pero obtuve el error "No se puede usar 'nuevo' con una expresión cuyo tipo carece de una llamada o de una firma de construcción". Alguien tiene una idea de qué hacer?
Spock
3
@Spock, la especificación parece haberse actualizado desde esta pregunta original. Ya no necesita el "nuevo" para lo observable, ya que lo hace por usted. Simplemente elimine el nuevo y avíseme qué sucede. Estoy jugando con algunas cosas ahora, si funciona para ti también actualizaré esta respuesta
Dennis Smolek
1
Usar EventEmitterpara cualquier cosa pero no @Output()se recomienda. Ver también stackoverflow.com/questions/34376854/…
Günter Zöchbauer
@ GünterZöchbauer, Sí, ahora es ... En ese momento iban a ser EventEmitters por todas partes, pero desde entonces se han estandarizado en Rx Observables. Mi ejemplo Observable todavía funciona, pero si iba a usar el ejemplo EventEmitter que le di, sugiero usar Temas directamente: github.com/Reactive-Extensions/RxJS/blob/master/doc/api/…
Dennis Smolek
1
@maxisam Gracias por la edición, aunque la respuesta fue / es relativa a la eliminación alfa "nueva" para el Observable es correcta ahora
Dennis Smolek
29

Este es un ejemplo de los documentos de Angular2 sobre cómo puede crear y usar sus propios Observables:

El servicio

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

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

Puede encontrar un ejemplo completo y funcional aquí: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

tibbus
fuente
18

Me gustaría agregar que si el objeto que se crea es estático y no llega a través de http, se puede hacer algo así:

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

Editar: para el mapeo Angular 7.xx se debe hacer usando pipe () como se describe aquí ( 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 respuesta a mi pregunta sobre observadores y datos estáticos: https://stackoverflow.com/a/35219772/986160

Michail Michailidis
fuente
17

Llego un poco tarde a la fiesta, pero creo que mi enfoque tiene la ventaja de que carece del uso de EventEmitters y Asignaturas.

Entonces, aquí está mi enfoque. No podemos escapar de subscribe (), y no queremos hacerlo. En ese sentido, nuestro servicio regresará Observable<T>con un observador que tiene nuestra preciosa carga. Desde la persona que llama, inicializaremos una variable Observable<T>, y obtendrá el servicio Observable<T>. A continuación, nos suscribiremos a este objeto. Finalmente, obtienes tu "T"! de su servicio

Primero, nuestro servicio de personas, pero el suyo no pasa parámetros, eso es más realista:

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, como pueden ver, estamos devolviendo un Observabletipo de "personas". La firma del método, incluso lo dice! Metimos el _peopleobjeto en nuestro observador. A continuación, accederemos a este tipo desde nuestra persona que llama en el Componente.

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

Inicializamos nuestro _peopleObservabledevolviendo eso Observable<people>de nuestro PeopleService. Luego, nos suscribimos a esta propiedad. Finalmente, establecemos this.peoplenuestra peoplerespuesta data ( ).

Diseñar el servicio de esta manera tiene una gran ventaja sobre el servicio típico: mapa (...) y componente: patrón de "suscripción (...)". En el mundo real, necesitamos asignar el json a nuestras propiedades en nuestra clase y, a veces, hacemos algunas cosas personalizadas allí. Entonces este mapeo puede ocurrir en nuestro servicio. Y, típicamente, debido a que nuestra llamada de servicio se usará no una vez, sino, probablemente, en otros lugares de nuestro código, no tenemos que realizar esa asignación en algún componente, nuevamente. Además, ¿qué pasa si agregamos un nuevo campo a las personas? ...

Perro salchicha grande
fuente
Estoy de acuerdo en que el formato debe estar en el servicio y también publiqué un método Observable estándar, pero la ventaja de los Sujetos en un servicio es que otras funciones pueden activarse en él. Si siempre necesita la llamada http directa, usaría el método Observable ...
Dennis Smolek
9

En el archivo service.ts:

a. importar 'de' de observable / de
b. crear una lista json
c. devolver el objeto json usando Observable.of ()
Ej. -

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

En el componente donde estamos llamando a la función get del servicio:

this.clientListService.getClientList().subscribe(res => this.clientList = res);
Anirban Bhadra
fuente
Buen trabajo @Anirban, también solo podía regresar de (this.clientList);
foo-baar
7

Observe que está utilizando el mapa Observable # para convertir el Responseobjeto sin procesar que su Observable base emite en una representación analizada de la respuesta JSON.

Si te entendí correctamente, quieres volver a hacerlo map. Pero esta vez, convirtiendo ese JSON sin procesar en instancias de tu Model. Entonces harías algo como:

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

Entonces, comenzaste con un Observable que emite un Responseobjeto, lo convertiste en un observable que emite un objeto del JSON analizado de esa respuesta, y luego lo convertiste en otro observable que convirtió ese JSON sin procesar en una matriz de tus modelos.

julioolvr
fuente