让我们从一个常见的用例开始。您有一些从外部来源获得的数据(例如通过调用 API)。你想在屏幕上显示它。
但是,您希望将数据传递给子组件进行显示,而不是在同一个组件上显示它。
子组件可能有一些逻辑来在屏幕上显示之前对数据进行预处理。
我们的例子
例如,您有一个博客组件,用于显示博客详细信息和她的帖子。Blogger 组件将从 API 获取帖子列表。
不是在博客组件中编写显示帖子的逻辑,而是要重用由您的队友创建的帖子组件,您需要做的是将帖子数据传递给它。
然后,帖子组件将按类别对帖子进行分组并相应地显示,如下所示:
这不是很容易吗?
乍一看可能很容易。大多数情况下,我们将在组件初始化期间启动所有过程 – 在ngOnInit生命周期钩子期间(有关组件生命周期钩子的更多详细信息,请参阅此处)。
在我们的例子中,你可能认为我们应该在帖子组件的ngOnInit期间运行帖子分组逻辑。
但是,由于posts
数据来自服务器,当博主组件将posts
数据传递给posts 组件时,posts 组件ngOnInit在数据更新之前已经被触发。您的帖子分组逻辑不会被触发。
我们如何解决这个问题?让我们编码!
- 演示:https : //ng-musing.firebaseapp.com/three-ways
- Github:https : //github.com/chybie/ng-musing/tree/master/src/app/three-ways
我们的发布接口和数据
让我们从接口开始。
// post.interface.ts
// each post will have a title and category
export interface Post {
title: string;
category: string;
}
// grouped posts by category
export interface GroupPosts {
category: string;
posts: Post[];
}
这是我们的模拟帖子数据assets/mock-posts.json
。
[
{ "title": "Learn Angular", "type": "tech" },
{ "title": "Forrest Gump Reviews", "type": "movie" },
{ "title": "Yoga Meditation", "type": "lifestyle" },
{ "title": "What is Promises?", "type": "tech" },
{ "title": "Star Wars Reviews", "type": "movie" },
{ "title": "Diving in Komodo", "type": "lifestyle" }
]
博客组件
让我们来看看我们的博客组件。
// blogger.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { Http } from '@angular/http';
import { Post } from './post.interface';
@Component({
selector: 'bloggers',
template: `
<h1>Posts by: {{ blogger }}</h1>
<div>
<posts [data]="posts"></posts>
</div>
`
})
export class BloggerComponent implements OnInit {
blogger = 'Jecelyn';
posts: Post[];
constructor(private _http: Http) { }
ngOnInit() {
this.getPostsByBlogger()
.subscribe(x => this.posts = x);
}
getPostsByBlogger() {
const url = 'assets/mock-posts.json';
return this._http.get(url)
.map(x => x.json());
}
}
我们将通过发出HTTP GET
调用来获取我们的模拟帖子数据。然后,我们将数据分配给posts
属性。随后,我们绑定posts
到视图模板中的帖子组件。
请注意,通常我们会在服务中执行 HTTP 调用。但是,由于这不是本教程的重点(为了缩短教程),我们将在同一组件中进行。
帖子组件
接下来,让我们编写posts 组件。
// posts.component.ts
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Post, GroupPosts } from './post.interface';
@Component({
selector: 'posts',
template: `
<div class="list-group">
<div *ngFor="let group of groupPosts" class="list-group-item">
<h4>{{ group.category }}</h4>
<ul>
<li *ngFor="let post of group.posts">
{{ post.title }}
</li>
</ul>
<div>
</div>
`
})
export class PostsComponent implements OnInit, OnChanges {
@Input()
data: Post[];
groupPosts: GroupPosts[];
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
}
groupByCategory(data: Post[]): GroupPosts[] {
// our logic to group the posts by category
if (!data) return;
// find out all the unique categories
const categories = new Set(data.map(x => x.category));
// produce a list of category with its posts
const result = Array.from(categories).map(x => ({
category: x,
posts: data.filter(post => post.category === x)
}));
return result;
}
}
我们有一个名为的输入data
,它将从父组件接收帖子数据。在我们的例子中,博客组件将提供。
可以看到我们实现了两个接口OnInit
和OnChanges
。这些是 Angular 提供给我们的生命周期钩子。我们没有做任何事情均ngOnInit
与ngOnChanges
,只是还没有。
该groupByCategory
函数是我们按类别对帖子进行分组的核心逻辑。分组后,我们将循环结果并在我们的模板中显示分组的帖子。
请记住在您的模块中导入这些组件(例如app.module.ts
)并将其添加到declarations
.
保存并运行它。您将看到一个仅包含博主姓名的空白页面。那是因为我们还没有编写我们的解决方案。
解决方案 1:使用 *ngIf
解决方案一是最简单的。使用*ngIf
在博客分量延迟职位组件的初始化。只有当 posts 变量具有 value 时,我们才会绑定 post 组件。然后,我们可以安全地在帖子组件中运行我们的分组逻辑ngOnInit
。
我们的博客组件:
// blogger.component.ts
...
template: `
<h1>Posts by: {{ blogger }}</h1>
<div *ngIf="posts">
<posts [data]="posts"></posts>
</div>
`
...
我们的帖子组件。
// posts.component.ts
...
ngOnInit() {
// add this line here
this.groupPosts = this.groupByCategory(this.data);
}
...
需要注意的几点:
- 由于分组逻辑在 中运行
ngOnInit
,这意味着它只会运行一次。如果将来有任何更新data
(从博客组件传入),它将不会再次触发。 - 因此,如果有人
posts: Post[]
将 blogger 组件中的属性更改为posts: Post[] = []
,则意味着我们的分组逻辑将使用空数组触发一次。当真实数据开始时,它不会再次被触发。
解决方案 2:使用 ngOnChanges
ngOnChanges
是一个生命周期钩子,它在检测到输入属性更改时运行。这意味着可以保证每次数据输入值发生变化时,如果我们将代码放在这里,我们的分组逻辑就会被触发。
请恢复以前解决方案中的所有更改
我们的博客组件,我们不再需要*ngIf
了。
// blogger.component.ts
...
template: `
<h1>Posts by: {{ blogger }}</h1>
<div>
<posts [data]="posts"></posts>
</div>
`
...
我们的帖子组件
// posts.component.ts
...
ngOnChanges(changes: SimpleChanges) {
// only run when property "data" changed
if (changes['data']) {
this.groupPosts = this.groupByCategory(this.data);
}
}
...
请注意,这changes
是一个键值对对象。关键是input
属性的名称,在我们的例子中是data
. 每当在 中编写代码时ngOnChanges
,您可能希望确保逻辑仅在目标数据更改时运行,因为您可能有一些输入。
这就是为什么我们只有在data
.
我不喜欢这个解决方案的一件事是我们失去了强类型,需要使用魔法字符串“数据”。如果我们将属性名称更改为data
其他名称,我们需要记住也要更改它。
当然,我们可以为此定义另一个接口,但这工作量太大。
解决方案 3:使用 RxJs BehaviorSubject
我们可以利用 RxJsBehaviorSubject
来检测变化。在我们继续之前,我建议您在这里查看官方文档的单元测试。
假设这BehaviorSubject
就像一个具有get
和set
能力的属性,加上一个额外的功能;你可以订阅它。因此,每当财产发生变化时,我们都会收到通知,我们可以采取行动。在我们的例子中,它将触发分组逻辑。
请恢复以前解决方案中的所有更改
我们的博客组件没有变化:
// blogger.component.ts
...
template: `
<h1>Posts by: {{ blogger }}</h1>
<div>
<posts [data]="posts"></posts>
</div>
`
...
让我们更新我们的 post 组件以使用BehaviorSubject
.
// posts.component.ts
...
// initialize a private variable _data, it's a BehaviorSubject
private _data = new BehaviorSubject<Post[]>([]);
// change data to use getter and setter
@Input()
set data(value) {
// set the latest value for _data BehaviorSubject
this._data.next(value);
};
get data() {
// get the latest value from _data BehaviorSubject
return this._data.getValue();
}
ngOnInit() {
// now we can subscribe to it, whenever input changes,
// we will run our grouping logic
this._data
.subscribe(x => {
this.groupPosts = this.groupByCategory(this.data);
});
}
...
首先,如果您不知道 Javacript 支持getter
并setter
像 C# 和 Java 一样,请查看MDN以获取更多信息。在我们的例子中,我们将 拆分data
为 usegetter
和setter
。然后,我们有一个私有变量_data
来保存最新值。
要将值设置为BehaviorSubject
,我们使用.next(theValue)
。为了获取值,我们使用.getValue()
,就这么简单。
然后在组件初始化期间,我们订阅_data
,监听更改,并在发生更改时调用我们的分组逻辑。
记下observable
和subject
,您需要取消订阅以避免性能问题和可能的内存泄漏。您可以手动执行此操作,ngOnDestroy
也可以使用某个运算符来指示observable
并subject
在满足某些条件后自行取消订阅。
在我们的例子中,我们希望一旦groupPosts
有价值就取消订阅。我们可以在我们的订阅中添加这一行来实现这一点。
// posts.component.ts
...
ngOnInit() {
this._data
// add this line
// listen to data as long as groupPosts is undefined or null
// Unsubscribe once groupPosts has value
.takeWhile(() => !this.groupPosts)
.subscribe(x => {
this.groupPosts = this.groupByCategory(this.data);
});
}
...
有了这一行.takeWhile(() => !this.groupPosts)
,它会在完成后自动取消订阅。还有其他方法可以自动取消订阅,例如take
, take Util
,但这超出了本主题。
通过使用BehaviorSubject
,我们可以获得强类型,可以控制和倾听变化。唯一的缺点是您需要编写更多代码。
我应该使用哪一种?
著名的问题伴随着著名的答案:这取决于。
使用*ngIf
,如果你确信你的变化只运行一次,这是非常简单的。使用ngOnChanges
或者BehaviorSubject
如果您想连续收听更改或想要保证。
- 演示:https : //ng-musing.firebaseapp.com/three-ways
- Github:https : //github.com/chybie/ng-musing/tree/master/src/app/three-ways
就是这样。快乐编码!