使用 Angular、HttpClient 和 Observables 避免回调地狱

cec*_*mel 2 javascript promise observable typescript angular

我目前正在努力理解 Angular (2+)、HttpClient 和 Observables。

我来自承诺异步/等待背景,我想在角度中实现的目标相当于:

//(...) Some boilerplate to showcase how to avoid callback hell with promises and async/await
  async function getDataFromRemoteServer() {
    this.result = await httpGet(`/api/point/id`);
    this.dependentKey = someComplexSyncTransformation(this.result);
    this.dependentResult = await httpGet(`/api/point/id/dependent/keys/${this.dependentKey}`);
    this.deeplyNestedResult = await httpGet(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`);
  }
Run Code Online (Sandbox Code Playgroud)

我能想到的最好的角度是:

import { HttpClient } from `@angular/common/http`;

//(...) boilerplate to set component up.

  constructor(private http: HttpClient) {}

// somewhere in a component.

  getDataFromRemoteServer() {
    this.http.get(`/api/point/id`).subscribe( result => {
       this.result = result;
       this.dependentKey = someComplexSyncTransformation(this.result);
       this.http.get(`/api/point/id/dependent/keys/${this.dependentKey}`).subscribe( dependentResult => {
         this.dependentResult = dependentResult;
         this.http.get(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`).subscribe( deeplyNestedResult => {
            this.deeplyNestedResult = deeplyNestedResult;
         });
       })
    });
  }

//...
Run Code Online (Sandbox Code Playgroud)

正如您可能已经注意到的,我正在通过这种方法进入末日金字塔,我想避免这种方法。那么我该如何编写角度片段以避免这种情况呢?

谢谢!

Ps:我知道您可以根据 .get 调用的结果调用 .toPromise。但现在我们假设我想采用完全可观察的方式。

Nic*_*wer 6

使用可观察量时,您不会经常调用 subscribe。相反,您将使用各种运算符将可观察量组合在一起,形成操作管道。

要获取一个可观察量的输出并将其转换为另一个可观察量的输出,基本运算符是map。这类似于如何用.map一个数组生成另一个数组。举一个简单的例子,这里将可观察值的所有值加倍:

const myObservable = of(1, 2, 3).pipe(
  map(val => val * 2)
);
// myObservable is an observable which will emit 2, 4, 6
Run Code Online (Sandbox Code Playgroud)

映射也是您为一个 http 请求获取可观察值,然后发出另一个 http 请求的操作。然而,我们需要额外的一块,所以下面的代码不太正确:

const myObservable = http.get('someUrl').pipe(
  map(result => http.get('someOtherUrl?id=' + result.id)
)
Run Code Online (Sandbox Code Playgroud)

这段代码的问题在于它创建了一个可观察量,该可观察量会吐出其他可观察量。如果你愿意的话,可以是二维可观察的。我们需要将其扁平化,以便我们有一个可观察的对象来输出第二个 http.get 的结果。有几种不同的方法可以进行扁平化,具体取决于我们希望多个可观察量发出多个值时结果的顺序。对于您的情况来说,这并不是什么大问题,因为每个 http observables 只会发出一项。但作为参考,以下是选项:

  • mergeMap 将使所有可观察量以任何顺序运行,并以值到达的任何顺序输出。这有其用途,但也可能导致竞争条件
  • switchMap 将切换到最新的 observable,并取消可能正在进行的旧的 observable。这可以消除竞争条件并确保您仅拥有最新数据。
  • concatMap 将完成第一个可观察对象的全部,然后再继续处理第二个可观察对象。这也可以消除竞争条件,但不会取消旧的工作。

就像我说的,这对你的情况来说并不重要,但我建议使用 switchMap。所以我上面的小例子将变成:

const myObservable = http.get('someUrl').pipe(
  switchMap(result => http.get('someOtherUrl?id=' + result.id)
)
Run Code Online (Sandbox Code Playgroud)

现在,我将介绍如何在您的代码中使用这些工具。在此代码示例中,我没有保存所有 this.result、this.dependentKey 等:

  getDataFromRemoteServer() {
    return this.http.get(`/api/point/id`).pipe(
      map(result => someComplexSyncTransformation(result)),
      switchMap(dependentKey => this.http.get(`/api/point/id/dependent/keys/${dependentKey}`)),
      switchMap(dependantResult => this.http.get(`/api/point/id/dependent/keys/${dependentResult.someValue}`)
    });
  }

// to be used like:

   getDataFromRemoteServer()
     .subscribe(deeplyNestedResult => {
       // do whatever with deeplyNestedResult
     });
Run Code Online (Sandbox Code Playgroud)

如果保存这些值对您很重要,那么我建议使用 tap 运算符来突出显示您正在产生副作用的事实。每当 observable 发出一个值时,tap 就会运行一些代码,但不会弄乱该值:

  getDataFromRemoteServer() {
    return this.http.get(`/api/point/id`).pipe(
      tap(result => this.result = result),
      map(result => someComplexSyncTransformation(result)),
      tap(dependentKey => this.dependentKey = dependentKey),
      // ... etc
    });
  }
Run Code Online (Sandbox Code Playgroud)