使用角度异步管道以完全反应式方式刷新数据

Posted

技术标签:

【中文标题】使用角度异步管道以完全反应式方式刷新数据【英文标题】:Refresh Data in Fully Reactive Way using angular async pipe 【发布时间】:2020-10-31 13:39:55 【问题描述】:

我觉得问这个有点尴尬,但我确定我错过了一些东西。花了很长时间寻找/研究,但只提出了需要主题或行为主题或通过 mergeMap 等管道的复杂而复杂的解决方案。

我正在组件 html 模板中从命令式 HTTP 可观察方法(手动订阅)转变为响应式 HTTP 可观察方法(observable$ | async)。

在页面初始化时让它与异步管道一起工作没有问题。

但是,“动态”刷新模板的最佳方式是什么,即我知道我已将一条记录插入到基础数据库中并希望将其反映在我的模板中。

使用命令式方法,我可以简单地再次订阅(例如从单击处理程序)并将结果重新分配给与 ngfor 指令关联的属性,然后更改检测将触发并更新 DOM。

我想出的唯一解决方案是简单地再次从服务中获取 observable,这会触发更改检测。

为了更清楚,这里是一段代码(非常简单,只需专注于解决方案)。

必须有一个简单的解决方案?

<div *ngIf="(model$ | async)?.length > 0" class="card mb-3 shadow">

  <div class="card-header">Actions Notes</div>

  <div class="card-body">

    <mat-list *ngFor="let action of model$ | async">
      <div class="small">
        <strong>
          action.createdDate | date:'dd/MM/yyyy HH:mm'
          action.createdBy
        </strong>
      </div>
      <div>action.notes</div>
    </mat-list>

  </div>
</div>

<button (click)="onClick()">name</button>
 model$: Observable<any>;

  constructor(
    private changeTransferActionsService: ChangeTransferActionsService
  ) 

  ngOnInit(): void 
    this.getObservable();
  

  private getObservable(): void 
    this.model$ = this.changeTransferActionsService
      .getActionsWithNotesById(this.model.id)
      .pipe(
        map((x) => x.result),
        share()
      );
  

  onClick(): void 

    //insert row into database here (via HTTP post service call)

    this.getObservable();
  

只是跟进我最初的问题,这里有三个带有解释的工作解决方案。感谢所有回复的人。

解决方案 1 - BehaviourSubject / SwitchMap

使用具有任意类型值和初始值的 RxJS BehaviorSubject。使用扁平化运算符 SwitchMap 将其与更高阶的 observable 结合起来。

单击按钮时,BehaviourSubject 发出(下一个)一个值(在本例中为 0),该值被馈送到 HTTP 可观察对象(忽略传递的值),从而导致内部可观察对象发出一个值并启动更改检测在。

当使用 BehaviourSubject 时,observable 会在订阅时发出一个值。

在组件 HTML 模板中。

<button (click)="onClick()">Click Me</button>

在组件 Typescript 中。

import  BehaviorSubject, Observable  from 'rxjs';
.
.
.
subject = new BehaviorSubject(0);
.
.
.

this.model$ = this.subject.asObservable().pipe(
      // startWith(0),
      switchMap(() => this.changeTransferActionsService
        .getActionsWithNotesById(this.model.id)
        .pipe(
          map((x) => x.result)
        )));
.
.
.
onClick(): void 
    this.subject.next(0);
  

解决方案 2 - Subject / SwitchMap / initialValue

使用 RxJS Subject 将其与使用扁平化运算符 SwitchMap 的高阶 observable 结合起来。

单击按钮时,Subject 发出(下一个)一个值(在这种情况下未定义),该值被馈送到 HTTP 可观察对象(忽略传递的值),从而导致内部可观察对象发出一个值并启动更改检测在。

Subject 需要 startWith(0) 否则当组件首次初始化并首次被异步管道订阅时,observable 不会发出任何值。

import Observable, Subject  from 'rxjs';
.
.
.
subject = new Subject();
.
.
.
 this.model$ = this.subject.asObservable().pipe(
      startWith(0),
      switchMap(() => this.changeTransferActionsService
        .getActionsWithNotesById(this.model.id)
        .pipe(
          map((x) => x.result)
        )));
.
.
.
onClick(): void 
    this.subject.next(0);
  
<button (click)="onClick()">Click Me</button>

请注意,在上述两种情况下,主题的 next 方法都可以直接在 HTML 组件模板中调用,因此主题具有公共访问修饰符(无论如何都是默认值)。这样就不需要在 Typescript 中使用 onCLick 方法了。

<button (click)="subject.next(0)">Click Me</button>

解决方案 3 - fromEvent

正如hanan所建议的那样

