MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular HTTP请求的缓存策略与实现

2023-08-016.4k 阅读

Angular HTTP 请求缓存策略概述

在前端开发中,网络请求往往是性能瓶颈之一。频繁的 HTTP 请求不仅会消耗用户的流量,还可能因为网络波动等原因导致请求失败或响应延迟。缓存策略能够有效地解决这些问题,通过存储和重用之前请求的结果,减少不必要的网络请求,提高应用程序的性能和响应速度。

在 Angular 中,HTTP 请求的缓存策略主要围绕着如何管理请求的响应数据,决定何时从缓存中获取数据,何时发起新的网络请求。这涉及到多个方面的考量,包括缓存的生命周期、缓存的一致性维护以及缓存的存储方式等。

基本缓存策略类型

  1. 强缓存 强缓存是指在缓存有效期内,浏览器直接从本地缓存中获取数据,而不会向服务器发送请求。在 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 请求,并在获取到响应后更新缓存。

  1. 协商缓存 协商缓存则是浏览器在发起请求时,会带上一些缓存相关的头信息(如 Last - ModifiedETag),服务器根据这些头信息判断资源是否有更新。如果资源未更新,服务器返回 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

缓存策略的应用场景

  1. 数据不经常变化的场景 对于一些基本配置信息、静态数据等不经常变化的数据,适合采用强缓存策略。例如,一个应用的全局配置信息,可能一个月才会更新一次。使用强缓存可以在缓存有效期内,每次请求都直接从本地获取数据,大大提高了响应速度。以获取应用配置信息为例:
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;
    });
  }
}
  1. 实时性要求不高,但偶尔需要更新的场景 协商缓存适用于这种场景。比如一个新闻列表页面,新闻内容可能会不定期更新,但用户不需要实时获取最新的新闻列表。每次请求新闻列表时,通过协商缓存,服务器可以判断新闻列表是否有更新,若未更新则直接让浏览器从本地缓存加载,若更新则返回新的列表数据。
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;
    });
  }
}
  1. 实时性要求高的场景 在实时性要求高的场景下,如股票行情、实时聊天消息等,缓存策略可能不太适用,因为需要保证获取到的数据是最新的。但即使在这种情况下,也可以采用一些折中的办法,例如设置较短的缓存有效期,在保证一定实时性的同时,减少部分不必要的请求。
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
        };
      })
    );
  }
}

缓存一致性维护

  1. 手动更新缓存 当数据发生变化时,需要手动更新缓存,以保证缓存数据与服务器数据的一致性。例如,在一个待办事项应用中,当用户完成一个待办事项并提交到服务器后,不仅要更新本地 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;
    });
  }
}
  1. 缓存失效机制 除了手动更新缓存,还可以设置缓存失效机制。例如,在一段时间后强制缓存失效,下次请求时获取最新数据。这可以通过设置缓存的过期时间来实现,如前面强缓存示例中的 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 列表缓存的逻辑
      }
    });
  }
}

缓存存储方式

  1. 内存缓存 内存缓存是指将缓存数据存储在应用程序的内存中。在 Angular 应用中,前面示例中的 CacheServiceETagService 就是使用内存缓存。内存缓存的优点是访问速度快,因为数据存储在内存中,不需要进行额外的磁盘 I/O 操作。但缺点是当应用程序关闭或刷新页面时,缓存数据会丢失。适合存储一些短期使用且不需要持久化的数据,如当前用户会话中的一些配置信息。
  2. 本地存储(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);
      })
    );
  }
}

会话存储适合存储一些只在当前会话中使用且不需要持久化的数据,如当前页面的临时配置。

复杂场景下的缓存策略

  1. 多个请求依赖相同数据 在实际应用中,可能会有多个组件或服务依赖相同的 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;
    });
  }
}
  1. 动态缓存策略 在一些复杂应用中,可能需要根据不同的条件动态选择缓存策略。例如,对于付费用户和免费用户,提供不同的缓存策略。付费用户可以享受更长时间的强缓存,而免费用户则采用较短时间的缓存或更多依赖协商缓存。
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;
    });
  }
}
  1. 缓存与拦截器结合 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 发起的请求都会经过这个缓存拦截器,实现统一的缓存管理。

缓存策略的性能优化

  1. 缓存命中率优化 缓存命中率是指缓存中能够直接提供所需数据的请求比例。为了提高缓存命中率,可以对缓存进行更精细的管理。例如,根据不同的请求参数来区分缓存。假设一个获取用户订单列表的 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 会预加载热门新闻列表并缓存,当用户访问热门新闻页面时,可以直接从缓存中获取数据,提高响应速度。

缓存策略的注意事项

  1. 安全性考虑 在使用缓存时,需要注意数据的安全性。特别是对于敏感数据,如用户的登录信息、金融数据等,不适合长时间缓存或使用客户端缓存。如果必须缓存,要确保缓存数据的加密存储和访问控制。例如,对于用户的登录令牌,可以采用加密算法对其进行加密后存储在本地缓存中,并且在每次使用时进行解密和验证。
  2. 服务器端配合 虽然客户端缓存策略可以提高应用性能,但在一些情况下,需要服务器端的配合。例如,协商缓存需要服务器正确处理 Last - ModifiedETag 等头信息。此外,服务器也可以通过设置合适的缓存控制头(如 Cache - Control)来指导客户端进行缓存。例如,服务器可以设置 Cache - Control: public, max - age = 3600 表示资源可以被公共缓存(如 CDN)缓存,且有效期为 3600 秒。
  3. 兼容性问题 不同的浏览器对缓存机制的支持可能存在差异。在使用缓存策略时,需要进行充分的兼容性测试。特别是在使用一些较新的缓存技术或特性时,要确保在主流浏览器上都能正常工作。例如,某些浏览器对本地存储的大小限制可能不同,在使用本地存储缓存时要考虑到这一点。

通过合理地设计和应用 Angular HTTP 请求的缓存策略,我们可以显著提升应用程序的性能、用户体验以及资源利用效率。从基本的缓存策略类型到复杂场景下的应用,再到性能优化和注意事项,每个环节都对实现高效的缓存机制至关重要。在实际开发中,需要根据具体的业务需求和应用场景,选择最合适的缓存策略,并不断优化和完善缓存管理。