Angular 可观察解析器完成得太早
Angular resolver observable completes too early
我正在将主题作为可观察对象从服务传递到解析器。当一个 http 请求完成或当它发现它需要的数据被缓存时,该服务可能会完成可观察的。在第二种情况下,在缓存数据的情况下,由于没有发生路由转换,因此 observable 似乎过早完成。如果我把 requestSubject.complete() 放在一个 setTimeout 中,有一些超时,它会工作,但否则它不会。
//resolve function from resolver
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const id = +route.paramMap.get('id');
const language = route.queryParamMap.get('language');
const request = this.service.get(id, language);
request.pipe(share()).subscribe(
() => {},
() => { this.router.navigateByUrl(AppRoutingConfig.search); });
return request;
}
//the service get function
get(id: number, language: string): Observable<any> {
const requestSubject = new Subject();
if (!isDataCached(id)) {
const url = `some url`;
const sub = this.http.get(url).subscribe((item: any) => {
requestSubject.next(1);
}, () => {
requestSubject.error(0);
}, () => {
requestSubject.complete();
});
} else {
requestSubject.complete();
}
return requestSubject.asObservable();
}
looks like the observable completes too early because the route transition does not happen
我会说 Subject 什么都不做。
如果数据未缓存,requestSubject.complete();
将 运行,这将 同步 向其所有订阅者发出完整通知。
这意味着 return 在 requestSubject
之后不会执行任何操作,因为 requestSubject
不会发出值。
它在数据未缓存时起作用,因为在 Subject 发出并完成时,它已经有一个订阅者可以决定路由转换是否应该继续。
例如,这就是 Angular 内部订阅 resolve
属性 中提供的所有可观察对象的方式:
return from(keys).pipe(
mergeMap(
(key: string) => getResolver(resolve[key], futureARS, futureRSS, moduleInjector)
.pipe(tap((value: any) => {
data[key] = value;
}))),
takeLast(1),
mergeMap(() => {
// Ensure all resolvers returned values, otherwise don't emit any "next" and just complete
// the chain which will cancel navigation
if (Object.keys(data).length === keys.length) {
return of(data);
}
return EMPTY;
}),
);
如您所见,如果 resolve()
fn 没有发出任何值,将发出 EMPTY
可观察值,这将导致导航被取消。
我还要稍微修改一下您的 resolve
函数:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const id = +route.paramMap.get('id');
const language = route.queryParamMap.get('language');
const request$ = this.service.get(id, language);
return request$.pipe(
tap(
null,
err => this.router.navigateByUrl(AppRoutingConfig.search),
),
);
}
我认为 share()
没有必要,因为您只有 一个订阅者。
所以,一个快速修复(我根本不推荐)我认为在这种情况下也发送缓存值:
// Inside `service.get()`
else {
// The value is cached
of(getCachedValue(id)).pipe(
subscribeOn(asyncScheduler) // delay(0) would also work
).subscribe(v => {
requestSubject.next(v)
requestSubject.complete()
})
}
return requestSubject.
另一种解决方案是考虑在 interceptors
级别添加缓存层,因为我认为这会大大简化您的代码。如果你决定切换到这种方法,你的 resolve()
fn 可以 return 类似 this.service.get(...)
的东西,因为每件事都会被拦截器 拦截 所以你可以 return 从那里缓存值。
在您的情况下,您需要使用 ReplaySubject
而不是 Subject
,它会记住最后一个值并在有人订阅后发出它。
尽管如此,我还是会更改 get()
以对流使用缓存状态。
//resolve function from resolver
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const id = +route.paramMap.get('id');
const language = route.queryParamMap.get('language');
return this.service.get(id, language).pipe(
catchError(() => {
this.router.navigateByUrl(AppRoutingConfig.search);
return EMPTY; // to suppress the stream
}),
);
}
//the service get function
private readonly cache = new Map(); // our cache for urls
get(id: number, language: string): Observable<any> {
const url = `some url`;
// if we had the id - we return its observable.
if (this.cache.has(id)) {
return cache.get(id);
}
// otherwise we create a replay subject.
const status$ = new ReplaySubject(1);
this.cache.set(id, status$);
this.http.get(url).subscribe(
() => status$.next(),
e => status$.error(e),
() => requestSubject.complete(),
);
return status$.asObservable();
}
我正在将主题作为可观察对象从服务传递到解析器。当一个 http 请求完成或当它发现它需要的数据被缓存时,该服务可能会完成可观察的。在第二种情况下,在缓存数据的情况下,由于没有发生路由转换,因此 observable 似乎过早完成。如果我把 requestSubject.complete() 放在一个 setTimeout 中,有一些超时,它会工作,但否则它不会。
//resolve function from resolver
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const id = +route.paramMap.get('id');
const language = route.queryParamMap.get('language');
const request = this.service.get(id, language);
request.pipe(share()).subscribe(
() => {},
() => { this.router.navigateByUrl(AppRoutingConfig.search); });
return request;
}
//the service get function
get(id: number, language: string): Observable<any> {
const requestSubject = new Subject();
if (!isDataCached(id)) {
const url = `some url`;
const sub = this.http.get(url).subscribe((item: any) => {
requestSubject.next(1);
}, () => {
requestSubject.error(0);
}, () => {
requestSubject.complete();
});
} else {
requestSubject.complete();
}
return requestSubject.asObservable();
}
looks like the observable completes too early because the route transition does not happen
我会说 Subject 什么都不做。
如果数据未缓存,requestSubject.complete();
将 运行,这将 同步 向其所有订阅者发出完整通知。
这意味着 return 在 requestSubject
之后不会执行任何操作,因为 requestSubject
不会发出值。
它在数据未缓存时起作用,因为在 Subject 发出并完成时,它已经有一个订阅者可以决定路由转换是否应该继续。
例如,这就是 Angular 内部订阅 resolve
属性 中提供的所有可观察对象的方式:
return from(keys).pipe(
mergeMap(
(key: string) => getResolver(resolve[key], futureARS, futureRSS, moduleInjector)
.pipe(tap((value: any) => {
data[key] = value;
}))),
takeLast(1),
mergeMap(() => {
// Ensure all resolvers returned values, otherwise don't emit any "next" and just complete
// the chain which will cancel navigation
if (Object.keys(data).length === keys.length) {
return of(data);
}
return EMPTY;
}),
);
如您所见,如果 resolve()
fn 没有发出任何值,将发出 EMPTY
可观察值,这将导致导航被取消。
我还要稍微修改一下您的 resolve
函数:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const id = +route.paramMap.get('id');
const language = route.queryParamMap.get('language');
const request$ = this.service.get(id, language);
return request$.pipe(
tap(
null,
err => this.router.navigateByUrl(AppRoutingConfig.search),
),
);
}
我认为 share()
没有必要,因为您只有 一个订阅者。
所以,一个快速修复(我根本不推荐)我认为在这种情况下也发送缓存值:
// Inside `service.get()`
else {
// The value is cached
of(getCachedValue(id)).pipe(
subscribeOn(asyncScheduler) // delay(0) would also work
).subscribe(v => {
requestSubject.next(v)
requestSubject.complete()
})
}
return requestSubject.
另一种解决方案是考虑在 interceptors
级别添加缓存层,因为我认为这会大大简化您的代码。如果你决定切换到这种方法,你的 resolve()
fn 可以 return 类似 this.service.get(...)
的东西,因为每件事都会被拦截器 拦截 所以你可以 return 从那里缓存值。
在您的情况下,您需要使用 ReplaySubject
而不是 Subject
,它会记住最后一个值并在有人订阅后发出它。
尽管如此,我还是会更改 get()
以对流使用缓存状态。
//resolve function from resolver
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const id = +route.paramMap.get('id');
const language = route.queryParamMap.get('language');
return this.service.get(id, language).pipe(
catchError(() => {
this.router.navigateByUrl(AppRoutingConfig.search);
return EMPTY; // to suppress the stream
}),
);
}
//the service get function
private readonly cache = new Map(); // our cache for urls
get(id: number, language: string): Observable<any> {
const url = `some url`;
// if we had the id - we return its observable.
if (this.cache.has(id)) {
return cache.get(id);
}
// otherwise we create a replay subject.
const status$ = new ReplaySubject(1);
this.cache.set(id, status$);
this.http.get(url).subscribe(
() => status$.next(),
e => status$.error(e),
() => requestSubject.complete(),
);
return status$.asObservable();
}