为按钮添加一个模板变量名并使用@ViewChild 获取元素引用。使用 fromEvent 创建一个 RxJS observable,然后使用 switchMap 高阶 observable 方法。

.
.
.
 @ViewChild('refreshButton') refreshButton: ElementRef;
.
.
.

 ngAfterViewInit(): void 
    const click$ = fromEvent(this.refreshButton.nativeElement, 'click');

    this.model$ = click$.pipe(
      startWith(0),
      switchMap(_ => 
        return this.changeTransferActionsService
          .getActionsWithNotesById(this.model.id)
          .pipe(
            map((x) => x.result),
          );
      ));
  


.
.
.
<button #refreshButton>Click Me</button>
.
.
.

总之,如果用户通过点击随意启动刷新,解决方案 3 最有意义,因为它不需要引入“虚拟”主题。

但是,主题方法可以判断 observable 是否应该以编程方式从任何地方发出...只需调用主题上的 next 方法,observable 就可以根据命令发出值。

您知道,这个主题还有更多内容,真正的反应式方法可能会开始您思考使用基于主题/行为主题/异步主题等的共享可注入单例服务和订阅的过程。不过,这些都是架构和设计决策。感谢您的所有建议。

【问题讨论】:

当你说我知道我已经在底层数据库中插入了一条记录时,这是否意味着你在你的 Angular 应用程序中订阅了这些数据库更改?跨度> 缺少的链接是您的事件处理程序方法。除了调用 onclick,您还可以使用 observable。所以完全移除 onClick 处理程序并引入一个 click$ 主题。在您的模板中(单击),只需执行 click$.next()。现在您有了另一个流,您可以将(使用 switchMap)与您的服务调用结合起来。其余部分实际上保持不变,但现在每次点击流触发时都会刷新您的服务 observable。 我问是因为如果您有办法通过套接字或其他方式从数据库中获取实时更改,那么带有 BehaviorSubject/Subject/AsyncSubject 的 shared service 将是您最好的选择。该服务可以是您的主要事实来源,组件订阅从该服务公开的可观察对象。每当基础主题更新时,所有现有订阅者和新订阅者都将获得新数据,避免手动需要触发另一个请求。带有主题的服务是您的朋友。 感谢您的回复。基本上,假设我可以从与 ngfor 相同的组件(如上所述)向数据库中插入一行。我在 onClick() 中添加了一条注释,表明单击也插入了一行。这是我想触发刷新。就在插入之后。我猜想通过服务使用行为主题是一种选择,但做如此简单的事情似乎需要很多代码,而且我没有任何其他订阅者。 【参考方案1】:

您可以获取刷新 Btn 引用并从其单击事件创建流,然后使用虚拟条目启动它。 现在你有一个刷新流, 然后像这样将其切换到您的数据流:

组件

@ViewChild('refreshButton') refreshBtn:ElementRef;  
  model$: Observable<any>;

  ngAfterViewInit() 
    let click$ = fromEvent(this.refreshBtn.nativeElement, 'click')
    this.model$ = click$.pipe(startWith(0),
    switchMap(_=> 
       return this.changeTransferActionsService
      .getActionsWithNotesById(this.model.id)
      .pipe(
        map((x) => x.result),
      );
    ));
  

模板

而不是使用 2 个异步管道,只使用 1 个和 ngLet 它将简化您的模板,并且您不必使用 share()

<button #refreshButton >Refresh</button>

<ng-container *ngLet="(model$ | async) as model">

<div *ngIf="model.length > 0" class="card mb-3 shadow">

  <div class="card-header">Actions Notes</div>

  <div class="card-body">

    <mat-list *ngFor="let action of model">
      <div class="small">
        <strong>
          action.createdDate | date:'dd/MM/yyyy HH:mm'
          action.createdBy
        </strong>
      </div>
      <div>action.notes</div>
    </mat-list>

  </div>
</div>

<button (click)="onClick()">name</button>

</ng-container>

但是要务实,不要花太多精力让它完全被动。您之前的解决方案也不错,您只需要改进您的模板即可。

注意:在这种情况下,您可以使用*ngIf 而不是*ngLet

【讨论】:

难道我们不需要手动检测更改,因为所有“动作”都发生在 ngAfterViewInit 而不是 ngOnInit 中?

以上是关于使用角度异步管道以完全反应式方式刷新数据的主要内容,如果未能解决你的问题,请参考以下文章

以角度将指令/输入传递给孩子的反应方式是啥?

角度 6 过滤异步管道结果

离子/角度:找不到管道“异步”

角度材料:反应性与模板?

如何使用 jboss 6.4 刷新角度?

以反应形式设置图像 url 在角度 2 中给出错误