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;
      }),
  );

Source,

如您所见,如果 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();
}