Angular2:如何在渲染组件之前加载数据?

180

在组件呈现之前,我想从我的API中加载一个事件。目前我正在使用我的API服务,并从组件的ngOnInit函数中调用它。

我的EventRegister组件:

import {Component, OnInit, ElementRef} from "angular2/core";
import {ApiService} from "../../services/api.service";
import {EventModel} from '../../models/EventModel';
import {Router, ROUTER_DIRECTIVES, ROUTER_PROVIDERS, RouteConfig, RouteParams, RouterLink} from 'angular2/router';
import {FORM_PROVIDERS, FORM_DIRECTIVES, Control} from 'angular2/common';

@Component({
    selector: "register",
    templateUrl: "/events/register"
    // provider array is added in parent component
})

export class EventRegister implements OnInit {
    eventId: string;
    ev: EventModel;

    constructor(private _apiService: ApiService, 
                        params: RouteParams) {
        this.eventId = params.get('id');
    }

    fetchEvent(): void {
        this._apiService.get.event(this.eventId).then(event => {
            this.ev = event;
            console.log(event); // Has a value
            console.log(this.ev); // Has a value
        });
    }

    ngOnInit() {
        this.fetchEvent();
        console.log(this.ev); // Has NO value
    }
}

我的EventRegister模板

<register>
    Hi this sentence is always visible, even if `ev property` is not loaded yet    
    <div *ngIf="ev">
        I should be visible as soon the `ev property` is loaded. Currently I am never shown.
        <span>{{event.id }}</span>
    </div>
</register>

我的API服务

import "rxjs/Rx"
import {Http} from "angular2/http";
import {Injectable} from "angular2/core";
import {EventModel} from '../models/EventModel';

@Injectable()
export class ApiService {
    constructor(private http: Http) { }
    get = {
        event: (eventId: string): Promise<EventModel> => {
            return this.http.get("api/events/" + eventId).map(response => {
                return response.json(); // Has a value
            }).toPromise();
        }     
    }     
}

ngOnInit函数中的API调用完成之前,该组件会被渲染。因此,我从未在视图模板中看到事件ID。所以这看起来是一个异步问题。我期望evEventRegister组件)的绑定在ev属性被设置后能做一些工作。不幸的是,当属性被设置时,它并没有显示标记为*ngIf="ev"

问题:我的方法可行吗?如果不是,加载数据的最佳方式是什么?

注意:在此Angular2教程中使用了ngOnInit方法。

编辑:

两个可能的解决方案。首先是放弃fetchEvent,直接在ngOnInit函数中使用API服务。

ngOnInit() {
    this._apiService.get.event(this.eventId).then(event => this.ev = event);
}

第二种解决方案。与给出的答案相似。

fetchEvent(): Promise<EventModel> {
    return this._apiService.get.event(this.eventId);
}

ngOnInit() {
    this.fetchEvent().then(event => this.ev = event);
}

身份验证守卫也在此目的中发挥作用。 - Praveen Gubbala
为什么你使用 Http 而不是 HttpClient?如果你使用 HttpClient,你就不需要手动将响应转换为 json,因为它会自动完成。 - J.Doe
3个回答

155

更新

原始内容

当执行this.fetchEvent();后执行console.log(this.ev)时,这并不意味着fetchEvent()已经完成调用,这只是表示它已被安排。当执行console.log(this.ev)时,甚至还没有发出对服务器的调用,当然还没有返回值。

fetchEvent()更改为返回一个Promise

     fetchEvent(){
        return  this._apiService.get.event(this.eventId).then(event => {
            this.ev = event;
            console.log(event); // Has a value
            console.log(this.ev); // Has a value
        });
     }

ngOnInit() 修改为等待 Promise 完成

    ngOnInit() {
        this.fetchEvent().then(() =>
        console.log(this.ev)); // Now has value;
    }

对于您的使用情况,实际上这并不会给您带来太多好处。

