将异步数据传递给 Angular 2+ 子组件的 3 种方法

让我们从一个常见的用例开始。您有一些从外部来源获得的数据(例如通过调用 API)。你想在屏幕上显示它。

但是,您希望将数据传递给子组件进行显示,而不是在同一个组件上显示它。

子组件可能有一些逻辑来在屏幕上显示之前对数据进行预处理。

我们的例子

例如,您有一个博客组件,用于显示博客详细信息和她的帖子。Blogger 组件将从 API 获取帖子列表。

不是在博客组件中编写显示帖子的逻辑,而是要重用由您的队友创建帖子组件,您需要做的是将帖子数据传递给它。

然后,帖子组件将按类别对帖子进行分组并相应地显示,如下所示:

博主和帖子

这不是很容易吗?

乍一看可能很容易。大多数情况下,我们将在组件初始化期间启动所有过程 – 在ngOnInit生命周期钩子期间有关组件生命周期钩子的更多详细信息,请参阅此处)。

在我们的例子中,你可能认为我们应该在帖子组件的ngOnInit期间运行帖子分组逻辑

但是,由于posts数据来自服务器,当博主组件将posts数据传递给posts 组件时,posts 组件ngOnInit在数据更新之前已经被触发您的帖子分组逻辑不会被触发。

我们如何解决这个问题?让我们编码!

我们的发布接口和数据

让我们从接口开始。

// 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,它将从父组件接收帖子数据。在我们的例子中,博客组件将提供。

可以看到我们实现了两个接口OnInitOnChanges这些是 Angular 提供给我们的生命周期钩子。我们没有做任何事情均ngOnInitngOnChanges,只是还没有。

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就像一个具有getset能力的属性,加上一个额外的功能;你可以订阅它。因此,每当财产发生变化时,我们都会收到通知,我们可以采取行动。在我们的例子中,它将触发分组逻辑。

请恢复以前解决方案中的所有更改

我们的博客组件没有变化:

// 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 支持gettersetter像 C# 和 Java 一样,请查看MDN以获取更多信息。在我们的例子中,我们将 拆分data为 usegettersetter然后,我们有一个私有变量_data来保存最新值。

要将值设置为BehaviorSubject,我们使用.next(theValue)为了获取值,我们使用.getValue(),就这么简单。

然后在组件初始化期间,我们订阅_data,监听更改,并在发生更改时调用我们的分组逻辑。

记下observablesubject,您需要取消订阅以避免性能问题和可能的内存泄漏。您可以手动执行此操作,ngOnDestroy也可以使用某个运算符来指示observablesubject在满足某些条件后自行取消订阅。

在我们的例子中,我们希望一旦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如果您想连续收听更改或想要保证。

就是这样。快乐编码!

觉得文章有用?

点个广告表达一下你的爱意吧 !😁