Angular HTTP请求的缓存策略与实现
Angular HTTP 请求缓存策略概述
在前端开发中,网络请求往往是性能瓶颈之一。频繁的 HTTP 请求不仅会消耗用户的流量,还可能因为网络波动等原因导致请求失败或响应延迟。缓存策略能够有效地解决这些问题,通过存储和重用之前请求的结果,减少不必要的网络请求,提高应用程序的性能和响应速度。
在 Angular 中,HTTP 请求的缓存策略主要围绕着如何管理请求的响应数据,决定何时从缓存中获取数据,何时发起新的网络请求。这涉及到多个方面的考量,包括缓存的生命周期、缓存的一致性维护以及缓存的存储方式等。
基本缓存策略类型
- 强缓存 强缓存是指在缓存有效期内,浏览器直接从本地缓存中获取数据,而不会向服务器发送请求。在 Angular 中,虽然没有直接内置针对 HTTP 请求的强缓存机制,但我们可以通过一些自定义的方式来模拟实现。例如,我们可以在服务中维护一个缓存对象,记录每个请求的响应数据以及缓存的过期时间。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache: { [url: string]: { data: any; expiration: number } } = {};
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<T> {
const cached = this.cache[url];
if (cached && cached.expiration > Date.now()) {
return of(cached.data);
}
return this.http.get<T>(url).pipe(
tap(data => {
this.cache[url] = {
data,
expiration: Date.now() + this.cacheDuration
};
})
);
}
}
在上述代码中,CacheService
维护了一个 cache
对象,键为请求的 URL,值为包含响应数据 data
和过期时间 expiration
的对象。当请求一个 URL 时,首先检查缓存中是否存在且未过期,如果是则直接返回缓存数据,否则发起 HTTP 请求,并在获取到响应后更新缓存。
- 协商缓存
协商缓存则是浏览器在发起请求时,会带上一些缓存相关的头信息(如
Last - Modified
、ETag
),服务器根据这些头信息判断资源是否有更新。如果资源未更新,服务器返回 304 Not Modified 状态码,浏览器从本地缓存中加载数据;如果资源已更新,服务器返回最新的数据。
在 Angular 中,我们可以通过设置 HttpClient
的请求头来实现协商缓存的部分功能。例如,假设服务器支持 ETag
验证:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ETagService {
private etagCache: { [url: string]: string } = {};
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<T> {
const headers = new HttpHeaders();
const cachedETag = this.etagCache[url];
if (cachedETag) {
headers.set('If - None - Match', cachedETag);
}
return this.http.get<T>(url, { headers }).pipe(
map(response => {
const newETag = response.headers.get('ETag');
if (newETag) {
this.etagCache[url] = newETag;
}
return response;
})
);
}
}
上述代码中,ETagService
维护了一个 etagCache
对象,存储每个 URL 对应的 ETag
。在每次请求时,将缓存的 ETag
添加到 If - None - Match
请求头中。服务器根据这个头信息判断资源是否更新,如果未更新返回 304 状态码,Angular 会根据响应状态处理数据(如从本地缓存加载)。如果服务器返回新数据,更新 etagCache
中的 ETag
。
缓存策略的应用场景
- 数据不经常变化的场景 对于一些基本配置信息、静态数据等不经常变化的数据,适合采用强缓存策略。例如,一个应用的全局配置信息,可能一个月才会更新一次。使用强缓存可以在缓存有效期内,每次请求都直接从本地获取数据,大大提高了响应速度。以获取应用配置信息为例:
import { Component } from '@angular/core';
import { CacheService } from './cache.service';
@Component({
selector: 'app - config',
templateUrl: './config.component.html'
})
export class ConfigComponent {
config: any;
constructor(private cacheService: CacheService) {
this.cacheService.get('/api/config').subscribe(data => {
this.config = data;
});
}
}
- 实时性要求不高,但偶尔需要更新的场景 协商缓存适用于这种场景。比如一个新闻列表页面,新闻内容可能会不定期更新,但用户不需要实时获取最新的新闻列表。每次请求新闻列表时,通过协商缓存,服务器可以判断新闻列表是否有更新,若未更新则直接让浏览器从本地缓存加载,若更新则返回新的列表数据。
import { Component } from '@angular/core';
import { ETagService } from './etag.service';
@Component({
selector: 'app - news - list',
templateUrl: './news - list.component.html'
})
export class NewsListComponent {
newsList: any[];
constructor(private etagService: ETagService) {
this.etagService.get('/api/news').subscribe(response => {
this.newsList = response;
});
}
}
- 实时性要求高的场景 在实时性要求高的场景下,如股票行情、实时聊天消息等,缓存策略可能不太适用,因为需要保证获取到的数据是最新的。但即使在这种情况下,也可以采用一些折中的办法,例如设置较短的缓存有效期,在保证一定实时性的同时,减少部分不必要的请求。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RealTimeCacheService {
private cache: { [url: string]: { data: any; expiration: number } } = {};
private cacheDuration = 5 * 1000; // 缓存有效期 5 秒
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<T> {
const cached = this.cache[url];
if (cached && cached.expiration > Date.now()) {
return of(cached.data);
}
return this.http.get<T>(url).pipe(
tap(data => {
this.cache[url] = {
data,
expiration: Date.now() + this.cacheDuration
};
})
);
}
}
缓存一致性维护
- 手动更新缓存 当数据发生变化时,需要手动更新缓存,以保证缓存数据与服务器数据的一致性。例如,在一个待办事项应用中,当用户完成一个待办事项并提交到服务器后,不仅要更新本地 UI,还要更新缓存中的待办事项列表数据。
import { Component } from '@angular/core';
import { CacheService } from './cache.service';
@Component({
selector: 'app - todo - list',
templateUrl: './todo - list.component.html'
})
export class TodoListComponent {
todoList: any[];
constructor(private cacheService: CacheService) {
this.cacheService.get('/api/todo').subscribe(data => {
this.todoList = data;
});
}
completeTodo(todo: any) {
// 提交完成任务到服务器的逻辑
// 假设成功后更新缓存
this.cacheService.get('/api/todo').subscribe(data => {
this.todoList = data;
});
}
}
- 缓存失效机制
除了手动更新缓存,还可以设置缓存失效机制。例如,在一段时间后强制缓存失效,下次请求时获取最新数据。这可以通过设置缓存的过期时间来实现,如前面强缓存示例中的
cacheDuration
参数。另外,也可以通过服务器推送的方式通知客户端缓存失效。假设服务器支持 WebSocket 推送,当数据更新时,服务器通过 WebSocket 向客户端发送缓存失效的消息,客户端接收到消息后清除相应的缓存。
import { Injectable } from '@angular/core';
import { WebSocketSubject } from 'rxjs/webSocket';
@Injectable({
providedIn: 'root'
})
export class CacheInvalidationService {
private webSocket: WebSocketSubject<any>;
constructor() {
this.webSocket = new WebSocketSubject('ws://localhost:8080/cache - invalidate');
this.webSocket.subscribe(message => {
// 根据消息内容清除相应的缓存
if (message.type === 'todo - list - invalidate') {
// 假设这里有清除 todo 列表缓存的逻辑
}
});
}
}
缓存存储方式
- 内存缓存
内存缓存是指将缓存数据存储在应用程序的内存中。在 Angular 应用中,前面示例中的
CacheService
和ETagService
就是使用内存缓存。内存缓存的优点是访问速度快,因为数据存储在内存中,不需要进行额外的磁盘 I/O 操作。但缺点是当应用程序关闭或刷新页面时,缓存数据会丢失。适合存储一些短期使用且不需要持久化的数据,如当前用户会话中的一些配置信息。 - 本地存储(Local Storage) 本地存储允许在浏览器端持久化存储数据,数据存储在用户的本地磁盘上,即使关闭浏览器或刷新页面,数据依然存在。在 Angular 中,可以将缓存数据存储在本地存储中。例如:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class LocalStorageCacheService {
private cacheKeyPrefix = 'app - cache - ';
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<T> {
const cacheKey = this.cacheKeyPrefix + url;
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { data, expiration } = JSON.parse(cached);
if (expiration > Date.now()) {
return of(data);
}
}
return this.http.get<T>(url).pipe(
tap(data => {
const cacheValue = JSON.stringify({
data,
expiration: Date.now() + this.cacheDuration
});
localStorage.setItem(cacheKey, cacheValue);
})
);
}
}
本地存储的优点是数据持久化,适合存储一些不需要频繁更新且需要长期保存的数据,如用户的个性化配置。但缺点是本地存储有大小限制(一般在 5MB 左右),并且读取和写入操作相对内存缓存较慢,因为涉及到磁盘 I/O。
3. 会话存储(Session Storage)
会话存储与本地存储类似,但数据仅在当前会话(浏览器标签页打开期间)内有效。当关闭标签页时,会话存储中的数据会被清除。在 Angular 中实现会话存储缓存与本地存储类似,只是使用 sessionStorage
对象代替 localStorage
。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class SessionStorageCacheService {
private cacheKeyPrefix = 'app - session - cache - ';
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<T> {
const cacheKey = this.cacheKeyPrefix + url;
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
const { data, expiration } = JSON.parse(cached);
if (expiration > Date.now()) {
return of(data);
}
}
return this.http.get<T>(url).pipe(
tap(data => {
const cacheValue = JSON.stringify({
data,
expiration: Date.now() + this.cacheDuration
});
sessionStorage.setItem(cacheKey, cacheValue);
})
);
}
}
会话存储适合存储一些只在当前会话中使用且不需要持久化的数据,如当前页面的临时配置。
复杂场景下的缓存策略
- 多个请求依赖相同数据 在实际应用中,可能会有多个组件或服务依赖相同的 HTTP 请求数据。例如,一个电商应用中,商品详情页和购物车组件都需要获取商品的基本信息。如果每个组件都独立发起请求,会造成不必要的网络开销。我们可以通过共享缓存服务来解决这个问题。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class SharedCacheService {
private cache: { [url: string]: { data: any; expiration: number } } = {};
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
constructor(private http: HttpClient) {}
get<T>(url: string): Observable<T> {
const cached = this.cache[url];
if (cached && cached.expiration > Date.now()) {
return of(cached.data);
}
return this.http.get<T>(url).pipe(
tap(data => {
this.cache[url] = {
data,
expiration: Date.now() + this.cacheDuration
};
})
);
}
}
在商品详情组件和购物车组件中,可以注入同一个 SharedCacheService
来获取商品基本信息:
import { Component } from '@angular/core';
import { SharedCacheService } from './shared - cache.service';
@Component({
selector: 'app - product - detail',
templateUrl: './product - detail.component.html'
})
export class ProductDetailComponent {
product: any;
constructor(private sharedCacheService: SharedCacheService) {
this.sharedCacheService.get('/api/product/1').subscribe(data => {
this.product = data;
});
}
}
import { Component } from '@angular/core';
import { SharedCacheService } from './shared - cache.service';
@Component({
selector: 'app - cart',
templateUrl: './cart.component.html'
})
export class CartComponent {
product: any;
constructor(private sharedCacheService: SharedCacheService) {
this.sharedCacheService.get('/api/product/1').subscribe(data => {
this.product = data;
});
}
}
- 动态缓存策略 在一些复杂应用中,可能需要根据不同的条件动态选择缓存策略。例如,对于付费用户和免费用户,提供不同的缓存策略。付费用户可以享受更长时间的强缓存,而免费用户则采用较短时间的缓存或更多依赖协商缓存。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DynamicCacheService {
private cache: { [url: string]: { data: any; expiration: number } } = {};
private paidUserCacheDuration = 3600 * 1000; // 付费用户缓存有效期 1 小时
private freeUserCacheDuration = 60 * 1000; // 免费用户缓存有效期 60 秒
constructor(private http: HttpClient) {}
get<T>(url: string, isPaidUser: boolean): Observable<T> {
const cached = this.cache[url];
const cacheDuration = isPaidUser? this.paidUserCacheDuration : this.freeUserCacheDuration;
if (cached && cached.expiration > Date.now()) {
return of(cached.data);
}
return this.http.get<T>(url).pipe(
tap(data => {
this.cache[url] = {
data,
expiration: Date.now() + cacheDuration
};
})
);
}
}
在组件中,可以根据用户的付费状态选择合适的缓存策略:
import { Component } from '@angular/core';
import { DynamicCacheService } from './dynamic - cache.service';
@Component({
selector: 'app - content',
templateUrl: './content.component.html'
})
export class ContentComponent {
content: any;
isPaidUser = true; // 假设用户是付费用户
constructor(private dynamicCacheService: DynamicCacheService) {
this.dynamicCacheService.get('/api/content', this.isPaidUser).subscribe(data => {
this.content = data;
});
}
}
- 缓存与拦截器结合 Angular 的 HTTP 拦截器可以在请求发送前和响应接收后进行一些通用的处理。我们可以将缓存策略与拦截器结合,实现更统一和灵活的缓存管理。例如,创建一个缓存拦截器,在每次请求前检查缓存,若缓存存在且有效则直接返回缓存数据,否则放行请求并在响应后更新缓存。
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache: { [url: string]: { data: any; expiration: number } } = {};
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const cached = this.cache[request.url];
if (cached && cached.expiration > Date.now()) {
return of(cached.data);
}
return next.handle(request).pipe(
map(response => {
this.cache[request.url] = {
data: response,
expiration: Date.now() + this.cacheDuration
};
return response;
})
);
}
}
在 app.module.ts
中注册拦截器:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { CacheInterceptor } from './cache - interceptor';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
这样,所有通过 HttpClient
发起的请求都会经过这个缓存拦截器,实现统一的缓存管理。
缓存策略的性能优化
- 缓存命中率优化 缓存命中率是指缓存中能够直接提供所需数据的请求比例。为了提高缓存命中率,可以对缓存进行更精细的管理。例如,根据不同的请求参数来区分缓存。假设一个获取用户订单列表的 API,支持按时间范围查询订单:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class OrderCacheService {
private cache: { [key: string]: { data: any; expiration: number } } = {};
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
constructor(private http: HttpClient) {}
getOrders(startDate: string, endDate: string): Observable<any> {
const cacheKey = `orders - ${startDate}-${endDate}`;
const cached = this.cache[cacheKey];
if (cached && cached.expiration > Date.now()) {
return of(cached.data);
}
const url = `/api/orders?startDate=${startDate}&endDate=${endDate}`;
return this.http.get(url).pipe(
tap(data => {
this.cache[cacheKey] = {
data,
expiration: Date.now() + this.cacheDuration
};
})
);
}
}
通过这种方式,不同时间范围的订单请求会有不同的缓存,避免了缓存冲突,提高了缓存命中率。 2. 缓存清理与内存管理 随着应用程序的运行,缓存数据可能会不断增加,占用大量内存。因此,需要定期清理缓存,特别是对于长时间未使用的缓存数据。可以通过维护一个缓存使用记录,定期检查并删除长时间未被访问的缓存。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class MemoryCleanCacheService {
private cache: { [url: string]: { data: any; expiration: number; lastAccessed: number } } = {};
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
private cleanInterval = 60 * 1000; // 清理间隔 60 秒
constructor(private http: HttpClient) {
setInterval(() => {
const now = Date.now();
Object.keys(this.cache).forEach(url => {
const { expiration, lastAccessed } = this.cache[url];
if (now - lastAccessed > this.cleanInterval || now > expiration) {
delete this.cache[url];
}
});
}, this.cleanInterval);
}
get<T>(url: string): Observable<T> {
const cached = this.cache[url];
if (cached && cached.expiration > Date.now()) {
this.cache[url].lastAccessed = Date.now();
return of(cached.data);
}
return this.http.get<T>(url).pipe(
tap(data => {
this.cache[url] = {
data,
expiration: Date.now() + this.cacheDuration,
lastAccessed: Date.now()
};
})
);
}
}
上述代码中,每次访问缓存时更新 lastAccessed
时间,通过 setInterval
定期检查并删除长时间未访问或已过期的缓存数据,有效管理内存使用。
3. 缓存预加载
对于一些用户经常访问的数据,可以采用缓存预加载的方式,在应用程序启动时或空闲时间提前请求并缓存这些数据。例如,一个新闻应用可以在启动时预加载热门新闻列表。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class NewsPreloadCacheService {
private cache: { [url: string]: { data: any; expiration: number } } = {};
private cacheDuration = 60 * 1000; // 缓存有效期 60 秒
constructor(private http: HttpClient) {
this.preloadNews();
}
private preloadNews() {
const url = '/api/hot - news';
this.http.get(url).pipe(
tap(data => {
this.cache[url] = {
data,
expiration: Date.now() + this.cacheDuration
};
})
).subscribe();
}
getHotNews(): Observable<any> {
const cached = this.cache['/api/hot - news'];
if (cached && cached.expiration > Date.now()) {
return of(cached.data);
}
const url = '/api/hot - news';
return this.http.get(url).pipe(
tap(data => {
this.cache[url] = {
data,
expiration: Date.now() + this.cacheDuration
};
})
);
}
}
在应用启动时,NewsPreloadCacheService
会预加载热门新闻列表并缓存,当用户访问热门新闻页面时,可以直接从缓存中获取数据,提高响应速度。
缓存策略的注意事项
- 安全性考虑 在使用缓存时,需要注意数据的安全性。特别是对于敏感数据,如用户的登录信息、金融数据等,不适合长时间缓存或使用客户端缓存。如果必须缓存,要确保缓存数据的加密存储和访问控制。例如,对于用户的登录令牌,可以采用加密算法对其进行加密后存储在本地缓存中,并且在每次使用时进行解密和验证。
- 服务器端配合
虽然客户端缓存策略可以提高应用性能,但在一些情况下,需要服务器端的配合。例如,协商缓存需要服务器正确处理
Last - Modified
、ETag
等头信息。此外,服务器也可以通过设置合适的缓存控制头(如Cache - Control
)来指导客户端进行缓存。例如,服务器可以设置Cache - Control: public, max - age = 3600
表示资源可以被公共缓存(如 CDN)缓存,且有效期为 3600 秒。 - 兼容性问题 不同的浏览器对缓存机制的支持可能存在差异。在使用缓存策略时,需要进行充分的兼容性测试。特别是在使用一些较新的缓存技术或特性时,要确保在主流浏览器上都能正常工作。例如,某些浏览器对本地存储的大小限制可能不同,在使用本地存储缓存时要考虑到这一点。
通过合理地设计和应用 Angular HTTP 请求的缓存策略,我们可以显著提升应用程序的性能、用户体验以及资源利用效率。从基本的缓存策略类型到复杂场景下的应用,再到性能优化和注意事项,每个环节都对实现高效的缓存机制至关重要。在实际开发中,需要根据具体的业务需求和应用场景,选择最合适的缓存策略,并不断优化和完善缓存管理。