Можете ли вы преобразовать результат switchMap observable, чтобы получить его значение, а не само наблюдаемое?

#angular #typescript #rxjs #rxjs-observables #switchmap

Вопрос:

Я пытаюсь создать синглтон в памяти, который содержит текущего поставщика, которого просматривает пользователь.

Защита используется на всех определенных маршрутах для перехвата параметра:

 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 amp;amp; v.displayName == name) {
      return v;
    }
    else {
      return this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => {
        this.vendor = v;
        return of(v)
        // ...
      }));
    }
  }))
  return resu<
}
 

Проблема в том , что мне нужно проверить vendor$ его значение, которое возвращает an Obervable<IUser> , но switchMap также возвращает an Obervable<IUser> , в результате чего результат будет Observable<Observable<IUser>> . Как я могу сделать result возврат видимым для одного пользователя?

Комментарии:

1. Вернитесь v вместо of(v)

2. return this.Get<IUser>(`api/vendor/${name}`).pipe всегда возвращает наблюдаемое, поэтому необходимо также использовать карту swtich в канале vendors$ this.vendor$.pipe(map(v => { , но имейте в виду, что вам необходимо преобразовать ненаблюдаемые результаты в вашей карте переключения в наблюдаемые

3. Дополнительно: Почему вы используете this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => { ? я не вижу сценария для switchmap, или ваш код просто короче, чем ... содержит реальную switchmap?

4. На вызов никогда не делается подписка getByName , потому что он вызывается из моего guard . Итак, я использую switchmap, который (как мне известно) выполняет внутреннюю подписку.

Ответ №1:

Я считаю, что вы неправильно понимаете случай использования switchMap() , потому что это не причина вашего Observable<Observable<IUser>> . Ваш первый map() оператор внутри getByName() либо возвращает значение типа IUser (когда оно равно true), либо наблюдаемое значение 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;
    })
  );
 

С помощью этой настройки ваш компонент(ы) amp; guard может подписаться vendor$ на получение IUser необходимых ему данных. Когда у вас есть имя, чтобы получить поставщика, вы можете вызвать getByName() . Это приведет к выполнению следующих действий:

  1. userName Субъект выдаст новое имя.
  2. vendor$ Наблюдаемое (подписанное на userName ) примет это имя, затем переключится на карту, чтобы получить значение из внутреннего наблюдаемого ( Get метод)
  3. shareReplay(1) Гарантирует, что любая подписка vendor$ (текущая или будущая) получит последнее (1) значение, выданное из памяти.

Если вам нужно обновить имя поставщика, просто введите getByName() свое новое имя, и все, на кого вы подписаны vendor$ , автоматически получат обновленное значение.

Комментарии:

1. Я не смог заставить его проверить, существует ли имя уже перед вызовом сервера. Я не хочу вызывать сервер на каждом угловом маршруте.

2. Извините, я обновил свой пример, чтобы он соответствовал вашему варианту использования. Я добавил distinctUntilChanged() это перед вызовом API. Это предотвращает выполнение повторяющихся вызовов, если vendorName выдается повторяющееся имя. Я также включил обновление guard, которое вызывает метод для выделения нового параметра, а затем возвращает ту же логику, основанную на последнем выделенном значении vendor$ .

3. Это сработало, НО мне все равно нужно было вручную добавить URL-адрес с именем поставщика в адресную строку браузера. Кроме того, у меня было странное поведение при навигации. Измените Subject значение на ReplaySubject<string>(1) так, чтобы оно отслеживало только одно значение.

Ответ №2:

в связи с тем, что я вижу, vendor$ — это поток пользователя, вот рефакторинг вашего кода с использованием оператора iif rxjs для подписки на первый или второй наблюдаемый на основе вашего состояния

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

Комментарии:

1. Это сработает только в том случае, если я соглашусь subscribe на это.

Ответ №3:

Я добавляю еще одно решение, которое сработало.

Видя, что canActivate и a Promise также возвращает, вы также можете использовать надежный старый async await метод.

Примечание: toPromise теперь lastValueFrom он находится в rxjs 7.

 async getByVendorName(name: string): Promise<IUser> {
  const v:IUser = await new Promise(resolve => { 
    this.vendor$.subscribe(a => {
      if (a != null amp;amp; 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);
}