你能转换 switchMap observable 的结果来获取它的值而不是 observable 本身吗?

问题描述

我正在尝试创建一个内存中的单例来保存用户正在浏览的当前供应商。

在所有特定路由上使用警卫来捕获参数:

canActivate(
  route: ActivatedRouteSnapshot,state: RouterStateSnapshot): Observable<boolean | UrlTree> {

  let currentUrl = this._router.url;
  const param = route.params['vendname'];

  return this._vendorService.getByName(param).pipe(map(a => {
    if (a == null) {
      this._snackBarService.open('vendor not found','x',{ duration: 5000 });
      return this._router.parseUrl(currentUrl);
    }
    return true;
  }));
}

服务用于按名称获取供应商。如果它存在于内存中,则返回它。如果没有,请先从服务器获取

set vendor(value: IUser) {
  this._vendor.next(value);
}

get vendor$(): Observable<IUser> {
  return this._vendor.asObservable();
}

getByName(name: string): Observable<IUser> {
  const result = this.vendor$.pipe(map(v => {
    if (v != null && v.displayName == name) {
      return v;
    }
    else {
      return this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => {
        this.vendor = v;
        return of(v)
        // ...
      }));
    }
  }))
  return result;
}

问题是我需要检查 vendor$ 的值,该值返回 Obervable<IUser>,但 switchMap 也返回 Obervable<IUser>,导致结果为 Observable<Observable<IUser>>。如何让 result 返回单个 User Observable?

解决方法

我相信您误解了 switchMap() 的用例,因为它不是您的 Observable<Observable<IUser>> 的原因。 map() 中的第一个 getByName() 运算符要么返回 IUser 类型的值(如果为真),要么返回 IUser 的可观察值(如果为假)。所以 getByName() 返回 Observable<IUser>Observable<Observable<IUser>>

但是,如果您想尝试从可观察对象中重放内存中的值,那么我建议使用 shareReplay()。此外,对于这种情况,这里有一个推荐的模式。

private vendorName = new Subject<string>();

public vendor$ = this.vendorName.pipe(
  distinctUntilChanged(),switchMap(name=>this.Get<IUser>(`api/vendor/${name}`)),shareReplay(1)
)

public getByName(name:string){
  this.vendorName.next(name);
}

然后在保护文件中。

canActivate(
  // ...
){
  let currentUrl = this._router.url;
  const param = route.params['vendname'];

  this._vendorService.getByName(param);

  return this._vendorService.vendor$.pipe(
    map(vendor=>{
      if(!vendor){
        this._snackBarService.open('Vendor not found','x',{ duration: 5000 });
        return this._router.parseUrl(currentUrl);
      }
      return true;
    })
  );

通过此设置,您的组件和守卫可以订阅 vendor$ 以获取所需的 IUser 数据。当您知道供应商的名称时,您可以调用 getByName()。这将触发以下步骤:

  1. userName 主题将发出新名称。
  2. vendor$ observable(订阅 userName)将采用该名称,然后 switchMap 从内部 observable 获取值(Get 方法)
  3. shareReplay(1) 确保对 vendor$(当前或未来)的任何订阅都将从内存中获取最后 (1) 个发出的值。

如果您需要更新供应商名称,只需使用您的新名称调用 getByName(),订阅 vendor$ 的所有内容都会自动获得更新的值。

,

与我看到的 vendor$ 相关的是 IUser 流,这里是使用 iif rxjs 运算符重构您的代码以订阅第一个或第二个可观察对象根据您的情况

 this.vendor$.pipe(
    mergeMap(v => iif(
      () => v && v.displayName == name,of(v),this.Get<IUser>(`url`).pipe(tap(v => this.vendor = v))
     )
    )
  )
,

虽然我喜欢@digclo 的回答,但我正在添加另一个有效的解决方案。

好像 canActivate 也返回 Promise,您也可以使用可靠的旧 async await 方法。

注意:在 rxjs 7 中,toPromise 现在是 lastValueFrom

async getByVendorName(name: string): Promise<IUser> {
  const v:IUser = await new Promise(resolve => { 
    this.vendor$.subscribe(a => {
      if (a != null && a.displayName.toLocaleLowerCase() == name) {
        resolve(a);
      }
    });
    resolve(null);
  });

  if (v == null) {
    return this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => {
      this.vendor = v;
      return of(v);
    })).toPromise();

  }
  return Promise.resolve(v);
}