在 Angular 中使用结构指令处理可观察对象
在 Angular 中,处理可观察对象是一个经常被讨论的话题。在模板中显示响应式值的方法有很多,但有时它们都显得有些笨拙。让我们来探索一下有哪些可用的选项,它们的工作原理以及如何改进它们。
observe
本文开发的指令可在 ngx-observe 库中找到📚如果您
在 GitHub 上给它一颗星⭐️,我将不胜感激,这有助于让人们了解它
处理将数据带入组件视图的可观察对象主要有两种解决方案:
- 手动订阅管理
- 将AsyncPipe与NgIf结合使用。
Tomas Trajan 已经写了一篇全面的文章,对两种方法进行了比较,最终宣布第二种方法获胜。
Tomas Trajan 的Angular 常见问题终极答案:subscribe() vs | async Pipe
NgIf 和 AsyncPipe 非常契合,但并非天作之合。它有一些明显的缺点:
- 我们的可观察对象发出的虚假值(
false
,,,,)将导致显示模板 - 这是因为 NgIf 不知道可观察对象0
,并且会简单地评估 AsyncPipe 传递给它的内容''
null
undefined
else
- 我们只能用 NgIf 捕获一个值,因此无法访问可观察对象发出的错误
- 当可观察对象仍在加载时和发生错误时使用相同的模板引用,因为两者都会触发
else
NgIf 的模板
让我们了解一下该方法如何发挥作用以及如何进一步改进它。
解构 ngIf 和 AsyncPipe
将反应数据放入视图涉及在我们的组件中定义可观察对象并通过著名的语法组合 NgIf 指令和 AsyncPipe 来绑定它as
。
但请记住,在处理代表动作的可观察对象时,您将无法使用 AsyncPipe - 例如,当您根据按钮单击更新用户时。
@Component({
...
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UsersComponent {
users$: Observable<User[]> = this.users.getAll()
constructor(private users: UserService) {}
}
<p *ngIf="users$ | async as users; else loading">
There are {{ users.length }} online.
</p>
<ng-template #loading>
<p>Loading ...</p>
</ng-template>
使用这种方法已经是一种很好的声明式处理可观察对象的方式了。让我们逐一看看它的优点,看看它们是如何工作的。
无订阅管理
我们无需取消订阅,因为我们从未手动订阅过users$
可观察对象。这一切都由 AsyncPipe 处理。查看GitHub 上的代码,你可以看到它是如何订阅传入的可观察对象transform()
并在其中取消订阅的——基本上就像我们手动调用或使用 RxJS 操作符takeUntilngOnDestroy()
一样,只是包装在了 Angular 管道中。subscribe()
unsubscribe()
OnPush 变更检测
一旦使用 AsyncPipe,就可以通过将组件配置OnPush
为其ChangeDetectionStrategy来提高性能。这并非与 AsyncPipe 本身绑定。相反,管道会在新的可观察值到达时显式触发变更检测(参见其代码中的 140-145行)。
目前还没有关于OnPush
变更检测具体工作原理的官方文档。我不喜欢依赖第三方博客文章来获取这些重要信息(你也不应该),所以让我们再看一些代码——或者更确切地说是代码测试。OnPush 有一个专门的测试套件,可以告诉我们所有需要了解的信息。在这种模式下,变更检测仅在以下三种情况下自动运行:
- 当组件的输入被重新分配时
- 当组件或其子组件上发生事件时
- 当组件“脏”时,意味着它通过调用 ChangeDetectorRef 明确标记为需要进行变更检测
markForCheck()
(就像在 AsyncPipe 中所做的那样)
变更检测意味着 Angular 将使用组件类实例中的值来更新模板绑定。使用默认的 ChangeDetectionStrategy 时,很多情况下都会发生这种情况,而不仅仅是上面提到的三种情况——这就是使用 OnPush 时性能提升的原因。
更新模板绑定通常意味着更新 DOM,这是一个相对昂贵的操作。因此,当 Angular 减少更新频率时,你的应用程序将运行得更流畅。另一方面,当发生更改时,你必须明确地告知 Angular——或者更确切地说,让 AsyncPipe 来做。
有条件地渲染模板
NgIf 在 Angular 中被称为结构指令- 之所以说是“结构”,是因为它操作的是 DOM:
结构指令负责 HTML 布局。它们通常通过添加、删除或操作元素来塑造或重塑 DOM 的结构。
指令名称前面的星号 (*) 告诉 Angular 使用微语法来评估赋值。虽然这听起来可能令人生畏,但它只是在指令实例上调用 JavaScript 设置器的一种简便方法。这种微语法表达式中的每个关键字 - 比如else
NgIf - 都对应于指令代码中的设置器。设置器的命名遵循以指令选择器开头,后跟关键字的模式。就像您从官方资料中看到的那样else
187行。这个 setter 接受一个TemplateRef,它是对标签的引用。在上面的例子中,它标有。结构指令可以将模板引用渲染到视图中,并有条件地提供上下文 - 很快会详细介绍。set ngIfElse(templateRef: TemplateRef<NgIfContext<T>>|null)
ng-template
#loading
还有一个关键字then
,可以用来动态地为 truthy 分支分配模板。默认情况下,NgIf 会使用分配给它的标签作为模板(参见第160行)。
现在,每当底层可观察对象发出新值时,AsyncPipe 都会通过我们的微语法表达式将其传递给 NgIf,并在其内部触发重新求值。随后,else
当可观察对象没有发出任何值(例如仍在加载或发生错误)或该值本身为假值时,该指令将添加 -template 。then
当可观察对象发出真值时,将添加 -template 。
所有这些的最后一点是as
关键字。事实证明,NgIf 指令的源代码中没有对应的 setter。这是因为它并非特定于 NgIf,而是与模板引用的上下文有关。这样的上下文是一种声明在渲染模板时所有可用变量的类型。对于 NgIf 来说,这种类型NgIfContext<T>
如下所示:
export class NgIfContext<T> {
public $implicit: T;
public ngIf: T;
}
泛型类型T
指的是你传递给指令的类型。所以当你绑定时,'hello'
它会是string
。当你Observable<string>
通过 AsyncPipe 传递 时,管道会有效地解开可观察对象,并T
再次将其缩小为string
。
我们可以通过使用模式中的关键字声明模板输入变量来获取此类模板上下文中的任何内容。以下是 NgIf 的一个例子:let
let-<your-var-name>="<context-property>"
<ng-template [ngIf]="'hello'" let-a="$implicit" let-b="ngIf" let-c>
<p>a = {{ a }}</p>
<p>b = {{ b }}</p>
<p>c = {{ c }}</p>
</ng-template>
<p *ngIf="'hello' as d">
d = {{ d }}
</p>
下面是实际示例,显示实际上所有变量a
、b
和c
都d
将被分配给'hello'
。
任何模板上下文中的属性$implicit
都将分配给未引用特定上下文属性的模板输入变量——在本例中为c
。这是一个便捷的快捷方式,因此您无需了解所使用的每个指令的具体上下文。它也解释了为什么a
和c
会获得相同的值。
对于 NgIf 来说, context 属性ngIf
也会引用被求值的条件。因此,它b
的求值结果也为'hello'
。这也是该关键字的原理as
。更准确地说,Angular 会根据你输入的字面量创建一个模板输入变量,as
并将与指令本身同名的 context 属性赋给它。同样,目前没有官方文档对此进行了说明,但有一些针对此功能的测试。
可观察对象的结构指令
正如我们现在看到的,这些部分都没有什么神奇之处——我们自己都能实现。那么,让我们想出一个特别适合在模板中渲染可观察对象的方法,然后一步步探索它:
import {
Directive, Input, TemplateRef, ViewContainerRef,
OnDestroy, OnInit, ChangeDetectorRef
} from '@angular/core'
import { Observable, Subject, AsyncSubject } from "rxjs";
import { takeUntil, concatMapTo } from "rxjs/operators";
export interface ObserveContext<T> {
$implicit: T;
observe: T;
}
export interface ErrorContext {
$implicit: Error;
}
@Directive({
selector: "[observe]"
})
export class ObserveDirective<T> implements OnDestroy, OnInit {
private errorRef: TemplateRef<ErrorContext>;
private beforeRef: TemplateRef<null>;
private unsubscribe = new Subject<boolean>();
private init = new AsyncSubject<void>();
constructor(
private view: ViewContainerRef,
private nextRef: TemplateRef<ObserveContext<T>>,
private changes: ChangeDetectorRef
) {}
@Input()
set observe(source: Observable<T>) {
if (!source) {
return
}
this.showBefore()
this.unsubscribe.next(true);
this.init.pipe(
concatMapTo(source),
takeUntil(this.unsubscribe)
).subscribe(value => {
this.view.clear()
this.view.createEmbeddedView(this.nextRef, {$implicit: value, observe: value})
this.changes.markForCheck()
}, error => {
if (this.errorRef) {
this.view.clear()
this.view.createEmbeddedView(this.errorRef, {$implicit: error})
this.changes.markForCheck()
}
})
}
@Input()
set observeError(ref: TemplateRef<ErrorContext>) {
this.errorRef = ref;
}
@Input()
set observeBefore(ref: TemplateRef<null>) {
this.beforeRef = ref;
}
ngOnDestroy() {
this.unsubscribe.next(true)
}
ngOnInit() {
this.showBefore()
this.init.next()
this.init.complete()
}
private showBefore(): void {
if (this.beforeRef) {
this.view.clear()
this.view.createEmbeddedView(this.beforeRef)
}
}
}
我们还有一个示例来展示它的用法,以便我们可以看到其中的联系:
<p *observe="users$ as users; before loadingTemplate; error errorTemplate">
There are {{ users.length }} online.
</p>
<ng-template #loadingTemplate>
<p>Loading ...</p>
</ng-template>
<ng-template #errorTemplate let-error>
<p>{{ error }}</p>
</ng-template>
从构造函数开始,我们可以获取ViewContainerRef的句柄。这将允许我们通过渲染模板来代替指令来操作 DOM。
Angular 还会为我们提供一个指向我们放置了 的标签模板的引用*observe
。在我们的示例中,它是p
绑定可观察值值的标签。我们可以调用它nextRef
(因为它用于显示下一个可观察值),并像 NgIf 一样为其上下文赋值。ObserveContext<T>
它将基于底层可观察值进行泛型处理,并将其值传递给隐式模板输入变量或通过as
关键字(因为有一个 context 属性,就像我们的指令一样)提供。
我们还将注入一个,ChangeDetectorRef
以便我们的指令能够与OnPush
变化检测一起工作。
设置器observeError
遵循observeBefore
微语法命名,可用于传递模板,以便在可观察对象发出值之前(基本上是在加载时)以及可观察对象出现错误时显示。
在第一种情况下,我们无法提供有意义的上下文,因此TemplateRef
for函数observeBefore
有一个泛型参数null
。我们将通过调用模板来渲染此模板,而无需上下文,view.createEmbeddedView()
正如您在 中看到的那样showBefore()
。我们还将确保首先渲染clear()
视图 - 否则我们可能会同时渲染多个模板。
如果发生错误,我们可以在上述$implicit
属性中提供一个包含实际错误的上下文。我们将为这个特定的上下文创建另一个名为 的类型,ErrorContext
并使用它来缩小TemplateRef
传入的相应值。这最终使我们能够在示例中observeError
定义模板输入变量。let-error
被AsyncSubject<void>
调用的init
仅仅是OnInit 钩子的一个可观察包装器。一旦我们从内部让它完成,ngOnInit()
它就会在订阅时发出。这将防止我们过早渲染任何模板。
事情变得有趣的地方在于setter observe
。它是我们指令的主要 setter,在我们的示例中,它接收users$
可观察对象。当source
传入这样的 时,所有先前的订阅都会通过this.unsubscribe.next(true)
与takeUntil
操作符的组合取消——这与在手动订阅管理中取消订阅的方式非常相似。接下来,我们将通过管道关闭,然后使用 RxJS 操作符concatMapTo映射到传入的可观察对象上,ngOnDestroy()
以确保等待。该操作符将等待前一个可观察对象完成,然后监听下一个可观察对象。ngOnInit()
init
最终,我们订阅了底层的可观察对象,每当有新值到来时,我们都会更新视图,首先清除它,然后基于模板创建一个包含该值的上下文的嵌入视图。最后,我们会通知变化检测器,以markForCheck()
支持OnPush
检测。
当发生错误时,我们将使用显示错误的模板并仅支持隐式输入变量来执行几乎相同的操作 - 前提是有可用的错误模板。
结论
我们的新指令比 NgIf 和 AsyncPipe 更适合处理可观察对象:
- 它可以显示虚假值
- 它允许您为加载和错误定义单独的模板
- 它允许您从错误模板内部访问错误
我在 StackBlitz 上整理了一个示例,展示了该指令的实际应用。我认为在某些情况下,它甚至比 NgIf 与 AsyncPipe 的结合更有用。总之,我们学习了很多关于结构化指令和变更检测的知识,从而更好地理解了该框架。
文章来源:https://dev.to/angular/handling-observables-with-structural-directives-in-angular-112j如果您有任何疑问,请随时在下方留言或在 Twitter 上关注我@n_mehlhorn。您也可以在 Twitter 上关注我,并加入我的邮件列表,以便及时了解新文章发布,并获取有关 Angular 和 Web 开发的小技巧。