如何确保数据在模板尝试呈现之前已经到达?
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 的内容。更加简洁和先进。
基本答案
要回答眼前的问题,从服务器获取异议并将它们放入模板中可以像这样简单地完成:
- 使用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>
- 在您的组件初始化时使用 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();
}
// ...
- 修复您的数据服务:
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,也经常使用它。但是,在您的示例中,您似乎在做一些非正统的事情:
您正在 ObservableStore 服务中创建一个商店 - 这向我表明您计划在您的应用程序中拥有多个商店。 Redux 的主要原则之一是全局不可变状态,这意味着在一个应用程序中通常只有一个 Redux 存储。
您似乎尝试从服务器获取初始数据集,然后在响应返回时创建您的商店。像这样将商店创建与 HTTP 请求耦合通常不是一个好主意。相反,我建议在初始化应用程序时创建一个空存储,然后在 HTTP 请求返回时通过 reducer 更新它。
您可以在 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 上维护了一些培训资源,我将在下面提供:
- 一个很好的 Angular2 特定的 redux 介绍:http://angular-2-training-book.rangle.io/handout/redux/
- Observables 简介:http://angular-2-training-book.rangle.io/handout/observables/
- Ng2-Redux 文档:https://github.com/angular-redux/ng2-redux/blob/master/README.md
我用 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 的内容。更加简洁和先进。
基本答案
要回答眼前的问题,从服务器获取异议并将它们放入模板中可以像这样简单地完成:
- 使用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>
- 在您的组件初始化时使用 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();
}
// ...
- 修复您的数据服务:
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,也经常使用它。但是,在您的示例中,您似乎在做一些非正统的事情:
您正在 ObservableStore 服务中创建一个商店 - 这向我表明您计划在您的应用程序中拥有多个商店。 Redux 的主要原则之一是全局不可变状态,这意味着在一个应用程序中通常只有一个 Redux 存储。
您似乎尝试从服务器获取初始数据集,然后在响应返回时创建您的商店。像这样将商店创建与 HTTP 请求耦合通常不是一个好主意。相反,我建议在初始化应用程序时创建一个空存储,然后在 HTTP 请求返回时通过 reducer 更新它。
您可以在 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 上维护了一些培训资源,我将在下面提供:
- 一个很好的 Angular2 特定的 redux 介绍:http://angular-2-training-book.rangle.io/handout/redux/
- Observables 简介:http://angular-2-training-book.rangle.io/handout/observables/
- Ng2-Redux 文档:https://github.com/angular-redux/ng2-redux/blob/master/README.md