Angular HttpInterceptor 缓存未触发更改检测
Angular HttpInterceptor Cache Not Triggering Change Detection
更新了代码以阐明。 TVC 组件托管交易视图 lightweight-charts 组件。
有一个带有项目列表的侧面导航。每次 new/different 项被 selected 时,它都会在主内容组件中触发 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,然后在模板 - 并作为输入 属性 传递给子组件。可观察对象包含呈现图表的数据。
数据(大部分)是静态的,select处理不同的项目会触发新的 Http 请求。因此,如果有人在多个图表之间来回跳动,他们将不必要地进行多次(重复)调用。因此,缓存。
问题是,虽然通过控制台日志记录确定缓存工作得很好)...当从缓存访问数据时,图表不会 update/re-render。第一次 select 项目 A,它从服务器获取数据并正确呈现。如果您移动 select 项目 B(不在缓存中),它会发出服务器请求,将响应放入缓存中,并呈现正确的图形。问题是如果您切换回项目 A,它会从缓存中获取正确的数据,但不会更新图形。
我正在使用默认的更改检测。
我假设 monthly$: Observable<any[]>
变量根据您编写的内容进行了正确更改,并且 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()
方法
中处理了
更新了代码以阐明。 TVC 组件托管交易视图 lightweight-charts 组件。
有一个带有项目列表的侧面导航。每次 new/different 项被 selected 时,它都会在主内容组件中触发 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,然后在模板 - 并作为输入 属性 传递给子组件。可观察对象包含呈现图表的数据。
数据(大部分)是静态的,select处理不同的项目会触发新的 Http 请求。因此,如果有人在多个图表之间来回跳动,他们将不必要地进行多次(重复)调用。因此,缓存。
问题是,虽然通过控制台日志记录确定缓存工作得很好)...当从缓存访问数据时,图表不会 update/re-render。第一次 select 项目 A,它从服务器获取数据并正确呈现。如果您移动 select 项目 B(不在缓存中),它会发出服务器请求,将响应放入缓存中,并呈现正确的图形。问题是如果您切换回项目 A,它会从缓存中获取正确的数据,但不会更新图形。
我正在使用默认的更改检测。
我假设 monthly$: Observable<any[]>
变量根据您编写的内容进行了正确更改,并且 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()
方法