【问题标题】:Angular HttpInterceptor Cache Not Triggering Change DetectionAngular HttpInterceptor 缓存未触发更改检测
【发布时间】:2021-04-09 04:45:05
【问题描述】:

更新了代码以澄清。 TVC 组件托管一个交易视图lightweight-charts 组件。

有一个带有项目列表的侧导航。每次选择一个新的/不同的项目时,它都会在主内容组件中触发 this.data.getDataForSymbol() 。图表在不使用缓存时重新呈现完美......但是当缓存被使用(并确认正在工作)......图表不会重新呈现。

这里是渲染图表的组件:

@Component({
  selector: 'tvc',
  template: '<div #chart></div>',
})
export class TvcComponent implements AfterViewInit {

  @ViewChild('chart') chartElem: ElementRef;

  @Input()
  data: (BarData | WhitespaceData)[] | null;

  chart: IChartApi = null;

  ngAfterViewInit() {
    this.buildChart();
  }

  buildChart() {
    this.chart = createChart(<HTMLElement>this.chartElem.nativeElement, {
      width: 600,
      height: 300,
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });

    this.chart.timeScale().fitContent();
    const candleSeries = this.chart.addCandlestickSeries();
    candleSeries.setData(this.data);
  }
}

这里是托管 TvcComponent 的组件,为图表提供数据:

@Component({
  selector: 'main-content',
  template: `
      <div *ngIf="monthly$ | async as monthly">
        <tvc
          [data]="monthly"
        ></tvc>
      </div>`
})
export class MainContentComponent implements OnInit {

  monthly$: Observable<any[]>;

  constructor(
    private route: ActivatedRoute,
    private itemStore: ItemStore,
    private data: DataService
  ) {}

  ngOnInit(): void {
    this.route.params.subscribe((params) => {
      let id = params['id'];
      this.itemStore.items$.subscribe((items) => {
        this.monthly$ = this.data.getDataForSymbol(id, 'monthly');
      });
    });
  }
}

拦截服务的相关代码如下:

@Injectable({ providedIn: 'root' })
export class CacheInterceptor implements HttpInterceptor {
  constructor(private cache: HttpCacheService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const cachedResponse = this.cache.get(req.urlWithParams);

    if (cachedResponse) {
      console.log(`${req.urlWithParams}: cached response`);
      return of(cachedResponse);
    }
    return next.handle(req).pipe(
      tap((event) => {
        if (event instanceof HttpResponse) {
          this.cache.put(req.urlWithParams, event);
          console.log(`${req.urlWithParams}: response from server`);
        }
      })
    );
  }
}

和缓存服务:

@Injectable()
export class HttpCacheService {
  private cache = {};

  get(url: string): HttpResponse<any> {
    return this.cache[url];
  }
  put(url: string, resp: HttpResponse<any>): void {
    this.cache[url] = resp;
  }
}

我已经为 caching 实现了一个 HttpInterceptor(来自 Angular Github 的示例),并且正在缓存 HttpResponse 以获取然后使用模板中的异步管道订阅的数据 - 并作为输入属性传递给子组件. observable 包含呈现图表的数据。

数据(大部分)是静态的,选择不同的项目会触发一个新的 Http 请求。因此,如果有人在多个图表之间来回跳动,他们将不必要地进行多次(重复)调用。因此,缓存。

问题在于,虽然通过控制台日志记录确定缓存工作得很好)...从缓存访问数据时,图表不会更新/重新渲染。第一次选择项目 A 时,它会从服务器获取数据并正确呈现。如果您移动选择项 B(不在缓存中),它会发出服务器请求,将响应放入缓存中,并呈现正确的图形。问题是如果您切换回项目 A,它会从缓存中获取正确的数据,但不会更新图表。

我正在使用默认更改检测。

【问题讨论】:

  • 需要 Mmmm 代码来检查您的刷新发生了什么......
  • 我认为这根本不是变更检测问题。我猜问题是 tvc 组件对数据的变化没有反应。它只会在重新创建时更新数据(ngAfterViewInit 只调用一次),这在您描述的情况下可能不会发生。如果它实际上是更改检测,那么在加载数据后调用angular.io/api/core/ChangeDetectorRef#detectChanges 将解决问题。如果不是更改检测,那么您可能需要在 tvc.data 上设置一个 setter 来更新图表
  • 感谢@x4rf41 的输入。它帮助我更接近解决方案。我注意到一件有趣的事情......当来自this.monthly$ = this.data.getDataForSymbol(id, 'monthly');(在上面的 MainContentComponent 中)的响应不在缓存中(因此发出 HTTP 请求)时,ngAfterViewInit 会触发。当同一个调用从缓存中返回数据时,ngAfterViewInit 不会触发。如何确定导致 ngAfterViewInit 触发的原因?
  • ngAfterViewInit 仅在创建 tvc 组件时触发。这只会发生在*ngIf="monthly$ | async as monthly" 从 null 更改为某个值或父组件由于路由更改而创建但仅当路由从另一个组件更改时才会发生,而不是仅当参数更改时才会发生,我猜会发生什么在你的情况下)

标签: angular caching rxjs tradingview-api lightweight-charts


【解决方案1】:

我假设 monthly$: Observable&lt;any[]&gt; 变量从您编写的内容中正确更改,并且 Observable 获得了新值(您可以通过记录来检查)。如果是这种情况,那么 [data]="monthly" 绑定将被更改检测正确更新。

这意味着,您的问题是 tvc 组件没有正确更新,因为它不会对 @Input() data 中的更改做出反应。如果您将组件更改为以下内容,它应该可以工作:

(我可能有语法错误,因为你没有提供有效的示例代码,所以我写了这个但无法检查它)

@Component({
  selector: 'tvc',
  template: '<div #chart></div>',
})
export class TvcComponent implements AfterViewInit {

  @ViewChild('chart') chartElem: ElementRef;
  
  private _data: (BarData | WhitespaceData)[] | null;

  get data(): (BarData | WhitespaceData)[] | null {
     return this._data;
  }

  @Input()
  set data(value: (BarData | WhitespaceData)[] | null) {
     // this gets called by the change-detection when the value of monthly changes from the [data]="monthly" binding
     // with that, we can use it to refresh the data
     // because the data is not bound to the chart by angular through a template, we have to do it manually. the change-detection only goes so far
     this._data = value;
     this.refreshData(); 
  }

  chart: IChartApi = null;
  candleSeries: any = null; // I don't know the correct type so I use any. You should change that

  ngAfterViewInit() {
    this.buildChart();
    this.refreshData();
  }

  buildChart() {
    this.chart = createChart(<HTMLElement>this.chartElem.nativeElement, {
      width: 600,
      height: 300,
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });

    this.chart.timeScale().fitContent();
    this.candleSeries = this.chart.addCandlestickSeries();
  }

  refreshData() {
    if (!this.candleSeries) return; // might not be initialized yet
    // I don't know the library, so I can't be sure that this is enough to update the data. 
    // You may have to do more. You can put a log here to see if it triggers
    this.candleSeries.setData(this.data);
  }
}

我希望这对你有用。只需确保在更改数据时正确调用 data setter。然后可以在refreshData() 方法中处理其余部分

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-02
    • 1970-01-01
    • 2019-08-15
    • 2023-04-09
    • 2018-10-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多