#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()
. Это приведет к выполнению следующих действий:
userName
Субъект выдаст новое имя.vendor$
Наблюдаемое (подписанное наuserName
) примет это имя, затем переключится на карту, чтобы получить значение из внутреннего наблюдаемого (Get
метод)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);
}