我有一个递归 JavaScript,我在 Angular 19 中使用它从 YouTube 提取数据。 脚本可以运行,但是有更好的方法吗?
public getSeriesList() {
this.record.webTubeSeries = [];
this._seqNum = 0;
this.getUrlYouTube()
.pipe(first())
.subscribe({
next: (res: any) => {
this.parseVideoList(res["items"]);
if (res['nextPageToken']) {
let repeatGetNextVideoPage = (_token: string) => {
this.getNextVideoPage(_token)
.subscribe(
(result: any) => {
this.parseVideoList(result["items"]);
if (result["nextPageToken"]) {
repeatGetNextVideoPage(result["nextPageToken"]);
}
}
),
(err: any) => {
console.log("HTTP Error", err.message)
}
}
repeatGetNextVideoPage(res["nextPageToken"]);
}
}
}
);
}
private getNextVideoPage(_token: string) {
let url = (this.urlYouTube + this.videoListId + '&pageToken=' + _token);
return this.http.get(url);
}
private parseVideoList(result: any) {
for (let v of result) {
var item = new WebtubeSeries;
item.videoTitle = v.snippet.title;
item.videoId = v.snippet.resourceId.videoId;
if (v.snippet.thumbnails) {
item.urlThumbNailDefault = v.snippet.thumbnails.default.url;
item.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
item.urlThumbNailHigh = v.snippet.thumbnails.high.url;
item.widthDefault = v.snippet.thumbnails.default.width.toString();
item.heightDefault = v.snippet.thumbnails.default.height.toString();
item.widthMedium = v.snippet.thumbnails.medium.width.toString();
item.heightMedium = v.snippet.thumbnails.medium.height.toString()
item.widthHigh = v.snippet.thumbnails.high.width.toString()
item.heightHigh = v.snippet.thumbnails.high.height.toString();
item.seqNumber = this._seqNum++;
//item.id = item.seqNumber;
this.record.webTubeSeries.push(item);
}
}
}
在 Angular19 中,他们引入了
resource
和 rxResource
,可用于基于输入信号的数据处理。我建议使用 rxResource
这种方法,因为我们可以利用 rxjs 来执行递归。
首先我们在类构造函数中定义类逻辑的构造。
export class WebtubeSeries {
videoTitle: any;
urlThumbNailDefault: any;
urlThumbNailMedium: any;
videoId: any;
urlThumbNailHigh: any;
widthDefault: any;
heightDefault: any;
widthMedium: any;
heightMedium: any;
widthHigh: any;
heightHigh: any;
seqNumber: number;
constructor(v: any, _seqNum = 0) {
this.seqNumber = _seqNum;
// this.videoTitle = v.snippet.title;
// this.videoId = v.snippet.resourceId.videoId;
// if (v.snippet.thumbnails) {
// this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
// this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
// this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
// this.widthDefault = v.snippet.thumbnails.default.width.toString();
// this.heightDefault = v.snippet.thumbnails.default.height.toString();
// this.widthMedium = v.snippet.thumbnails.medium.width.toString();
// this.heightMedium = v.snippet.thumbnails.medium.height.toString();
// this.widthHigh = v.snippet.thumbnails.high.width.toString();
// this.heightHigh = v.snippet.thumbnails.high.height.toString();
// this.seqNumber = _seqNum;
// }
// this.seqNumber = 0;
}
}
然后,我们可以使用名为
rxResource
的属性定义 loader
来进行 API 调用。
在加载器内部,我们使用
expand
用于递归 API 调用,当没有下一个 API 令牌时,我们还使用 takeUntil
来停止序列。
最后,我们使用
reduce
合并结果并构建最终结果。
youtube = rxResource({
loader: () => {
let _seqNum = 0;
let records: any = [];
return this.getUrlYouTube().pipe(
expand((response: any) =>
this.getNextVideoPage(response['nextPageToken'])
),
takeWhile((response: any) => !!response['nextPageToken'], true),
reduce((all: any, data: any) => all.concat(data.items), []),
map((response: any) => {
console.log(response);
return this.parseVideoList(response, _seqNum);
})
);
},
});
import { Component } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { of, first, EMPTY, expand, takeWhile, tap, reduce, map } from 'rxjs';
import { CommonModule } from '@angular/common';
export class WebtubeSeries {
videoTitle: any;
urlThumbNailDefault: any;
urlThumbNailMedium: any;
videoId: any;
urlThumbNailHigh: any;
widthDefault: any;
heightDefault: any;
widthMedium: any;
heightMedium: any;
widthHigh: any;
heightHigh: any;
seqNumber: number;
constructor(v: any, _seqNum = 0) {
this.seqNumber = _seqNum;
// this.videoTitle = v.snippet.title;
// this.videoId = v.snippet.resourceId.videoId;
// if (v.snippet.thumbnails) {
// this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
// this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
// this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
// this.widthDefault = v.snippet.thumbnails.default.width.toString();
// this.heightDefault = v.snippet.thumbnails.default.height.toString();
// this.widthMedium = v.snippet.thumbnails.medium.width.toString();
// this.heightMedium = v.snippet.thumbnails.medium.height.toString();
// this.widthHigh = v.snippet.thumbnails.high.width.toString();
// this.heightHigh = v.snippet.thumbnails.high.height.toString();
// this.seqNumber = _seqNum;
// }
// this.seqNumber = 0;
}
}
@Component({
selector: 'app-root',
imports: [CommonModule],
template: `
{{youtube.value() | json}}
`,
})
export class App {
a = 0;
record: any = {
webTubeSeries: [],
};
getUrlYouTube() {
this.a++;
const randon = Math.random();
return of({
items: [0, 0, 0, 0, 0],
nextPageToken: randon,
});
}
youtube = rxResource({
loader: () => {
let _seqNum = 0;
let records: any = [];
return this.getUrlYouTube().pipe(
expand((response: any) =>
this.getNextVideoPage(response['nextPageToken'])
),
takeWhile((response: any) => !!response['nextPageToken'], true),
reduce((all: any, data: any) => all.concat(data.items), []),
map((response: any) => {
console.log(response);
return this.parseVideoList(response, _seqNum);
})
);
},
});
private parseVideoList(response: any, _seqNum: number) {
const result = [];
for (let v of response) {
console.log(v);
_seqNum++;
// if (v.snippet.thumbnails) {
result.push(new WebtubeSeries(v, _seqNum));
// }
}
return result;
}
private getNextVideoPage(_token: string) {
this.a++;
const randon = Math.random();
return of({
items: [1, 2, 3, 4, 5],
nextPageToken: this.a > 10 ? null : randon,
});
// let url = this.urlYouTube + this.videoListId + '&pageToken=' + _token;
// return this.http.get(url);
}
}
bootstrapApplication(App);
import { Component, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { of, first, EMPTY, expand, takeWhile, tap, reduce, map } from 'rxjs';
import { CommonModule } from '@angular/common';
import { HttpClient, provideHttpClient } from '@angular/common/http';
export class WebtubeSeries {
videoTitle: any;
urlThumbNailDefault: any;
urlThumbNailMedium: any;
videoId: any;
urlThumbNailHigh: any;
widthDefault: any;
heightDefault: any;
widthMedium: any;
heightMedium: any;
widthHigh: any;
heightHigh: any;
seqNumber: number;
constructor(v: any, _seqNum = 0) {
this.seqNumber = _seqNum;
this.videoTitle = v.snippet.title;
this.videoId = v.snippet.resourceId.videoId;
if (v.snippet.thumbnails) {
this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
this.widthDefault = v.snippet.thumbnails.default.width.toString();
this.heightDefault = v.snippet.thumbnails.default.height.toString();
this.widthMedium = v.snippet.thumbnails.medium.width.toString();
this.heightMedium = v.snippet.thumbnails.medium.height.toString();
this.widthHigh = v.snippet.thumbnails.high.width.toString();
this.heightHigh = v.snippet.thumbnails.high.height.toString();
this.seqNumber = _seqNum;
}
}
}
@Component({
selector: 'app-root',
imports: [CommonModule],
template: `
{{youtube.value() | json}}
`,
})
export class App {
a = 0;
record: any = {
webTubeSeries: [],
};
public readonly YOUTUBE_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // your own Youtube Key
public readonly YOUTUBE_URL = 'https://www.googleapis.com/youtube/v3/playlistItems?key=' + this.YOUTUBE_KEY +'&part=snippet&maxResults=12&playlistId='
videoListId = 'PLnR2Na1oFxkgk-ucqKSN1gQ2GJyz27cPw';
http = inject(HttpClient);
getUrlYouTube() {
return this.http.get(this.YOUTUBE_URL + this.videoListId)
}
youtube = rxResource({
loader: () => {
let _seqNum = 0;
let records: any = [];
return this.getUrlYouTube().pipe(
expand((response: any) =>
this.getNextVideoPage(response['nextPageToken'])
),
takeWhile((response: any) => !!response['nextPageToken'], true),
reduce((all: any, data: any) => all.concat(data.items), []),
map((response: any) => {
console.log('response:'+JSON.stringify(response));
return this.parseVideoList(response, _seqNum);
})
);
},
});
private parseVideoList(response: any, _seqNum: number) {
const result = [];
for (let v of response) {
console.log(v);
_seqNum++;
// if (v.snippet.thumbnails) {
result.push(new WebtubeSeries(v, _seqNum));
// }
}
return result;
}
private getNextVideoPage(_token: string) {
let url = this.YOUTUBE_URL + this.videoListId + '&pageToken=' + _token;
return this.http.get(url);
}
}
bootstrapApplication(App,{
providers:[
provideHttpClient()
]
});
function provideAnimationsAsync(): import("@angular/core").Provider | import("@angular/core").EnvironmentProviders {
throw new Error('Function not implemented.');
}
要测试它,您需要 Youtube 的密钥。 您可以使用我现有的 videolistId,也可以使用您自己的。 谢谢。