Last active
May 12, 2021 13:32
-
-
Save Dok11/6eb097bb66280fce91f86d31fafda795 to your computer and use it in GitHub Desktop.
Angular 11. CacheInterceptor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HttpRequest, HttpResponse } from '@angular/common/http'; | |
import { Injectable } from '@angular/core'; | |
import { BehaviorSubject } from 'rxjs'; | |
export interface CachedData { | |
time: number; | |
body: object & HttpResponse<any>; | |
} | |
type StorageKey = string; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class CacheInterceptorControllerService { | |
private canUseLocalStorage: Record<StorageKey, boolean> = {}; | |
private hasLocalStorage: boolean; | |
private httpEvents: Record<StorageKey, BehaviorSubject<any>> = {}; | |
private httpMadeStory: Record<StorageKey, boolean> = {}; | |
private tempStorage: Record<StorageKey, string> = {}; | |
private readonly cacheTimeOver = 86400 * 1000; // ms | |
private readonly localStorageItemMaxBytes = 512 * 1024; // kb | |
constructor() { | |
this.checkLocalStorage(); | |
this.clearOldLocalStorageItems(); | |
} | |
/** | |
* Метод определяет конфигурацию обработчика, можно ли использовать localStorage браузера | |
*/ | |
public setCanUseLocalStorage(storageKey: StorageKey, isCan: boolean): void { | |
this.canUseLocalStorage[storageKey] = isCan; | |
} | |
/** | |
* Метод возвращает информацию о том, был ли уже выполнен заданный запрос в пределах | |
* текущей сессии | |
*/ | |
public checkIsHttpMade(storageKey: StorageKey): boolean { | |
return !!this.httpMadeStory[storageKey]; | |
} | |
/** | |
* Метод очищает из памяти выполненных запросов нужный по ключу | |
* @param storageKeys - Символьные коды storageKey или регулярных выражений | |
*/ | |
public cleanHttpMadeStoryByKeys(storageKeys: (StorageKey | RegExp)[]): void { | |
storageKeys.forEach(storageKey => { | |
if (typeof storageKey === 'string') { | |
if (this.httpMadeStory && this.httpMadeStory[storageKey]) { | |
this.httpMadeStory[storageKey] = false; | |
} | |
} else { | |
Object.keys(this.httpMadeStory).forEach(key => { | |
if (storageKey.test(key)) { | |
this.httpMadeStory[key] = false; | |
} | |
}); | |
} | |
}); | |
} | |
/** | |
* Метод формирует ключ кеша на основе http-запроса | |
* @param req - Объект с описанием body http-запроса | |
* @returns Ключ строкой | |
*/ | |
public getRequestKey(req: HttpRequest<any>): string { | |
return req.body | |
? JSON.stringify(req.body) | |
: req.urlWithParams || req.url; | |
} | |
/** | |
* Метод сохраняет данные в localStorage или временном хранилище с текущим временем | |
* @param storageKey - Ключ записи | |
* @param body - Сохраняемые данные | |
*/ | |
public saveData(storageKey: StorageKey, body: any): void { | |
const time = Date.now(); | |
const data: CachedData = { time, body }; | |
const cachedData = JSON.stringify(data); | |
if (this.canUseLocalStorage[storageKey]) { | |
this.saveDataInLocalStorage(storageKey, cachedData); | |
} else { | |
this.tempStorage[storageKey] = cachedData; | |
} | |
this.httpEvents[storageKey].next(body); | |
} | |
/** | |
* Метод определяет, что заданный запрос уже был выполнен в пределах одной сессии | |
*/ | |
public setHttpStoryMade(storageKey: StorageKey): void { | |
this.httpMadeStory[storageKey] = true; | |
} | |
/** | |
* Метод удаляет информацию о созданном запросе | |
*/ | |
public unsetHttpStoryMade(storageKey: StorageKey): void { | |
delete this.httpMadeStory[storageKey]; | |
} | |
/** | |
* Метод сохраняет ссылку на HTTP-запрос к данным | |
*/ | |
public saveHttpEvent(storageKey: StorageKey, defaultValue?: CachedData['body']): void { | |
this.httpEvents[storageKey] = new BehaviorSubject<any>(defaultValue); | |
} | |
/** | |
* Метод удаляет подписку на HTTP-запрос | |
*/ | |
public removeHttpEvent(storageKey: StorageKey, reason: any = 'Error'): void { | |
if (!this.httpEvents[storageKey]) { return; } | |
console.log('Http Event was stopped by', reason); | |
delete this.httpEvents[storageKey]; | |
} | |
/** | |
* Метод возвращает ссылку на HTTP-запрос к данным | |
*/ | |
public getHttpEvent(storageKey: StorageKey): BehaviorSubject<any> { | |
return this.httpEvents[storageKey]; | |
} | |
/** | |
* Метод возвращает сохраненное значение для заданного ключа | |
* @param storageKey - Ключ записи | |
*/ | |
public getData(storageKey: StorageKey): CachedData['body'] { | |
if (this.canUseLocalStorage[storageKey]) { | |
const cachedData = this.getDataFromLocalStorage(storageKey); | |
if (cachedData && cachedData.body) { | |
return cachedData.body; | |
} | |
} else { | |
const json = this.tempStorage[storageKey]; | |
if (json) { | |
const cachedData = JSON.parse(this.tempStorage[storageKey]) as CachedData; | |
if (cachedData && cachedData.body) { | |
return cachedData.body; | |
} | |
} | |
} | |
} | |
/** | |
* Метод удаляет из localStorage старые данные (старше суток) | |
*/ | |
private clearOldLocalStorageItems(): void { | |
if (!this.hasLocalStorage) { | |
return; | |
} | |
const oldCacheTimestamp = Date.now() - this.cacheTimeOver; | |
for (const storageKey in localStorage) { | |
if (!localStorage[storageKey]) { | |
continue; | |
} | |
try { | |
const data = JSON.parse(localStorage[storageKey]); | |
const isCacheTooOld = data && data.time && data.time < oldCacheTimestamp; | |
if (isCacheTooOld) { | |
localStorage.removeItem(storageKey); | |
} | |
} catch (e) { | |
} | |
} | |
} | |
/** | |
* Метод проверяет, доступен ли на клиенте localStorage и сохраняет | |
*/ | |
private checkLocalStorage(): void { | |
try { | |
const test = 't'; | |
localStorage.setItem(test, test); | |
localStorage.removeItem(test); | |
this.hasLocalStorage = true; | |
} catch (e) { | |
this.hasLocalStorage = false; | |
} | |
} | |
/** | |
* Метод сохраняет данные в localStorage | |
* @param storageKey - Ключ записи | |
* @param cachedData - Сохраняемые значения | |
*/ | |
private saveDataInLocalStorage(storageKey: StorageKey, cachedData: any): void { | |
if (!this.hasLocalStorage || !cachedData) { return; } | |
if (cachedData.length < this.localStorageItemMaxBytes) { | |
localStorage.setItem(storageKey, cachedData); | |
} else { | |
delete this.httpMadeStory[storageKey]; | |
} | |
} | |
/** | |
* Метод возвращает сохраненное в localStorage значение для заданного ключа | |
* @param storageKey - Ключ записи | |
*/ | |
private getDataFromLocalStorage(storageKey: StorageKey): CachedData { | |
if (!this.hasLocalStorage) { return; } | |
const cachedDataJson = localStorage.getItem(storageKey); | |
if (cachedDataJson) { | |
return JSON.parse(cachedDataJson) as CachedData; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse | |
} from '@angular/common/http'; | |
import { Injectable } from '@angular/core'; | |
import { EMPTY, merge, Observable, of } from 'rxjs'; | |
import { catchError, filter, tap } from 'rxjs/operators'; | |
import { BackendQuery } from '../../../interfaces/backend-query'; | |
import { CacheInterceptorControllerService } from './cache-interceptor-controller.service'; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class CacheInterceptor implements HttpInterceptor { | |
constructor( | |
private controller: CacheInterceptorControllerService, | |
) {} | |
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |
if (this.isCanCache(req)) { | |
const query = req.body as BackendQuery; | |
const storageKey = this.controller.getRequestKey(req); | |
const canUseLocalStorage = !query || query.canUseLocalStorage !== false; | |
this.controller.setCanUseLocalStorage(storageKey, canUseLocalStorage); | |
const cachedData = this.controller.getData(storageKey); | |
const isCachedDataValid = !!cachedData && cachedData.status === 200; | |
if (this.controller.checkIsHttpMade(storageKey)) { | |
// Если запрос уже был выполнен, можно вернуть ссылку на него | |
return this.controller.getHttpEvent(storageKey).asObservable(); | |
} | |
// Сохраняем подписку на HttpEvent | |
this.controller.setHttpStoryMade(storageKey); | |
this.controller.saveHttpEvent(storageKey, cachedData); | |
// Возвращаем подписку из двух событий | |
return merge( | |
// 1. Локальные данные из localStorage, если не игнорируем первый кеш | |
!this.isIgnoreFirstCache(req) | |
? this.controller.getHttpEvent(storageKey).asObservable() | |
: EMPTY, | |
// 2. И настоящий http-запрос | |
next.handle(req).pipe( | |
catchError(error => { | |
this.controller.removeHttpEvent(storageKey, error); | |
this.controller.unsetHttpStoryMade(storageKey); | |
return isCachedDataValid ? of(new HttpResponse<any>(cachedData)) : EMPTY; | |
}), | |
filter(data => data && 'body' in data), | |
tap(data => this.controller.saveData(storageKey, data)), | |
), | |
); | |
} | |
return next.handle(req); | |
} | |
private isCanCache(req: HttpRequest<any>): boolean { | |
const query = req.body as BackendQuery; | |
const disableCacheForGetRequest = req.params.get('canCache') === 'false' | |
|| req.url.indexOf('canCache=false') > 0; | |
/** | |
* Кеширование AJAX-запросов включено, | |
* если явно не задано выключение кеша для GET-запросов к /api/ или любых POST-запросов | |
* или, если это запрос json файлов меню или локализаций | |
*/ | |
return (req.body && req.method === 'POST' && query.canCache === true) | |
|| (req.method === 'GET' && req.url.indexOf('/api/v') === 0 && !disableCacheForGetRequest) | |
|| (req.url && req.url === '/api/v1/menu.json') | |
|| (req.url && req.url === './api/i18n/ru.json') | |
|| (req.url && req.url === './api/i18n/en.json'); | |
} | |
private isIgnoreFirstCache(req: HttpRequest<any>): boolean { | |
// Модуль локализаций по take(1) прерывает поток, поэтому ему не отдаем первый кеш | |
return !!req.url.match(/i18n\/\w{2,5}\.json/); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment