通过ng2中的Observables的相关和并行请求

时间:2016-07-29 19:19:49

标签: angular observable

我正在整理一个应用程序,显示有关'Modyules'(在课程中)的信息,它必须通过两个http.gets(我无法控制api):

  1. 获取Modyule.ids列表 - 需要存储id
  2. 为每个Modyule.id另一个http.get获取Modyule.name
  3. 在我的modyule.component.ts中,我有:

    this.modyuleService.getModyules()
         .subscribe(
              modyules => this.modyules = modyules,
              error =>  this.errorMessage = <any>error
         );
    

    在我的modyule.service.ts中,我有:

    getModyules (): Observable<Modyule[]> {
            return this.http.get(listOfModyuleIdsUrl + '.json')
                .map(this.getModyulesDetails)
                .catch(this.handleError);
        }
    

    但getModyuleDetails是我在努力的地方。这是我到目前为止所处的地方,主要基于:

    http://www.syntaxsuccess.com/viewarticle/angular-2.0-and-http

    并观察,但没有看到我如何申请:

    https://stackoverflow.com/a/35676917/2235210

    private getModyulesDetails = (res: Response) => {  // instance method for defining function so that the this.http below refers to the class.
        let listOfModyules = res.json();
        let modyulesToReturn = [];
    
        for (let individualModyule of listOfModyules){
            let tempModyule = new Modyule;
            tempModyule.id = individualModyule.id;
    
            this.http.get(individualModyuleUrl + individualModyule.id + '.json')
                .map((res: Response) => res.json()))
                .subscribe(res => {
                    tempModyule.name = res.name
                    modyulesToReturn.push(tempModyule);
                });
            }
        return modyulesToReturn;      
    }
    

    现在这是在我的本地机器上工作,我已经将.json响应模拟为静态.json文件..但是,当我处理真正的api时,我不能依赖于http.gets在返回modyulesToReturn之前完成for循环。

    我在forkJoin上做了几次尝试,但是没有看到我如何将它与从第一个http.get捕获Modyule.id的要求结合起来。

    我非常感谢有关如何执行并行请求的指针,这些请求依赖于初始请求,但能够组合来自两者的数据。

    编辑回应与@Matt和@batesiiic的讨论:

    所以,接受@ batesiiic的建议重写getModyulesDetails以返回一个Subject,我无法理解的问题是如何在第一次调用中填充Modyule.id然后在第二次调用中填充Modyule.name call(不返回对Modyule.id的任何引用)即:

    private getModyulesDetails = (res: Response) => {  // instance method for defining function so that the this.http below refers to the class.
        let listOfModyules = res.json();
        let modyulesToReturn = [];
        let calls = [];
    
        for (let individualModyule of listOfModyules){
            /* I REALLY NEED TO POPULATE Modyule.id HERE */
            calls.push(
                this.http.get(individualModyuleUrl + individualModyule.id + '.json')
                    .map((res: Response) => res.json()))
            );
        }
    
        var subject = new Subject<Modyules[]>();
    
        Observable.zip.apply(null, calls).subscribe((modyules: Modyules[]) => {
            var modyulesToReturn = [];
            modyules.forEach(modyule => {
                let tempModyule = new Modyule;
                /*I COULD POPULATE Modyle.name HERE BUT IT WON'T MATCH THE Modyule.id SET ABOVE - EXCEPT BY LUCK*/
                modyulesToReturn.push(tempModyule);
            }
            subject.next(modyulesToReturn);
        });
    
        return subject;      
    }
    

    我想我需要在for循环中逐个调用,但不知怎的,等到它们都返回modyulesToReturn之前都已响应?

4 个答案:

答案 0 :(得分:1)

如果一个请求依赖于另一个请求返回的数据(在您需要module.id之前,您可以调用module.name或其详细信息),则应该启动相关请求。回调第一个请求。

this.modyuleService.getModyules()
 .subscribe(
      modyules => {
        // start your request(s) for the details here
      },
      error =>  ...
 );

此外,您使用以下构造:

getModyulesDetails() {
  // this is what you want to return
  let modyulesToReturn = [];

  for (let individualModyule of listOfModyules){
    //.. you are starting asynchronous tasks here,
    // and fill your modyulesToReturn in their callbacks
  }

  // instantly return the variable that is filled asynchrously
  return modyulesToReturn;  
}    

这不能按照你想要的方式运作。 for循环中的http请求是异步的,即它们已经启动但当前线程不会等到它们完成,而是继续for loop的下一次迭代。当您从方法返回时,您的订阅中的回调很可能尚未被调用。当他们这样做时,你已经返回了你的(可能)空数组modyulesToReturn

因此,您不能同步返回。相反,您应该提供一个返回Observable的方法,每当您收到另一个modyule详细信息时,它都会触发。需要这些详细信息的组件将自己订阅Observable并异步接收更新。

如果您开始使用异步任务,则需要将“设计”推送到组件。

答案 1 :(得分:1)

您可以使用Observable.zip来处理一次发生的多个调用。这需要进行getModyules调用以返回一个主题(某种类型),然后在拉链完成后再调用它。

getModyules (): Observable<Modyule[]> {
    var subject = new Subject<Modyule[]>();
    this.http.get(listOfModyuleIdsUrl + '.json')
        .subscribe(result => {
            this.getModyulesDetails(result)
                .subscribe(modyules => {
                    subject.next(modyules);
                });
        }, error => subject.error(error));

    return subject;
}

细节调用变得类似(zip返回所有返回的集合,我相信在你的情况下会Modyules[]):

private getModyulesDetails = (res: Response) => {  // instance method for defining function so that the this.http below refers to the class.
    let listOfModyules = res.json();
    let modyulesToReturn = [];
    let calls = [];

    for (let individualModyule of listOfModyules){
        calls.push(
            this.http.get(individualModyuleUrl + individualModyule.id + '.json')
                .map((res: Response) => res.json()))
        );
    }

    var subject = new Subject<Modyules[]>();

    Observable.zip.apply(null, calls).subscribe((modyules: Modyules[]) => {
        var modyulesToReturn = [];
        modyules.forEach(modyule => {
            let tempModyule = new Modyule;
            //populate modyule data
            modyulesToReturn.push(tempModyule);
        }
        subject.next(modyulesToReturn);
    });

    return subject;      
}

我在浏览器中进行了这些更改,因此我为语法错误道歉,但我认为一般的想法可以解决您尝试做的事情。

答案 2 :(得分:1)

好吧,多亏了@batesiiic,@ Matt(我已经对你的答案提出了支持)和一个有用的时间(与两个漫长的夜晚和周六的整个星期一相比,海洋中的一滴水#I;我已经花了这么多时间!)花了很多时间看Angular University,我有一个有效的解决方案(到目前为止!)。 .switchMap将两个请求链接在一起,将Observable从第一个传递到第二个,@ batesiiic的精彩主题思想(和forkJoin - 我无法让zip工作)并意识到我可以得到我的通过查看响应对象本身的依赖请求中的Modyule.id!

在ngOnInit()的modyule.component.ts中:

this.modyuleService.getModyules()
        .switchMap(modyules =>this.modyuleService.getModyulesDetails(modyules)) 
        .subscribe(
            modyules => {
                this.modyules = modyules  
                },
           error =>  this.errorMessage = <any>error);

并在module.service.ts中:

getModyules (): Observable<Modyule[]> {
    return this.http.get(this.listOfModyuleIdsUrl)
        .map(this.initialiseModyules)
        .catch(this.handleError);            
}

private initialiseModyules(res: Response){
    let body = res.json();
    let modyulesToReturn = [];
    for (let individualModyule of listOfModyules){
        let tempModyule = new Modyule;
        tempModyule.id = individualModyule.id;
        modyulesToReturn.push(tempModyule);
        }
    }
    return modyulesToReturn;
}