我的建议是:用<div *ngIf="isDataAvailable"> (模板内容)</div>包装整个模板,并在ngOnInit()中使用。

    isDataAvailable:boolean = false;

    ngOnInit() {
        this.fetchEvent().then(() =>
        this.isDataAvailable = true); // Now has value;
    }

4
实际上,你的第一条评论效果非常好。我只是删除了整个 fetchEvent 函数,将 apiService.get.event 函数放入 ngOnInit 中,这样它就按照我的意图运行了。谢谢!我会尽快接受你的答案。 - Tom Aalbers
由于某些原因,我在Angular 1.x上使用了这种方法。依我之见,这应该被视为一种hack而不是一个适当的解决方案。我们应该在Angular 5中做得更好。 - windmaomao
你到底不喜欢那个方案的什么?我认为这两种方法都很好。 - Günter Zöchbauer

68
你可以使用Angular2+中的解析器来预取数据,解析器会在组件完全加载之前处理你的数据。
有许多情况下,只有在特定事件发生时才想要加载组件,例如仅在用户已登录后导航到仪表板,在这种情况下,解析器非常方便。
看一下我为你创建的简单图示,其中介绍了一种使用解析器将数据发送到组件的方法。

enter image description here

Resolver应用于您的代码非常简单,我为您创建了代码片段,以便您了解如何创建Resolver:

import { Injectable } from '@angular/core';
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { MyData, MyService } from './my.service';

@Injectable()
export class MyResolver implements Resolve<MyData> {
  constructor(private ms: MyService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<MyData> {
    let id = route.params['id'];

    return this.ms.getId(id).then(data => {
      if (data) {
        return data;
      } else {
        this.router.navigate(['/login']);
        return;
      }
    });
  }
}

模块中:

import { MyResolver } from './my-resolver.service';

@NgModule({
  imports: [
    RouterModule.forChild(myRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    MyResolver
  ]
})
export class MyModule { }

你可以在你的组件中这样访问它:
/////
 ngOnInit() {
    this.route.data
      .subscribe((data: { mydata: myData }) => {
        this.id = data.mydata.id;
      });
  }
/////

路由 中,通常在 app.routing.ts 文件中,代码如下:

////
{path: 'yourpath/:id', component: YourComponent, resolve: { myData: MyResolver}}
////

1
我认为您错过了“路由”数组下的路由配置条目(通常在_app.routing.ts_文件中):{path: 'yourpath/:id', component: YourComponent, resolve: { myData: MyData}} - Voicu
1
@Voicu,它不应该涵盖所有部分,但我同意,这使得答案更全面,所以我添加了...谢谢。 - Alireza
5
仅需更正最后一部分,路由中的resolve参数需要指向解析器本身,而不是数据类型,即resolve: { myData: MyResolver } - Chris Haines
1
MyData是在MyService中定义的模型对象吗? - Isuru Madusanka
1
这个解决方案适用于在页面加载之前获取数据,但不适用于检查已登录的用户或用户角色。为此,您应该使用 Guards。 - Angel Q

65

我发现的一个好方法是在UI上做如下操作:

<div *ngIf="isDataLoaded">
 ...Your page...
</div

只有当isDataLoaded为true时,页面才会被渲染。


3
我认为这个解决方案仅适用于Angular 1而不是Angular >= 2,因为Angular的单向数据流规则禁止在视图组合后更新视图。这两个钩子都在组件视图组合后触发。 - zt1983811
4
div元素根本没有被呈现。我们不是想停止它的呈现,而是想延迟它的呈现。它不能正常工作的原因是在Angular2中没有双向绑定。@phil能否解释一下你是如何做到的? - ishandutta2007
1
好的,我之前使用的是 ChangeDetectionStrategy.Push,将其改为 ChangeDetectionStrategy.Default 可以使其与模板进行双向绑定。 - ishandutta2007
1
Angular 6。运行正常。 - TDP

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接