RxJS在非平凡Angular组件中的最佳实践

Mat*_*nda 8 observable rxjs redux angular

我们的团队正在从AngularJS迁移到Angular(4).

在我们的Angular组件中,我们使用RxJS库来处理从Http,ActivatedRoute等服务或从用户界面上的输入接收值的主体返回的可观察量.当对这些可观察量产生的值进行某些处理时,我们将它们映射或组合成新的可观察量以产生"派生的"可观察量.这里没什么特别的.

我们最终得到的组件几乎所有从模板绑定的公共属性都是可观察的.

但是,这种技术似乎有很大的缺点:

  • 它需要非常好的知识和对RxJS库的理解,许多以前看似简单的任务变得非常复杂,花费了大量时间来调试/优化observables订阅.
  • 我们的模板现在填充了异步管道,我们经常遇到像这样的Angular错误:https://github.com/angular/angular/issues/10165.

举个例子,这是一个简单的例子(到目前为止不是最差的):分页组件的模板.注意大量的异步管道,其中一些嵌套在一个结构指令中,本身是异步的(导致我刚才提到的bug).

<nav aria-label="Pagination" class="mt-5" *ngIf="($items | async)?.length">
  <ul class="pagination">
    <li class="page-item" [class.disabled]="(currentPage$ | async) === 1">
      <a class="page-link" [routerLink]="" [queryParams]="{ page: (currentPage$ | async) - 1 }" queryParamsHandling="merge">Previous</a>
    </li>
    <li *ngFor="let page of (pages$ | async)" class="page-item" [class.active]="page === (currentPage$ | async)">
      <a class="page-link app-page-number" [routerLink]="" [queryParams]="{ page: page }" queryParamsHandling="merge">{{ page }} <span class="sr-only" *ngIf="page === (currentPage$ | async)">(current)</span></a>
    </li>
    <li class="page-item" [class.disabled]="(currentPage$ | async) === (lastPage$ | async)">
      <a class="page-link" [routerLink]="" [queryParams]="{ page: (currentPage$ | async) + 1 }" queryParamsHandling="merge">Next</a>
    </li>
  </ul>
</nav>
Run Code Online (Sandbox Code Playgroud)

将"派生"可观察对象更改为从.subscribe()填充的简单(不可观察)属性有助于降低代码复杂性以及模板内异步管道的数量.但这并不是一个令人满意的解决方案.

此外,我们遇到了一些与OnPush策略相关的变化检测相关的新问题.由于我们将可观察的属性转换为"普通"属性并删除了异步管道,因此Angular不再知道我们正在更改这些属性,我们经常需要调用ChangeDetectorRef.detectChanges().

到目前为止,我们还没有找到关于如何在非平凡组件中处理RxJS的明确最佳实践.您对如何解决这些困难有什么建议吗?Redux可以解决(某些)我们的问题吗?

编辑:事实证明,上述"错误"实际上是我们对RxJS库的误用.有关更多详细信息,请参阅我对(GitHub问题)的贡献.

Ric*_*sen 3

变化检测

问题之一*ngIf="(obs | async)"是变化检测。变量obs在发出时本身不会改变,再加上这是模板中的表达式,使得更改检测很难检测到。

在这种情况下,有几个原则值得考虑:

  • 可观察变量是“管道”,即通过它们传递的值的包装器,而不是变化的值本身。两者通常是等同的,但这类似于认为数组和数组元素是同一件事。

  • RxJs 是一个“外部”(非 Angular)库。对于这些库,我们需要了解库处理的数据更改对于 Angular 更改检测是否可见。

#10165的修复是

<div style="background-color: green;" *ngIf="trigger">{{(val1 | async)}}</div>
<div style="background-color: green;" *ngIf="!trigger">{{(val2 | async)}}</div>

ngOnInit() {
  this.trigger = this.ifObservable.subscribe();
}
Run Code Online (Sandbox Code Playgroud)

处理异步数据

关于“无处不在的可观察值”这个更广泛的问题,你是完全正确的,但这不是网络应用程序异步性质固有的问题吗?

将旧的 AngularJS 代码与新的 Angular 代码进行对比会很有趣。同一套功能实现的复杂度是否会增加?

我喜欢应用的一个原则是处理“一次性”可观察量,例如http.get通过订阅它们,并通过异步管道保持“多次”可观察量连接。

按下时

这本质上是一种减少自动更改检测量的方法,从而加快应用程序的速度。当然,这意味着您可能必须更频繁地手动进行火灾变化检测 - 速度与复杂性的权衡。

终极版

虽然 Redux 存储通常将状态公开为可观察量(因此您仍然需要在模板中使用异步管道),但它确实消除了多个状态更新点的复杂性,这可能意味着需要更少的可观察量(或者至少将可观察量从成分)。

我见过的唯一不涉及模板中去异步数据的 Redux 风格是 Mobex,它本质上将简单变量转换为具有 getter 和 setter 的对象,其中包含监视异步更改并解包它们的逻辑。但它对于数组也有问题/警告。

简化模板

简化所显示模板的一种方法(我尚未测试过)是将其包装在子组件中并传入未包装的值

<nav aria-label="Pagination" class="mt-5" *ngIf="items.length">
  <ul class="pagination">
    <li class="page-item" [class.disabled]="currentPage === 1">
      <a class="page-link" [routerLink]="" [queryParams]="{ page: currentPage - 1 }" queryParamsHandling="merge">Previous</a>
    </li>
    <li *ngFor="let page of pages" class="page-item" [class.active]="page === currentPage">
      <a class="page-link app-page-number" [routerLink]="" [queryParams]="{ page: page }" queryParamsHandling="merge">{{ page }} <span class="sr-only" *ngIf="page === currentPage">(current)</span></a>
    </li>
    <li class="page-item" [class.disabled]="currentPage === lastPage">
      <a class="page-link" [routerLink]="" [queryParams]="{ page: currentPage + 1 }" queryParamsHandling="merge">Next</a>
    </li>
  </ul>
</nav>


export class PageComponent {

  @Input() currentPage = 0;
  @Input() pages = [];
  @Input() items = [];
Run Code Online (Sandbox Code Playgroud)

在父级中,

<page-component [pages]="pages$ | async" [items]="items$ | async" [currentPage]="currentPage$ | async" >
Run Code Online (Sandbox Code Playgroud)

从本质上讲,@Input 挂钩到更改检测,即使使用 onPush 策略也是如此。
请小心为子输入提供默认值。