Вероятно, есть много способов решить эту проблему.
Самый простой
Чем проще будет использовать ngx-cachable для украшения конечных точек вашего сервиса. Примером для вашего случая будет:
organization.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Cacheable } from 'ngx-cacheable';
@Injectable({providedIn: 'root'})
export class OrganizationService {
constructor(private http: HttpClient) { }
@Cacheable()
public getOrganizations(): object[] {
return this.http.get('organizations'); // or w/e your endpoint is
}
// see https://github.com/angelnikolov/ngx-cacheable#configuration for other options
@Cacheable({
maxCacheCount: 40 // items are cached based on unique params (in your case ids)
})
public getOrganizationById(id: number): object {
return this.http.get(`organization/${id}`); // or w/e your endpoint is
}
}
Плюсы:
- HTTP-вызов выполняется только один раз, и последующие вызовы вернут кэшированное значение как наблюдаемое
Минусы:
- Если вы позвоните
getOrganizations()
и загрузите org 1 & 2, затем позвоните getOrganizationById(1)
, getOrganizationById
снова сделает HTTP-запрос для организации
Сверните свой собственный кэш
Это немного больше работы и может быть хрупким (в зависимости от сложности ваших данных и услуг). Это всего лишь пример, и его нужно конкретизировать подробнее:
import { Injectable } from "@angular/core";
import { of, Observable } from "rxjs";
import { delay, tap } from "rxjs/operators";
@Injectable({providedIn: 'root'})
export class OrganizationService {
// cache variables
private _loadedAllOrgs = false;
private _orgs: IOrg[] = [];
constructor() {}
public getOrganizations(busteCache: boolean): Observable<IOrg[]> {
// not the most verbose, but it works
// if we haven't loaded all orgs OR we want to buste the cache
if (this._loadedAllOrgs || busteCache) {
// this will be your http request to the server
// just mocking right now
console.log("Calling the API to get all organizations");
return of(organizationsFromTheServer).pipe(
delay(1000),
tap(orgs => this._orgs = orgs)
);
}
// else we can return our cached orgs
console.log("Returning all cached organizations");
return of(this._orgs);
}
public getOrganizationById(id: number): Observable<IOrg> {
const cachedOrg = this._orgs.find((org: IOrg) => org.id === id);
// if we have a cached value, return that
if (cachedOrg) {
return of(cachedOrg);
}
// else we have to fetch it from the server
console.log("Calling API to get a single organization: " + id);
return of(organizationsFromTheServer.find(o => o.id === id)).pipe(
delay(1000),
tap(org => this._orgs.push(org))
);
}
}
interface IOrg {
id: number;
name: string;
}
const organizationsFromTheServer: IOrg[] = [
{
id: 1,
name: "First Organization"
},
{
id: 2,
name: "Second Organization"
}
];
Плюсы:
- у вас есть контроль над кэшированием
- вам не нужно делать последующие вызовы на сервер, если у вас уже есть орг в памяти
Минусы:
- у вас есть для управления кешем и его уничтожения
Использовать Redux-подобный магазин
Redux довольно сложен. Мне потребовалось несколько дней, чтобы полностью понять это. Для большинства Angular приложений настройка полного редукса (на мой взгляд) излишня. Однако мне нравится иметь центральный объект или хранилище для хранения состояния моего приложения (или даже части состояния). Я так много использую эту реализацию, что я наконец-то создал библиотеку, чтобы я мог использовать ее в своих проектах. rx js -util-classes конкретно BaseStore .
В приведенном выше примере вы могли бы сделать что-то вроде этого:
organiz.service.ts
// other imports
import { BaseStore } from 'rxjs-util-classes';
export interface IOrg {
id: number;
name: string;
}
export interface IOrgState {
organizations: IOrg[];
loading: boolean;
// any other state you want
}
@Injectable({providedIn: 'root'})
export class OrganizationService extends BaseStore<IOrgState> {
constructor (private http: HttpClient) {
// set initial state
super({
organizations: [],
loading: false
});
}
// services/components subscribe to this service's state
// via `.getState$()` which returns an observable
// or a snapshot via `.getState()`
// this method will load all orgs and emit them on the state
loadAllOrganizations (): void {
// this part is optional, but if you are loading don't fire another request
if (this.getState().loading) {
console.log('already loading organizations. not requesting again');
return;
}
this._dispatch({ loading: true });
this.http.get('organizations').subscribe(orgs => {
// this will emit the new orgs to any service/component listening to
// the state via `organizationService.getState$()`
this._dispatch({ organizations: orgs });
this._dispatch({ loading: false });
});
}
}
Затем в ваших компонентах вы подписываетесь в штат и загрузите ваши данные:
organization-list.component.ts
// imports
@Component({
selector: 'app-organization-list',
templateUrl: './organization-list.component.html',
styleUrls: ['./organization-list.component.css']
})
export class OrganizationListComponent implements OnInit {
public organizations: IOrg[];
public isLoading: boolean = false;
constructor(private readonly _org: OrganizationService) { }
ngOnInit() {
this._org.getState$((orgState: IOrgState) => {
this.organizations = orgState.organizations;
this.isLoading = orgState.loading; // you could show a spinner if you wanted
});
// only need to call this once to load the orgs
this._org.loadAllOrganizations();
}
}
organization-single.component.ts
// imports...
import { combineLatest } from 'rxjs';
@Component({
selector: 'app-organization-users',
templateUrl: './organization-users.component.html',
styleUrls: ['./organization-users.component.css']
})
export class OrganizationUsersComponent implements OnInit {
public org: IOrg;
constructor(private readonly _org: OrganizationService, private readonly _route: ActivatedRoute) { }
ngOnInit() {
// combine latest observables from route and orgState
combineLatest(
this._route.paramMap,
this._org.getState$()
).subscribe([paramMap, orgState]: [ParamMap, IOrgState] => {
const id = paramMap.get('organizationId);
this.org = orgState.organizations.find(org => org.id === id);
});
}
}
Плюсы:
- Все компоненты всегда используют одни и те же организации и заявляют
Минусы:
- Вам все еще нужно вручную управлять тем, как вы загружаете свои организации в состояние OrganizationService
Этот пример не полностью раскрыт, но вы можете увидеть, как реализовать быструю версию Redux-подобного магазина без реализации всех шаблонов Redux. BaseStore
действует как ваш единственный источник правды. Затем он предоставляет методы, которые позволяют сервисам и компонентам взаимодействовать с государством.
Другой вариант
Есть еще один вариант, над которым я работал, чтобы решить аналогичную проблему в приложении, которое я создаю. У меня не все детали проработаны, поэтому я не буду пытаться описать это здесь. Как только я закончу код, я обновлю свой ответ.
TL; DR Версия: Создайте класс, который имеет объект кэша и предоставляет несколько методов для получения значений из «кэша» и / или отслеживания изменений в этом «кэше» (аналогично на пример Redux выше). Затем компоненты могут загружать весь «кеш» или только один элемент.