Angular 15无后端场景下批量API请求的最优处理方案
Angular 15 结合TMDB API 实现剧集筛选与无限滚动最优方案
针对你遇到的TMDB API筛选限制问题,结合Angular 15的特性,以下是一套高效的实现方案,核心思路是按需分页加载+并发请求控制+本地缓存,配合MatCard展示与无限滚动提升用户体验:
一、核心API调用策略优化
- 按需分页触发:利用无限滚动仅在用户滚动到底部时加载下一页的discover结果,避免一次性拉取3k页的巨大数据量
- 并发请求限流:对单页20个剧集ID,按批次(比如每5个一组)调用tv-details接口,避免触发TMDB的API限流规则(默认40次/10秒)
- 本地缓存复用:用Map存储已获取的剧集详情,后续相同ID直接读取缓存,减少重复请求
二、基于Angular CDK的无限滚动实现
首先确保项目已引入Angular CDK的滚动模块及Angular Material组件:
// 你的模块文件(如ShowFilterModule.ts) import { ScrollingModule } from '@angular/cdk/scrolling'; import { MatCardModule } from '@angular/material/card'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatSkeletonModule } from '@angular/material/skeleton'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ imports: [ // ...其他依赖模块 ScrollingModule, MatCardModule, MatProgressBarModule, MatSkeletonModule, MatProgressSpinnerModule ] }) export class ShowFilterModule {}
模板中结合cdkInfiniteScroll指令与MatCard实现列表展示,同时加入加载状态的骨架屏:
<div class="show-list-container" cdkInfiniteScroll [cdkInfiniteScrollDisabled]="isLoading || hasNoMoreData" (cdkInfiniteScroll)="loadNextPage()"> <mat-card *ngFor="let show of filteredShows$ | async; trackBy: trackShowId" class="show-card"> <!-- 加载中骨架屏 --> <ng-container *ngIf="show.isLoading; else showContent"> <mat-card-content class="skeleton-wrapper"> <mat-skeleton height="60px"></mat-skeleton> <mat-skeleton width="80%" height="20px" style="margin: 8px 0;"></mat-skeleton> <mat-skeleton width="60%" height="16px"></mat-skeleton> </mat-card-content> </ng-container> <!-- 剧集详情内容 --> <ng-template #showContent> <mat-card-title>{{show.details.name}}</mat-card-title> <mat-card-subtitle> 第{{show.details.number_of_seasons}}季 · {{show.details.number_of_episodes}}集 <span class="status-tag">{{show.details.status}}</span> </mat-card-subtitle> <mat-card-content> <p>{{show.details.overview || '暂无剧集简介'}}</p> </mat-card-content> </ng-template> </mat-card> <!-- 加载更多提示 --> <div *ngIf="isLoading" class="loading-footer"> <mat-progress-spinner diameter="30" mode="indeterminate"></mat-progress-spinner> </div> </div>
三、组件核心逻辑实现
import { Component, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable, of, forkJoin, concatMap } from 'rxjs'; import { map, tap, catchError, retry } from 'rxjs/operators'; // 定义带加载状态的剧集结构 interface ShowWithLoading { id: number; details?: any; // 对应TMDB TV Details返回结构 isLoading: boolean; } @Component({ selector: 'app-show-filter', templateUrl: './show-filter.component.html', styleUrls: ['./show-filter.component.css'] }) export class ShowFilterComponent implements OnInit { filteredShows$: Observable<ShowWithLoading[]>; private showsSubject = new BehaviorSubject<ShowWithLoading[]>([]); currentPage = 1; totalPages = 1; isLoading = false; hasNoMoreData = false; private showCache = new Map<number, any>(); // 筛选条件(可根据用户输入动态调整) private filterConditions = { episodeCount: 8, status: 'ended' }; constructor(private http: HttpClient) { this.filteredShows$ = this.showsSubject.asObservable(); } ngOnInit(): void { // 初始化加载第一页 this.loadNextPage(); } loadNextPage(): void { if (this.isLoading || this.currentPage > this.totalPages) return; this.isLoading = true; // 1. 调用discover接口获取当前页剧集ID列表 this.http.get<any>(`https://api.themoviedb.org/3/discover/tv`, { params: { api_key: 'YOUR_TMDB_API_KEY', // 替换为你的API密钥 page: this.currentPage.toString(), status: this.filterConditions.status } }).pipe( tap(res => { this.totalPages = res.total_pages; this.currentPage++; }), // 转换为带加载状态的临时条目 map(res => res.results.map((item: any) => ({ id: item.id, isLoading: true } as ShowWithLoading))), // 追加到现有列表 tap(temporaryItems => { const currentItems = this.showsSubject.value; this.showsSubject.next([...currentItems, ...temporaryItems]); }), // 分批次获取剧集详情,控制并发数 concatMap(temporaryItems => { const idChunks = this.chunkArray(temporaryItems.map(item => item.id), 5); return forkJoin( idChunks.map(chunk => forkJoin( chunk.map(id => this.getShowDetails(id)) )) ).pipe(map(results => results.flat())); }), retry(2), // 临时错误重试2次 catchError(err => { console.error('数据加载失败:', err); this.isLoading = false; return of([]); }) ).subscribe(detailsList => { // 更新列表:保留符合筛选条件的剧集,移除不符合的 const updatedShows = this.showsSubject.value.map(item => { const matchDetail = detailsList.find(d => d.id === item.id); if (!matchDetail) return item; this.showCache.set(item.id, matchDetail); // 检查是否满足集数条件 if (matchDetail.number_of_episodes === this.filterConditions.episodeCount) { return { ...item, details: matchDetail, isLoading: false }; } else { // 不符合条件的直接过滤掉 return null; } }).filter(item => item !== null) as ShowWithLoading[]; this.showsSubject.next(updatedShows); this.isLoading = false; this.hasNoMoreData = this.currentPage > this.totalPages; }); } // 获取单部剧集详情,优先读缓存 private getShowDetails(id: number): Observable<any> { if (this.showCache.has(id)) { return of(this.showCache.get(id)); } return this.http.get<any>(`https://api.themoviedb.org/3/tv/${id}`, { params: { api_key: 'YOUR_TMDB_API_KEY' } }).pipe( tap(detail => this.showCache.set(id, detail)) ); } // 数组拆分工具函数:按指定大小拆分数组 private chunkArray<T>(arr: T[], size: number): T[][] { const chunks = []; for (let i = 0; i < arr.length; i += size) { chunks.push(arr.slice(i, i + size)); } return chunks; } // trackBy函数:提升列表渲染性能 trackShowId(index: number, show: ShowWithLoading): number { return show.id; } // 当用户修改筛选条件时调用此方法重置状态 resetFilter(newConditions: { episodeCount: number; status: string }): void { this.filterConditions = newConditions; this.currentPage = 1; this.totalPages = 1; this.hasNoMoreData = false; this.showCache.clear(); this.showsSubject.next([]); this.loadNextPage(); } }
四、关键优化细节
- 筛选条件动态适配:通过
resetFilter方法,可在用户修改筛选条件时重置所有状态,重新加载数据 - 性能优化:使用
trackBy函数避免列表重复渲染,骨架屏提升加载过程的用户体验 - 限流防护:分批次调用详情接口,配合
retry处理临时网络错误,降低触发TMDB API限流的概率 - 缓存复用:本地缓存已请求的剧集详情,重复筛选时无需再次调用接口
内容的提问来源于stack exchange,提问作者user6888766
相关产品推荐
相关产品推荐

