如何确保数据在模板尝试呈现之前已经到达?

How do I ensure the data has arrived before the template tries to render it?

我用 angular-cli、Immutable 和 Redux 制作了一个应用程序。我遵循了 this 文章中的说明,该文章没有描述如何获取数据。我的应用需要使用来自异步 http 调用的数据来初始化 Redux 存储。

我有一个用于列出具有模板的数据的组件。该组件从依赖于进行 http 调用的服务的商店获取数据。 http 调用有效,但应用程序抛出异常,表明列表组件正在尝试在数据到达之前获取数据。

我的存储库是 here

该应用程序的演示是 here

错误信息:

main.js:21 TypeError: Cannot read property 'getState' of undefined
    at a.get [as objections] (https://dancancro.github.io/bernierebuttals/main.js:18:22268)
    at a._View_a0.detectChangesInternal (a.template.js:189:37)
    at a.detectChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13138)
    at a.detectViewChildrenChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13774)
    at a._View_a_Host0.detectChangesInternal (a.template.js:34:8)
    at a.detectChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13138)
    at a.detectContentChildrenChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13588)
    at a.detectChangesInternal (https://dancancro.github.io/bernierebuttals/main.js:32:13345)
    at a.detectChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13138)
    at a.detectViewChildrenChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13774)

以下是代码的一些相关部分:(我正在研究这个。存储库包含当前代码)

list.component.html

...

<ul id="objection-list" [sortablejs]="store.objections" [sortablejsOptions]="options" (update)="setTouched()">
<li *ngFor="let objection of store.objections">
   <list-objection 
   [objection]="objection" 
   [editable]="editable"
   (onEdit)="setTouched()" 
   (onReordered)="setReordered(objection)"
   ></list-objection>
</li>
</ul>

...

list.component.ts

import { Component, OnInit, ContentChildren, QueryList } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

import { SortablejsOptions, SORTABLEJS_DIRECTIVES } from 'angular-sortablejs';
import Immutable = require('immutable');

import { ObjectionComponent } from './objection/objection.component';
import { ObjectionModel } from '../objection';
import { ObjectionStore } from '../objection-store';
import { DataService } from '../data.service';
import { addObjection } from '../actions';


@Component({
  moduleId: module.id,
  selector: 'app-list',
  templateUrl: 'list.component.html',
  styleUrls: ['list.component.css'],
  providers: [ObjectionStore, DataService],
  directives: [ObjectionComponent, SORTABLEJS_DIRECTIVES]
})
export class ListComponent implements OnInit {

  private sub: any;
  editable: boolean = false;
  touched: boolean = false;
  expanded: boolean = false;
  options: SortablejsOptions = {
    disabled: false
  };
  objectionID: number;

  constructor(
    private store: ObjectionStore,
    private route: ActivatedRoute) {}

  ...

}

反对-store.ts

import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import Immutable = require('immutable');
import { createStore } from 'redux';

import { ObjectionAction } from './actions';
import { reducer } from './reducer';
import { ObjectionModel } from './objection';
import { DataService } from './data.service';

@Injectable()
export class ObjectionStore {
  private sub: any;
  store: any;

  constructor(
    private dataService: DataService) {
           this.store = createStore(reducer, Immutable.List<ObjectionModel>(objections.json()));
    });

  }

data.service.ts

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';

import { ObjectionModel } from './objection';
import { Area } from './area';

let objectionsPromise;

@Injectable()
export class DataService {
    result: Object;
    combined: any;
    error: Object;
    getUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec?json=1';
    postUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec';

    static getObjection(objections: any[], id: number): ObjectionModel {
        return objections.filter(function(objection) {
            return objection.id === id
        })[0];
    }

    constructor(private http: Http) {
        objectionsPromise = this.http.get(this.getUrl).toPromise();
    }

您的数据服务有误。你错过了 promise/observable 的要点。 您应该阅读有关 http 客户端的 angular 文档。至少阅读这一部分: https://angular.io/docs/ts/latest/guide/server-communication.html#!#promises

构造函数中没有http调用! 这更像它:

getHeroes (): Promise<Hero[]> {
  return this.http.get(this.heroesUrl)
                  .toPromise()
                  .catch(this.handleError);
}

在你习惯之后,我真的建议你阅读一些关于 Observables 的内容。更加简洁和先进。

基本答案

要回答眼前的问题,从服务器获取异议并将它们放入模板中可以像这样简单地完成:

  1. 使用async pipe 将可观察对象直接绑定到模板中。异步管道 'unboxes' observables(也是承诺)并在它们发生变化时更新您的模板。
<ul id="objection-list"
    [sortablejs]="store.objections"
    [sortablejsOptions]="options"
    (update)="setTouched()">
  <li *ngFor="let objection of objections | async">
    <list-objection 
                    [objection]="objection" 
                    [editable]="editable"
                    (onEdit)="setTouched()" 
                    (onReordered)="setReordered(objection)">
    </list-objection>
  </li>
</ul>
  1. 在您的组件初始化时使用 DataService 初始化此可观察对象:
@Component({
  moduleId: module.id,
  selector: 'app-list',
  templateUrl: 'list.component.html',
  styleUrls: ['list.component.css'],
  providers: [ObjectionStore, DataService],
  directives: [ObjectionComponent, SORTABLEJS_DIRECTIVES]
})
export class ListComponent implements OnInit {

