问题
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:
How do I join the
videoData
back todata.items
and make sure the correct item inside thedata.items
is joined with the correctvideoData
(thevideoData
that is using theentry.id.videoId
of the related item insidedata.items
) and create that newnewArray
?How do I know when everything is done and do something based on all the data after all the async requests have finished?
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