Criando e retornando o Observable do Angular 2 Service


132

Essa é mais uma questão de "melhores práticas". Existem três jogadores: a Component, a Servicee a Model. O Componentestá chamando o Servicepara obter dados de um banco de dados. O Serviceestá usando:

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

retornar um Observable.

O Componentpoderia apenas assinar o Observable:

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

No entanto, o que eu realmente quero é Serviceretornar Array of Modelobjetos que foram criados a partir dos dados que foram Servicerecuperados do banco de dados. Percebi que o Componentpoderia apenas criar essa matriz no método de inscrição, mas acho que seria mais limpo se o serviço fizer isso e disponibilizá-lo para o Component.

Como pode Servicecriar um novo Observable, contendo essa matriz, e retornar isso?

Respostas:


159

ATUALIZAÇÃO: 24/09/16 Angular 2.0 Estável

Como essa questão ainda causa muito tráfego, eu queria atualizá-la. Com a insanidade das alterações dos candidatos Alpha, Beta e 7 RC, parei de atualizar minhas respostas de SO até que se tornassem estáveis.

Esse é o caso perfeito para usar Assuntos e ReplaySubjects

Eu pessoalmente prefiro usar ReplaySubject(1), pois permite que o último valor armazenado para ser passado quando novos assinantes anexar mesmo quando final:

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

Portanto, mesmo se eu conectar tarde ou precisar carregar mais tarde, sempre posso receber a última chamada e não me preocupar em perder a chamada de retorno.

Isso também permite que você use o mesmo fluxo para pressionar para:

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

Mas e se você tiver 100% de certeza de que precisa fazer a ligação apenas uma vez? Deixar assuntos abertos e observáveis ​​não é bom, mas sempre existe o "E se?"

É aí que entra o 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

Impressionante! Apesar de termos encerrado o assunto, ele ainda respondeu com a última coisa que carregou.

Outra coisa é como assinamos a chamada http e lidamos com a resposta. O mapa é ótimo para processar a resposta.

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

Mas e se precisássemos aninhar essas chamadas? Sim, você pode usar assuntos com uma função 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();

Mas isso é muito e significa que você precisa de uma função para fazer isso. Digite FlatMap :

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

Doce, varé um observável que obtém os dados da chamada http final.

OK, isso é ótimo, mas eu quero um serviço angular2!

Entendi:

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

 }

Sou um grande fã de observadores e observáveis, então espero que esta atualização ajude!

Resposta original

Acho que este é um caso de uso de usar um Assunto Observable ou Angular2a EventEmitter.

No seu serviço, você cria um EventEmitterque permite inserir valores nele. No Alpha 45 você precisa convertê-lo toRx(), mas eu sei que eles estavam trabalhando para se livrar disso; portanto, no Alpha 46, você pode simplesmente devolver o EvenEmitter.

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

Dessa maneira, é possível selecionar EventEmitteras diferentes funções de serviço.

Se você quiser retornar um observável diretamente de uma chamada, poderá fazer algo assim:

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

Isso permitiria que você fizesse isso no componente: peopleService.myHttpCall('path').subscribe(people => this.people = people);

E mexa com os resultados da chamada em seu serviço.

Gosto de criar o EventEmitterfluxo por conta própria, caso precise obter acesso a partir de outros componentes, mas pude ver as duas maneiras de trabalhar ...

Aqui está um desentupidor que mostra um serviço básico com um emissor de evento: Plunkr


Eu tentei essa abordagem, mas obtive o erro "Não é possível usar 'novo' com uma expressão cujo tipo não possui uma chamada ou constrói uma assinatura" ". Alguém tem uma idéia do que fazer?
Spock

3
@ Spock, a especificação pareceu atualizar desde esta pergunta original. Você não precisa mais do "novo" para o observável, pois faz isso para você. Basta remover o novo e me informar o que acontece. Eu estou brincando com algumas coisas agora, se ele funciona para você, assim eu vou atualizar esta resposta
Dennis Smolek

