RXJS在Angular中的使用(一)

任何用angular开发的研发人员应该都对rxjs非常熟悉了。实际上框架本身也用rxjs和它的一些概念去进行构建。但它的意义远远不止这些,事实上,我们可以使用rxjs和observable流去写出更多且更好可读性的代码,还可以去减少我们很多的代码量。

使用RXJS去减少我们组件的状态

angular中的每件事都是围绕着组件的状态以及如何将其投影到ui中展开的。有很多时候我们都可以使用流去代表视图中数据。尤其是当处理表单和其他变化很大的内容时,它特别有用。

  1. 当我们有一个新任务时,我们将考虑状态将如何改变
  2. 我们在组件中存储新的状态(新属性,嵌套对象等)
  3. 我们设计新方法来封装新状态如何变化的方式
  4. 在我们的模板中编写复杂的逻辑

来让我们看下下面这个例子:

@Component({
  selector: 'my-component',
  template: `
    <select [(ngModel)]="selectedUserId" (ngModelChange)="changeUser()">
      <option>Select a User</option>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    <select [(ngModel)]="blackListedUsers" (ngModelChange)="changeUser()" multiple>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    Allow black listed users <input type="checkbox" [(ngModel)]="allowBlackListedUsers"/>
    <button [disabled]="isUserBlackListed && !allowBlackListedUsers">Submit</button>
  `,
})
export class MyComponent  {
  users = [
    {name: 'John', id: 1},
    {name: 'Andrew', id: 2},
    {name: 'Anna', id: 3},
    {name: 'Iris', id: 4},
  ];

  blackListedUsers = [];

  selectedUserId = null;
  isUserBlackListed = false;
  allowBlackListedUsers = false;

  changeUser() {
    this.isUserBlackListed = !!this.blackListedUsers.find(
      blackListedUserId => +this.selectedUserId === blackListedUserId
    );
  }
}

想象一下我们有一个页面,其中有一个下拉选择框,我们从中可以选择用户(里面的选项通常是用ngfor循环遍历出来的)。现在有另一个下拉选择框有相同的用户,选将一些用户将其列入黑名单,列入黑名单的用户不允许被提交。还有一个复选框,勾选表明允许将某如列入黑名单并提交他们(因此哪怕被勾选的用户已经被列入黑名单,该按钮也不会被禁用)。如果用户从第一个下拉框中选择其中一个,则“提交”按钮将被禁用。让我们尝试使用简单的模板驱动表单模型来完全不使用 RxJS 来实现它。

因此,我们有两个数组,三个表单绑定,以及ngModelChange的一个方法来处理状态的更改。后续我们还需要以下逻辑:

  1. 需要一些状态(isUserBlackListed和allowBlackListedUsers)来存储一些仅在模板中实际需要的数据;
  2. 需要声明ngmodel绑定的状态
  3. 写一个对应的方法处理状态的更改
  4. 在模板中需要对应的禁用逻辑([disabled]=”isUserBlackListed && !allowBlackListedUsers”))

按这样的写法来说,对于初学者,遵循应用程序的逻辑会变得更加困难。例如,如果我是阅读此代码的人,并且我发现某个按钮有时会被禁用,我将执行以下步骤:

  1. 找到 [disabled] 绑定,可以看到它绑定了两个属性,isUserBlackListed 和allowBlackListedUsers;
  2. 在组件代码中查找,发现只是一些从 false 开始的基本属性;
  3. 然后搜索 component.ts 文件以查找对它们的引用;这看起来似乎是理所当然的事情,但是如果我们的属性在多个方法中被引用怎么办?我必须仔细检查所有这些,才能准确找出哪一个会影响禁用按钮;
  4. 阅读并理解我最终找到的方法。在我们的例子中,这很容易;但在实际开发中,可能就会非常混乱。

另一个缺点是,当出现另一段此类逻辑时,我们最终将在组件代码中增加改变它们的属性和方法。所以我们应该怎么做呢?

更多的响应式思考

现在我们将设计一个简单的三步思考计划。尝试解决同样的问题,但现在使用 Reactive Forms 和 RxJS,我们将执行以下操作:

  1. 了解状态的哪一部分影响 UI 并使其成为 Observable 流;
  2. 使用 RxJS 运算符执行计算并导出要在 UI 中使用的最终状态
  3. 使用异步管道将计算结果放入模板中
import { FormControl } from '@angular/forms';
import { combineLatest } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

@Component({
  selector: 'my-app',
  template: `
    <select [formControl]="selectedUserId">
      <option>Select a User</option>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    <select [formControl]="blackListedUsers" multiple>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    Allow black listed users <input type="checkbox" [formControl]="allowBlackListedUsers"/>
    <button [disabled]="isDisabled$ | async">Submit</button>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  users = [
    {name: 'John', id: 1},
    {name: 'Andrew', id: 2},
    {name: 'Anna', id: 3},
    {name: 'Iris', id: 4},
  ];

  blackListedUsers = new FormControl([]);
  selectedUserId = new FormControl(null);
  allowBlackListedUsers = new FormControl(false);
  isDisabled$ = combineLatest([
    this.allowBlackListedUsers.valueChanges.pipe(startWith(false)),
    this.blackListedUsers.valueChanges.pipe(startWith([])),
    this.selectedUserId.valueChanges.pipe(startWith(null), map(id => +id)),
  ]).pipe(
    map(
      ([allowBlackListed, blackList, selected]) => !allowBlackListed && blackList.includes(selected),
    ),
  )
}

正如你所看到的,我们实现了一个属性,它是一个 Observable,来处理一些 UI。它使用combineLatest组合了三个表单控件的输出,然后使用它们的组合输出来导出布尔状态。

(注:我们使用 startWith 是因为在 Angular formControl.valueChanges 中,直到用户通过 UI 控件手动更改它,或者强制地通过 setValue 更改它时,Control.valueChanges 才会开始发出,并且在所有源 Observables 至少发出一次之前,combineLatest 不会触发;所以我们需要使用startwith让它们全部在最开始发出默认值。)

现在,当我读到这个组件的模板并思考这个按钮什么时候被禁用时,我将执行以下步骤:

  1. 只关注isDisabled,看见[disabled]=isDisabled,看见 [disabled]=”isDisabled | async” 绑定末尾的美元符号会立即表明它是一个 Observable;
  2. 转到该属性定义并查看它是三个数据源的组合;
  3. 查看数据如何映射到布尔值

isDisabledObservable没有在任何其他方法中被引用,即使被引用,也没关系——其他人可以订阅它,但他们不能更改它的数据。如果出现了bug并且按钮在不应该被禁用的情况下被禁用(反之亦然),那么我们可以100 Observable 没有在任何其他方法中被引用,即使被引用,也没关系——其他人可以订阅它,但他们不能更改它的数据。如果出现了bug并且按钮在不应该被禁用的情况下被禁用(反之亦然),那么我们可以 100% 确定该错误位于 isDisabled 及其运算符的定义内,而不是其他地方。你可能认为我们需要取消订阅 Observable?但事实上我们不需要——异步管道为我们做到了这一点。

总结

本文样例在经过rxjs进行重构后,有了以下改变:

  1. 更容易搜索和找到对应的代码
  2. 更加简洁;相互影响的逻辑片段被收集在一个地方,而不是分散在整个组件中
  3. 声明式而非命令式

rxjs是一个强大的工具。它有很多概念和技巧,可以用来使我们的 Angular 代码更好、更易读、更容易推理、更容易理解。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY5Ja3eT' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片