Angular开发中如何确保BehaviorSubject始终有值 解决Resolver请求参数依赖问题
问题根因
你当前的实现中ip定位请求是异步执行的,直接访问二级页面时路由Resolver会立即触发后端接口调用,此时定位请求还没返回,this.country 还是 undefined,调用 toLowerCase() 自然会抛出错误。
修复方案
提供两种可行方案,可根据业务场景选择:
方案1:用APP_INITIALIZER阻塞应用初始化,拿到国家后再启动应用
这是最彻底的方案,从根源保证所有业务逻辑执行前国家数据已经就绪。
首先修改HelperService,把detectUserLocation改成返回Observable:
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, tap } from 'rxjs'; import { take } from 'rxjs/operators'; @Injectable( { providedIn: 'root' } ) export class HelperService { public showRightDrawer = new BehaviorSubject<boolean>(false); public requestCountry = new BehaviorSubject<string>(undefined); constructor(private http: HttpClient) {} // 改成返回Observable,供APP_INITIALIZER使用 detectUserLocation(): Observable<any> { const url = 'http://ip-api.com/json'; return this.http.get(url).pipe( take(1), tap((c: any) => { const cc = c.countryCode; this.requestCountry.next(cc); }) ); } }
然后在AppModule里配置APP_INITIALIZER提供者:
import { NgModule, APP_INITIALIZER } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; import { HelperService } from './common/services/helper.service'; import { AppComponent } from './app.component'; // 初始化工厂函数 export function appInitializer(helperService: HelperService) { return () => helperService.detectUserLocation().toPromise(); } @NgModule({ imports: [BrowserModule, HttpClientModule], declarations: [AppComponent], providers: [ { provide: APP_INITIALIZER, useFactory: appInitializer, multi: true, deps: [HelperService] } ], bootstrap: [AppComponent] }) export class AppModule { }
配置完成后Angular会等detectUserLocation请求完成之后才会启动应用、渲染页面、执行路由Resolver,不会出现国家数据没拿到的情况。
方案2:改造业务服务,异步等待国家数据就绪后再发请求
如果不想阻塞应用初始化,可以修改RoasterService的getRoasters方法,先等requestCountry有有效值再发起后端请求:
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { take, switchMap, filter } from 'rxjs/operators'; import { SERVER_URL } from 'src/environments/environment'; import { HelperService } from '../common/services/helper.service'; import { Roaster } from './roaster.interface'; @Injectable() export class RoasterService { private endPoint: string = SERVER_URL; private httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json', responseType: 'text' }) }; constructor(private httpService: HttpClient, private helper: HelperService) {} getRoasters(page?: number, limit?: number): Observable<Roaster[]> { const reqLimit = limit || 15; const reqPage = page || 0; // 先过滤掉undefined的无效值,拿到国家后再发起请求 return this.helper.requestCountry.pipe( filter(country => !!country), take(1), switchMap(country => { return this.httpService.get<Roaster[]>( `${this.endPoint}roaster?limit=${reqLimit}&page=${reqPage}&country=${country.toLowerCase()}`, this.httpOptions ) }), take(1) ); } }
这个方案不需要改应用初始化逻辑,只需要调整业务服务的请求顺序,用switchMap把国家流和后端请求流串起来,Resolver会自动等待整个异步链执行完成再渲染页面。
补充说明
建议给ip-api.com的请求加失败降级逻辑,比如请求异常时默认给国家赋值为常用值,避免请求失败导致应用阻塞或报错。
内容的提问来源于stack exchange,提问作者MrRobot
相关产品推荐
相关产品推荐

