使用 :id 更改子路由时如何避免不必要的调用 API
How to avoid unnecessary calling API when changing sub-routes with :id
我有一个很常见的情况,我想知道行业 standard/best 处理这个问题的方法是什么。
问题
假设您有多条路线:
/organization
/organization/:id
/organization/:id/users
/organization/:id/users/:userId
/organization/:id/payments
- ...你学会了
正如您在此示例中所见,一切都围绕着 Organization Id
。要显示与组织相关的所有信息,我需要调用 API.
现在最大的问题是我有这些页面,但我真的不想在每条路由上都调用 API。在 /organization/:id
上调用 getOrganizaionById
感觉很不对,然后用户导航到 /organization/:id/users
我必须再次调用 getOrganizaionById
(因为,例如,我想显示组织名称页面某处)。
我尝试了什么
显然我有一些想法,我只是想请一些 SPA/Angular 专业人士告诉我对于我的特定问题什么是更好的解决方案。
我tried/can做的是:
Memoize getOrganizaionById
- 不是最好的,如果我想做缓存破坏也过于复杂(有人更改组织细节等)
将 selectedOrganizaion$
另存为 OrganizationService
内的 ReplaySubject
以便我知道我当前选择的组织 - 感觉不对,缓存也会有问题,这个本质上与 memoization
相同
用 Redux Store 做点什么?通过将它放在 Redux Store 中,是否与将它作为 ReplaySubject 保存在服务中一样?此外,很难找到关于此类问题的 redux 的任何内容,因为我没有构建待办事项应用程序。
我迷路了,希望有一个正确的方法来解决我的问题,因为这应该是一个非常常见的场景。
这是一个准备 fiddle 确切问题的 stackblitz:https://stackblitz.com/edit/angular-bbhhoy
可能有很多方法可以解决这个问题。
最简单
更简单的方法是使用 ngx-cachable 装饰您的服务端点。
你的情况的一个例子是:
organizations.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 调用只进行一次,后续调用将return缓存值作为可观察值
缺点:
- 如果您调用
getOrganizations()
并加载组织 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 应用程序,设置它是矫枉过正的
完整的 redux 系统(在我看来)。但是,我喜欢有一个中央对象或商店来保存我的应用程序状态
(甚至部分州)。我经常使用这个实现,所以我终于做了一个库,这样我就可以在我的
项目。 rxjs-util-classes specifically the BaseStore。
在上面的例子中,你可以这样做:
organizations.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 });
});
}
}
然后在您的组件中订阅状态并加载数据:
组织-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();
}
}
组织-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 的商店的快速版本
没有实现 all 的 Redux 模式。 BaseStore
是您唯一的真实来源。然后暴露
允许服务和组件与状态交互的方法。
另一种选择
我一直在研究另一个选项来解决我正在构建的应用程序中的类似问题。
我没有制定出所有的细节,所以我不会在这里尝试描述它。一旦我完成了代码,
我会更新我的答案。
TL;DR 版本: 创建一个 class,它有一个缓存对象并公开一些从 "cache" 中获取值的方法
and/or 观察 "cache" 上的变化(类似于上面的 Redux 示例)。然后组件可以加载所有的
"cache" 或只有一项。
我有一个很常见的情况,我想知道行业 standard/best 处理这个问题的方法是什么。
问题
假设您有多条路线:
/organization
/organization/:id
/organization/:id/users
/organization/:id/users/:userId
/organization/:id/payments
- ...你学会了
正如您在此示例中所见,一切都围绕着 Organization Id
。要显示与组织相关的所有信息,我需要调用 API.
现在最大的问题是我有这些页面,但我真的不想在每条路由上都调用 API。在 /organization/:id
上调用 getOrganizaionById
感觉很不对,然后用户导航到 /organization/:id/users
我必须再次调用 getOrganizaionById
(因为,例如,我想显示组织名称页面某处)。
我尝试了什么
显然我有一些想法,我只是想请一些 SPA/Angular 专业人士告诉我对于我的特定问题什么是更好的解决方案。
我tried/can做的是:
Memoize
getOrganizaionById
- 不是最好的,如果我想做缓存破坏也过于复杂(有人更改组织细节等)将
selectedOrganizaion$
另存为OrganizationService
内的ReplaySubject
以便我知道我当前选择的组织 - 感觉不对,缓存也会有问题,这个本质上与 memoization 相同
用 Redux Store 做点什么?通过将它放在 Redux Store 中,是否与将它作为 ReplaySubject 保存在服务中一样?此外,很难找到关于此类问题的 redux 的任何内容,因为我没有构建待办事项应用程序。
我迷路了,希望有一个正确的方法来解决我的问题,因为这应该是一个非常常见的场景。
这是一个准备 fiddle 确切问题的 stackblitz:https://stackblitz.com/edit/angular-bbhhoy
可能有很多方法可以解决这个问题。
最简单
更简单的方法是使用 ngx-cachable 装饰您的服务端点。 你的情况的一个例子是:
organizations.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 调用只进行一次,后续调用将return缓存值作为可观察值
缺点:
- 如果您调用
getOrganizations()
并加载组织 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 应用程序,设置它是矫枉过正的 完整的 redux 系统(在我看来)。但是,我喜欢有一个中央对象或商店来保存我的应用程序状态 (甚至部分州)。我经常使用这个实现,所以我终于做了一个库,这样我就可以在我的 项目。 rxjs-util-classes specifically the BaseStore。
在上面的例子中,你可以这样做:
organizations.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 });
});
}
}
然后在您的组件中订阅状态并加载数据:
组织-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();
}
}
组织-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 的商店的快速版本
没有实现 all 的 Redux 模式。 BaseStore
是您唯一的真实来源。然后暴露
允许服务和组件与状态交互的方法。
另一种选择
我一直在研究另一个选项来解决我正在构建的应用程序中的类似问题。 我没有制定出所有的细节,所以我不会在这里尝试描述它。一旦我完成了代码, 我会更新我的答案。
TL;DR 版本: 创建一个 class,它有一个缓存对象并公开一些从 "cache" 中获取值的方法 and/or 观察 "cache" 上的变化(类似于上面的 Redux 示例)。然后组件可以加载所有的 "cache" 或只有一项。