  // ...

  private objections: Observable<ObjectionModel[]>;

  constructor(
    private dataService: DataService,
    private route: ActivatedRoute) {}

  ngOnInit() {
    this.objections = this.dataService.getObjections();
  }

  // ...
  1. 修复您的数据服务:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

import { ObjectionModel } from './objection';

@Injectable()
export class DataService {
    result: Object;
    combined: any;
    error: Object;
    getUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec?json=1';
    postUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec';

    getObjections(private http: Http): Observable<ObjectionModel[]> {
        return this.http.get(this.getUrl) // returns an observable of the response
            .map(response => response.json()); // transforms it into an observable of ObjectionModels
    }
}

关于 Redux 的注释

请注意,这一切都是在没有 Redux 的情况下完成的。

总的来说,我喜欢 Redux,也经常使用它。但是,在您的示例中,您似乎在做一些非正统的事情:

  1. 您正在 ObservableStore 服务中创建一个商店 - 这向我表明您计划在您的应用程序中拥有多个商店。 Redux 的主要原则之一是全局不可变状态,这意味着在一个应用程序中通常只有一个 Redux 存储。

  2. 您似乎尝试从服务器获取初始数据集,然后在响应返回时创建您的商店。像这样将商店创建与 HTTP 请求耦合通常不是一个好主意。相反,我建议在初始化应用程序时创建一个空存储,然后在 HTTP 请求返回时通过 reducer 更新它。

  3. 您可以在 Angular 2 中执行原始 Redux,但您可能会发现让它与 Angular 的大量可观察 API 一起工作有点令人沮丧。幸运的是,人们(包括我)已经以面向 Observable 的 redux 库的形式为你完成了这项工作,比如 ng2-redux and ngrx/store

如果你使用 ng2-redux,事情会看起来更像这样:

顶级应用程序组件:构建您的商店并对其进行初始化:

import { NgRedux } from 'ng2-redux';
import { rootReducer } from './reducers';

@Component({ /* ... */ })
class App {
  constructor(private ngRedux: NgRedux<any>) {
    this.ngRedux.configureStore(rootReducer, {});
  }
}

列表组件:将您的模板绑定到商店当前数据的选择器。还会在初始化时触发数据提取。

import { NgRedux, select } from 'ng2-redux';

@Component({
  moduleId: module.id,
  selector: 'app-list',
  templateUrl: 'list.component.html',
  styleUrls: ['list.component.css'],
  providers: [DataService],
  directives: [ObjectionComponent, SORTABLEJS_DIRECTIVES]
})
export class ListComponent implements OnInit {

  // ...

  // Magic selector from ng2-redux that makes an observable out
  // of the 'objections' property of your store.
  @select('objections') objections: Observable<ObjectionModel[]>;

  constructor(
      private ngRedux: NgRedux<any>,
      private dataService: DataService) {}

  ngOnInit() {
      this.subscription = this.dataService.getObjections()
         .subscribe(objections => this.ngRedux.dispatch({
             type: FETCH_OBJECTIONS_OK,
             payload: objections
         },
         error => this.ngRedux.dispatch({
             type: FETCH_OBJECTIONS_ERROR,
             error: error
         });
      )
  }
}

好的...那么数据实际上是如何进入商店的呢?通过减速器。请记住,在 redux store state 中,只能从 reducer.

更改
export function objectionReducer(state = [], action) {
    switch(action.type) {
        case FETCH_OBJECTIONS_OK: return [ ...action.payload ];
        case ADD_OBJECTION: return [ ...state, action.payload ];
        // etc.
    }

    return state;
}

如果需要,我们也可以跟踪 reducer 中的错误,具体结构由您决定。

export function errorReducer(state = {}, action) {
    switch(action.type) {
        case FETCH_OBJECTIONS_ERROR: return { objectionFetch: action.error }
    }
}

因为我们只有一个 store,所以我们将 reducer 模块化并将它们组合在一起:

import { combineReducers } from 'redux';
import { objectionReducer } from './objection.reducer';
import { errorReducer } from './error.reducer';

export const rootReducer = combineReducers({
    objections: objectionReducer,
    error: errorReducer
});

更多 Learn/Disclosure

披露:我是 Ng2-Redux 的作者之一。然而,Ngrx/Store 也是使用 Ng2 进行 redux 的一个很好的选择,虽然实现方式不同,但使用它与我上面描述的非常相似。

我和我的同事们还在 Angular2 和 Redux 上维护了一些培训资源,我将在下面提供: