How to do a async HTTP request inside an Angular2 loop/map and modify the original loop array?

北慕城南 提交于 2019-12-09 18:27:27

问题


New to Angular2 here and wondering how to do this async request on array pattern (not so sure about the pattern name).

So let's say I use Angular2's Http to get a YouTube Playlist that contains videoId in each return item. Then I need to loop through this item with the videoId and do another request to YouTube to get the individual video information. This can be another common HTTP request, get the list then loop through the list and get the details of every individual item.

let url = [YouTube API V3 list url]
let detailUrl = 'https://www.googleapis.com/youtube/v3/videos?part=contentDetails,statistics';

this.http.get(url)
    .map(res => res.json())
    .subscribe(data => {
        let newArray = data.items.map(entry => {

            let videoUrl = detailUrl + '&id=' + entry.id.videoId + 
                                       '&key=' + this.googleToken;

            this.http.get(videoUrl)
                .map(videoRes => videoRes.json())
                .subscribe(videoData => {
                     console.log(videoData);

                     //Here how to I join videoData into each item inside of data.items. 
                });
    });
});

So the above code does work. But I still have these two questions:

  1. How do I join the videoData back to data.items and make sure the correct item inside the data.items is joined with the correct videoData (the videoData that is using the entry.id.videoId of the related item inside data.items) and create that new newArray?

  2. How do I know when everything is done and do something based on all the data after all the async requests have finished?

  3. The newArray keep the same order of the first HTTP request. As the first HTTP hit the YouTube search list API with order by viewCount. Simply nest observable like above will not keep the original order.

UPDATE

With inoabrian solution, I can successfully achieve the above three points. But when I call the myvideoservice again (like changing the url as new NextPageToken), the Subject - this._videoObject has 2 observers. And load again, it has 3 observers and so on. I need a way to reset the Subject to have only 1 observers so I won't have duplicate videos. Is it a best practice way to clear/reset subject?


回答1:


// You need to set up a subject in your sevice
private _videoObject = new Subject < any > ();
videoDataAnnounced$ = this._videoObject;


let url = [YouTube API V3 list url]
let detailUrl = 'https://www.googleapis.com/youtube/v3/videos?part=contentDetails,statistics';

this.http.get(url)
    .map(res => res.json())
    .subscribe(data => {
        this.cache = data.items;
        data.items.forEach(entry => {
            let videoUrl = detailUrl + '&id=' + entry.id.videoId + '&key=' + this.googleToken;
            this.http.get(videoUrl)
                .map(videoRes => videoRes.json())
                .subscribe(videoData => {
                    // This will announce that the full videoData is loaded to what ever component is listening.
                    this._videoObject.next(videoData);
                });
        });
    });



// In your constructor for your component you can subscribe to wait on the data being loaded.
public newVideos: any[] = [];
constructor(private myServiceToLoadVideos: myvideoservice) {
    let self = this;
    this.myServiceToLoadVideos.videoDataAnnounced$.subscribe((video) => {
        // Here you could loop through that
        self.myServiceToLoadVideos.cache.map(d => {
            if (d.id == video.id) {
                // Here you have the old data set without the details which is (d) from the cached data in the service.
                // And now you can create a new object with both sets of data with the video object
                d.snippet = video.items[0].snippet;
                d.statistics = video.items[0].statistics;
                d.contentDetails = video.items[0].contentDetails;
                this.posts = this.posts.concat(d);
            }
        });
        if(this.posts.length == this.cache.length){
          this.posts.sort(function(l,r){
            // if l is < r then it will return a negative number
            // else it will return a positive number.
            // That is how this sort function works
            return l.viewCount - r.viewCount;
          });

          // Here you will have all of the data and it will be sorted in order.
        }
    });
}

UPDATE -- -- --

The Subject is an observable - http: //reactivex.io/documentation/subject.html

    When you listen to the Subject it is not like q.all it will complete one by one until they complete.

The cache in the service call is just to keep track of the playlist.
Then when you receive the details in the constructor you can match them back up and keep order with the cache.

Q: "And when subscribe to the subject as in your code, the result is when ALL the "
next " in _videoObject finished, right?"
A: No, when each individual finishes you will get once at a time.

JS Sort - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort




回答2:


Assuming that you only need the modified data, once all the sub-requests are completed

This is what you'll need

this.http.get(url) // original request
      .map(res => res.json())  // extract json from response
      .switchMap(data => {  // take-over the original data and make the subsribers wait for sub-requests to complete

        let modifiedRequest = new Subject(); // observable to to publish modified data, once all sub-requests are completed

        let allSubRequests = data.items.map(entry => {  // array of sub-requests

          let videoUrl = detailUrl + '&id=' + entry.id.videoId +
                         '&key=' + this.googleToken;

          return this.http.get(videoUrl)
            .map(videoRes => videoRes.json())  // extract json for every sub-request
            .map(videoJson => {  // modify the original 'data' object with the data in videoJson (I don't know the format, so can't tell how-to)

              // modify your "data" here

              return videoJson; // let the flow continue
            });
        });

        let mergedSubRequests = Observable.range(0, 1).merge.apply(null, allSubRequests);  // merge all sub-requests to know when will all are completed

        mergedSubRequests.subscribe(  // subscribe to merged observable
          (d) => {}, // data from every sub-request, we don't need it
          (e) => {}, // error from every sub-request, handle it as you want
          () => {   // success, called after all sub-requests are completed
              modifiedRequest.next(data);  // send out the modified data
              modifiedRequest.complete();  // complete the temprary observable
              }
        );

        return modifiedRequest; // provide the observable with modified data, to subscribers of orginal request, they will not know the difference
      });

Required Imports:

import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';
import 'rxjs/add/observable/range';
import 'rxjs/add/operator/switchMap';

Hope it helps, not tested though :)




回答3:


let addDetail = (entry) => {
  let videoUrl = detailUrl + '&id=' + entry.id.videoId + 
                '&key=' + this.googleToken;

  return this.http.get(videoUrl)
    .map(videoRes => Object.assign({videoData: videoRes.json()}, entry)
};

let maxConcurrentRequest = 1;
let source = this.http.get(url)
  // any of mergeMap, concatMap, switchMap... will do
  .mergeMap(res => Observable.from(res.json().items))
  .mergeMap(addDetail, null, maxConcurrentRequest);
source.subscribe( /* Do anything you wish */ );

Now each item that source emits is an entry in the playlist merged with its videoData in videoData field (your first question)

source completes when everything is done (your second question)

you can control the number of maximum concurrent requests to detailUrl by setting maxConcurrentRequest. With maxConcurrentRequest = 1, source will emit items in the same order as they are in the playlist. If you don't care about order, increase maxConcurrentRequest for faster speed




回答4:


You already have access within the .map function to entry you should be able to just put another value onto it using dot notation. so do something like this:

let url = [YouTube API V3 list url]
let detailUrl = 'https://www.googleapis.com/youtube/v3/videos?part=contentDetails,statistics';

this.http.get(url)
    .map(res => res.json())
    .subscribe(data => {
        let newArray = data.items.map(entry => {

            let videoUrl = detailUrl + '&id=' + entry.id.videoId + 
                                       '&key=' + this.googleToken;

            this.http.get(videoUrl)
                .map(videoRes => videoRes.json())
                .subscribe(videoData => {
                     console.log(videoData);

                     entry.videoData = videoData;

                     //Here how to I join videoData into each item inside of data.items. 
                });
    });
});


来源:https://stackoverflow.com/questions/38155110/how-to-do-a-async-http-request-inside-an-angular2-loop-map-and-modify-the-origin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!