1
Usando EventEmitterpara qualquer coisa, mas @Output()é desencorajado. Veja também stackoverflow.com/questions/34376854/…
Günter Zöchbauer

@ GünterZöchbauer, Sim, agora é ... Na época, os EventEmitters estariam por todo o lado, mas desde então padronizaram o Rx Observables. Meu exemplo Observable ainda funciona, mas se você estava indo para usar o exemplo EventEmitter eu dei eu sugiro usar Assuntos diretamente: github.com/Reactive-Extensions/RxJS/blob/master/doc/api/...
Dennis Smolek

1
@maxisam Obrigado pela edição, embora a resposta era / é relativo ao Alpha remoção "novo" para o observável é correto agora
Dennis Smolek

29

Este é um exemplo dos documentos do Angular2 de como você pode criar e usar seus próprios Observables:

O serviço

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

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

Um exemplo completo e funcional pode ser encontrado aqui: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service


18

Gostaria de acrescentar que, se o objeto criado for estático e não for enviado através de http, algo assim poderá ser feito:

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: Para o mapeamento Angular 7.xx, é necessário usar o pipe (), conforme descrito aqui ( 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
      }
    }

da resposta à minha pergunta sobre observadores e dados estáticos: https://stackoverflow.com/a/35219772/986160


17

Estou um pouco atrasado para a festa, mas acho que minha abordagem tem a vantagem de não ter o uso de EventEmitters e Subject.

Então, aqui está a minha abordagem. Não podemos deixar de subscrever () e não queremos. Nesse sentido, nosso serviço retornará Observable<T>com um observador que possui nossa preciosa carga. No chamador, inicializaremos uma variável Observable<T>, e ela receberá os serviços Observable<T>. Em seguida, assinaremos este objeto. Finalmente, você recebe o seu "T"! do seu serviço.

Primeiro, nosso pessoal atende, mas o seu não passa parâmetros, isso é mais 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 você pode ver, estamos retornando um Observabletipo de "pessoas". A assinatura do método, até diz isso! Colocamos o _peopleobjeto em nosso observador. Acessaremos esse tipo de nosso chamador no Componente, a seguir!

No 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 nosso _peopleObservableretornando isso Observable<people>de nosso PeopleService. Em seguida, assinamos esta propriedade. Por fim, definimos this.peoplenossa peopleresposta data ( ).

A arquitetura do serviço dessa maneira possui uma grande vantagem sobre o serviço típico: mapa (...) e componente: padrão "subscrever (...)". No mundo real, precisamos mapear o json para nossas propriedades em nossa classe e, às vezes, fazemos algumas coisas personalizadas lá. Portanto, esse mapeamento pode ocorrer em nosso serviço. E, normalmente, como nossa chamada de serviço será usada não apenas uma vez, mas, provavelmente, em outros lugares do nosso código, não precisamos executar esse mapeamento em algum componente novamente. Além disso, e se adicionarmos um novo campo às pessoas? ....


Concordo que a formatação deve estar no serviço e também publiquei um método Observable padrão, mas a vantagem de Assuntos em um serviço é que outras funções podem ser acionadas nele. Se você está sempre precisando apenas da chamada http direta, então eu usaria o método Observable ..
Dennis Smolek

9

No arquivo service.ts -

uma. importação 'de' de observável / de
b. crie uma lista json
c. retornar objeto json usando 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);
    }
};

No componente em que estamos chamando a função get do serviço -

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

Bom trabalho @Anirban, também só poderia retornar de (this.clientList);
foo-baar

7

Observe que você está usando o mapa Observable # para converter o Responseobjeto bruto que o Observable básico emite em uma representação analisada da resposta JSON.

Se eu entendi você corretamente, você quer mapnovamente. Mas desta vez, convertendo esse JSON bruto em instâncias do seu arquivo Model. Então você faria algo como:

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

Então, você começou com um Observable que emite um Responseobjeto, transformou isso em um observável que emite um objeto do JSON analisado dessa resposta e, em seguida, transformou isso em outro observável que transformou esse JSON bruto em uma matriz de seus modelos.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.