getModyulesDetails (modyules:Modyule[]): Observable<Modyule[]> {
    let calls  = [];

    for (let modyule of modyules){
        calls.push(
            this.http.get(this.individualModyuleUrl + modyule.id + '.json')
            );
    }

    var subject = new Subject<Modyule[]>();   

    Observable.forkJoin(calls).subscribe((res: any) => {
        for (let response of res){
            //Note this is a really very awkward way of matching modyule with a id assigned in getModyules (above) with the correct response from forkJoin (could come back in any order), by looking at the requested url from the response object
            let foundModyule = modyules.find(modyule=> {
                let modyuleUrl = this.individualModyuleUrl + modyule.id + '.json';
                return modyuleUrl === response.url;
            });
            let bodyAsJson = JSON.parse(response._body);
            foundModyule.name = res.name;
            }
        subject.next(modyules);
    });

    return subject;
}

希望能帮到别人。然而,仍然无法感觉到应该有一些方法不会从多个调用返回以获取详细信息,直到它们全部完成,而不必诉诸于所有.forkJoin或.zip的所有内容。除了通过查看response.url ...

,与调用的modyule有更长的关系

再次感谢您的帮助。

答案 3 :(得分:0)

我必须做类似的事情,在我可以发出任何后续请求之前,我必须请求身份验证令牌。

我想为此公开一个“ApiService.Get(url)”,并且需要完成所有工作。使用从更高级别的呼叫者隐藏的令牌,但仍然只执行调用以获取令牌一次。

我最终得到了这样的东西......

export class ApiService {
  private apiToken: string;
  private baseHeaders: Headers;
  private tokenObserver: ReplaySubject<Response>;

  constructor(private http: Http) {
    this.tokenObserver = null;
    try {
      // set token if saved in local storage
      let apiData = JSON.parse(localStorage.getItem('apiData'));
      if (apiData) {
        this.apiToken = apiData.apiToken;
      }
      this.baseHeaders = new Headers({
        'Accept-encoding': 'gzip',
        'API-Key': 'deadbeef-need-some-guid-123456789012',
      });
    } catch(e) {
      console.log(e);
    }
  }

  private requestHeaders(): Headers {
    if (this.apiToken) {
      this.baseHeaders.set('Authorization', 'Bearer ' + this.apiToken);
    }
    return this.baseHeaders;
  }

  private getToken(): Observable<Response> {
    if (this.tokenObserver == null) {
      this.tokenObserver = new ReplaySubject<Response>();
      this.http.get('/newtoken', this.requestHeaders())
        .subscribe(
          this.tokenObserver.next,
          this.tokenObserver.error,
          this.tokenObserver.complete,
        );
    }
    return this.tokenObserver.asObservable();
  }

  private isApiTokenValid():boolean {
    console.log("Pretending to check token for validity...");
    return !!this.apiToken;
  }

  // Get is our publicly callable API service point
  //
  // We don't accept any options or headers because all
  // interface to our API calls (other than auth) is through
  // the URL.    
  public Get(url: string): Observable<Response> {
    let observer: Subject<Response> = new Subject<Response>();
    try {
      if (this.authTokenValid()) {
        this.getRealURL(observer, url);
      } else {
        this.GetToken().subscribe(
          (v) => this.getRealURL(observer, url),
          observer.error,
          // ignore complete phase
        );
      }
    } catch(e) {
      console.log("Get: error: " + e);
    }
    return observer.asObservable();
  }

  private getRealURL(observer: Subject<Response>, url: string): void {
    try {
      this.http.get(url, {headers: this.requestHeaders()})
        .subscribe(
          observer.next,
          observer.error,
          observer.complete,
        );
    } catch(e) {
      console.log("GetRealURL: error: " + e);
    }
  }
}

有了这个,我的API调用将归结为:

this.api.Get('/api/someurl')
  .subscribe(this.handleSomeResponse)
  .catch(this.handleSomeError);