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

Posted

技术标签:

【中文标题】你可以转换 switchMap observable 的结果来获取它的值而不是 observable 本身吗?【英文标题】:Can you transform the result of switchMap observable to get its value and not the observable itself? 【发布时间】:2021-10-01 06:44:06 【问题描述】:

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

在所有特定路由上都使用一个守卫来捕获参数:

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&lt;IUser&gt;,但switchMap 也返回Obervable&lt;IUser&gt;,导致结果为Observable&lt;Observable&lt;IUser&gt;&gt;。如何让 result 返回单个 User Observable?

【问题讨论】:

返回 v 而不是 of(v) return this.Get&lt;IUser&gt;(`api/vendor/$name`).pipe 总是返回一个 observable,因此需要在 vendor$ 管道中使用一个 swtich 映射 this.vendor$.pipe(map(v =&gt; ,但请注意,您需要将 switchmap 中的不可观察返回转换为 observables 另外:你为什么使用this.Get&lt;IUser&gt;(`api/vendor/$name`).pipe(switchMap(v =&gt; ?我看不到 switchmap 的场景,还是你的代码更短,... 包含真正的 switchmap? getByName 的调用从未进行过订阅,因为它是从我的guard 调用的。所以,我使用 switchmap(据我所知)进行内部订阅。 【参考方案1】:

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

但是,如果您想尝试利用从 observable 重放内存中的值,那么我建议您使用 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()。这将触发以下步骤:

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

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

【讨论】:

我无法在调用服务器之前检查名称是否已经存在。我不想在每个角度路由上调用服务器。 抱歉,我更新了我的示例以适应您的用例。我在调用 API 之前添加了distinctUntilChanged()。这可以防止在vendorName 发出重复名称时进行重复调用。我还包括了调用方法以发出新参数的保护更新,然后根据vendor$ 的最后发出值返回相同的逻辑。 这行得通,但我仍然需要能够在浏览器地址栏中手动添加带有供应商名称的 URL。此外,我在导航时遇到了奇怪的行为。将您的 Subject 更改为 ReplaySubject&lt;string&gt;(1),以便它只跟踪单个值。【参考方案2】:

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

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

【讨论】:

这只有在我明确subscribe 时才有效。【参考方案3】:

我正在添加另一个有效的解决方案。

看起来canActivate 也返回了Promise,您也可以使用可靠的旧async await 方法。

注意:toPromise 现在在 rxjs 7 中是 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);

【讨论】:

以上是关于你可以转换 switchMap observable 的结果来获取它的值而不是 observable 本身吗?的主要内容,如果未能解决你的问题,请参考以下文章

RxJS mergeMap和switchMap

如何以及在何处使用 Transformations.switchMap?

了解rxjs中的SwitchMap

使用SwitchMap()处理取消先前的请求

switchMap 中 of(...) 和 [...] 的区别

map() 和 switchMap() 方法有啥区别?