Найти в Дзене
Frontend Campus

Кэширование на стороне клиента с помощью Interceptor

Еще больше интересного в нашем телеграм канале - @frontend_campus Angular известен огромным количеством встроенных функций, но иногда мы используем лишь некоторые из них, не зная, каким может быть их полный потенциал. Сегодняшняя проблема Представим себе, что у нас есть часть данных, которую мы получаем снова и снова. Например, мы можем создать приложение для библиотеки фотографий, в котором будут отображаться альбомы пользователя. Обычно пользователи перемещаются по приложению туда-сюда, просматривая свои фотографии. Без кэширования это означает, что каждый раз, когда пользователь обращается к альбому, все данные о каждой фотографии должны быть получены с сервера заново. Это как раз то, что мы можем улучшить! Начальное состояние Вот наше приложение в действии: Пока что оно просто отображает количество фотографий в каждом альбоме. Но если мы посмотрим на вкладку "Сеть", то увидим, что каждый раз, когда мы обращаемся к альбому, приложение снова извлекает информацию о нем, даже если мы т
Оглавление
Еще больше интересного в нашем телеграм канале - @frontend_campus

Angular известен огромным количеством встроенных функций, но иногда мы используем лишь некоторые из них, не зная, каким может быть их полный потенциал.

Сегодняшняя проблема

Представим себе, что у нас есть часть данных, которую мы получаем снова и снова.

Например, мы можем создать приложение для библиотеки фотографий, в котором будут отображаться альбомы пользователя. Обычно пользователи перемещаются по приложению туда-сюда, просматривая свои фотографии.

Без кэширования это означает, что каждый раз, когда пользователь обращается к альбому, все данные о каждой фотографии должны быть получены с сервера заново.

Это как раз то, что мы можем улучшить!

Начальное состояние

Вот наше приложение в действии:

Пока что оно просто отображает количество фотографий в каждом альбоме. Но если мы посмотрим на вкладку "Сеть", то увидим, что каждый раз, когда мы обращаемся к альбому, приложение снова извлекает информацию о нем, даже если мы только что щелкнули по нему.

Поскольку эти альбомы принадлежат пользователю, есть вероятность, что любые изменения будут происходить с его стороны. Пока они просто просматривают страницы, это может мало что изменить.

Похоже, что здесь может помочь кэширование !

Привлечение Interceptor на помощь

Чтобы добавить дополнительное поведение в HttpClient во время обработки запросов, нам нужно создать новый перехватчик для определения связанной с ним логики.

Перехватчик - это особый тип сервиса, который позволяет нам настроить в своем роде слой, через который будет проходить HTTP-запрос, прежде чем он будет отправлен на сервер.

Для кэширования это означает, что мы можем перехватить запрос, если уже знаем, каким будет ответ.

Создание Interceptor

Чтобы определить перехватчик, нам сначала нужно создать и зарегистрировать его.

Перехватчик имеет тип HttpInterceptorFn, который принимает запрос и следующий обработчик в конвейере в качестве параметров и возвращает результат в виде Observable HttpEvent:

📁 app/caching.interceptor.ts

export const cachingInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
// For now, this is just a pass-through
return next(req);
};

Чтобы зарегистрировать наш Interceptor, нам нужно добавить его в provideHttpClient при монтировании приложения:

📁 main.ts

bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(
// 👇 Добавляем interceptor в пайплайн
withInterceptors([cachingInterceptor])
),
],
}).catch((err) => console.error(err));

Все готово!

Если мы снова запустим наше приложение, то пока что ничего не изменится. Хотя это может быть не очень полезно, по крайней мере, это указывает на то, что мы ничего не сломали, и HTTP-запросы по-прежнему поступают в наше приложение.

Создание нашего кэша

Самый простой подход к созданию кэша - это создать Map коллекцию ответов по урлам:

📁 app/caching.interceptor.ts

const cache = new Map<string, HttpEvent<unknown>>();

export const cachingInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
const cached = cache.get(req.url);

// 👇 If the response is known, return it without making a request
const isCacheHit = cached !== undefined;
if (isCacheHit) {
return of(cached);
}

return next(req).pipe(
// 👇 Cache the response as it flows back into our application
tap((response) => cache.set(req.url, response))
);
};

Если мы снова запустим наше приложение, то увидим, что информация об альбоме запрашивается только при первом обращении:

-2

Ограничение кэша

Несмотря на то, что этот подход позволяет кэшировать наши запросы, его недостатком является то, что все запросы будут кэшироваться, в то время как мы, возможно, хотим кэшировать только те, которые связаны с альбомом.

Поскольку мы хотим добавить некоторую логику в наш кэш, мы можем преобразовать его в более умную логику и превратить его в сервис:

📁 app/caching.service.ts

@Injectable({ providedIn: "root" })
export class CachingService {
readonly #cache = new Map<string, HttpEvent<unknown>>();

get(key: string): HttpEvent<unknown> | undefined {
return this.#cache.get(key);
}

set(key: string, value: HttpEvent<unknown>): void {
if (key.includes("album")) {
this.#cache.set(key, value);
}
}
}

Здесь мы используем простое условие (ключ album) для проверки необходимости кэширования. В реальных приложениях вы можете написать свои условия кеширования запроса.

Поскольку функции перехватчиков вызываются в контексте инъекции, мы можем использовать инъекцию зависимостей в их определении:

📁 app/caching.interceptor.ts

export const cachingInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
// 👇 Наш кеширующий сервис
const cache = inject(CachingService);

const cached = cache.get(req.url);

const isCacheHit = cached !== undefined;
if (isCacheHit) {
return of(cached);
}

return next(req).pipe(tap((response) => cache.set(req.url, response)));
};

С точки зрения interceptorа ничего не изменилось. Однако, поскольку теперь у нас есть выделенная служба, добавление более сложной логики значительно упростится.

Улучшение кэша

Наконец, мы могли бы захотеть, чтобы кэш истекал через некоторое время.

Для этого мы можем немного расширить его, добавив время жизни для каждой записи:

📁 app/caching.service.ts

interface CacheEntry {
value: HttpEvent<unknown>;
expiresOn: number;
}

И при необходимости истекает срок действия связанного с ним значения:

📁 app/caching.service.ts

const TTL = 3000;

@Injectable({ providedIn: "root" })
export class CachingService {
readonly #cache = new Map<string, CacheEntry>();

get(key: string): HttpEvent<unknown> | undefined {
const cached = this.#cache.get(key);

if (!cached) {
return undefined;
}

// 👇 Удалите данные из кеша, если срок действия истек
const hasExpired = new Date().getTime() >= cached.expiresOn;
if (hasExpired) {
this.#cache.delete(key);
return undefined;
}

return cached.value;
}

set(key: string, value: HttpEvent<unknown>): void {
if (key.includes("album")) {
this.#cache.set(key, {
value,
// 👇 Устанавливаем время сколько будет жить кеш
expiresOn: new Date().getTime() + TTL,
});
}
}
}

Если мы снова посмотрим на вкладку "Сеть" нашего браузера, то увидим, что каждый запрос кэшируется в течение нескольких секунд, после чего кэш истекает:

-3

Выводы

В этой статье мы рассмотрели, как перехватывать HTTP-запросы, выполняемые нашим приложением Angular, и как ими манипулировать.

Мы также увидели, как использовать преимущества системы инъекции зависимостей Angular для добавления пользовательской логики в перехватчик.

Наконец, мы создали небольшую систему кэширования на стороне клиента для нашего приложения.

Еще больше интересного у нас в телеграме: frontend_campus