使用 :id 更改子路由时如何避免不必要的调用 API

How to avoid unnecessary calling API when changing sub-routes with :id

我有一个很常见的情况,我想知道行业 standard/best 处理这个问题的方法是什么。

问题

假设您有多条路线:

正如您在此示例中所见,一切都围绕着 Organization Id。要显示与组织相关的所有信息,我需要调用 API.

现在最大的问题是我有这些页面,但我真的不想在每条路由上都调用 API。在 /organization/:id 上调用 getOrganizaionById 感觉很不对,然后用户导航到 /organization/:id/users 我必须再次调用 getOrganizaionById (因为,例如,我想显示组织名称页面某处)。

我尝试了什么

显然我有一些想法,我只是想请一些 SPA/Angular 专业人士告诉我对于我的特定问题什么是更好的解决方案。

我tried/can做的是:

我迷路了,希望有一个正确的方法来解决我的问题,因为这应该是一个非常常见的场景。

这是一个准备 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" 或只有一项。