我目前正在使用Redux(ngrx)和RxJS(主要用于学习目的)构建一个Angular 2应用程序,但它仍然有点(至少可以说)让我感到困惑。
我正在尝试实现“/ projects”路由,以及“/ projects /:id”路由。在这两种情况下,我都会发出HTTP请求来获取所需的数据。
目前,如果我导航到“项目”(通过URL或通过导航调用ajax),它将从服务器返回所有15个左右的项目,并将它们添加到Redux上的“项目”商店。现在,如果我目前尝试从特定项目(例如,从浏览器的搜索栏 - >“localhost:3000 / projects / 2”)进入,它将只获取一个,这是我想要的并将其放入商店,但是如果我从那里导航到“项目”部分,它将只打印商店中的一个项目。
我想要完成的是:
我希望以高效,高效和优雅的方式实现这一目标。
我相信,我目前至少从两个地方订阅了同一个Observable,我认为这不是正确的做法。最重要的是,如果我先从“/ projects:/ id”路线进入,然后导航到“/ projects”路线,我仍然无法得到我想要的结果。
以下是我认为相关的代码:
projects.directive.ts
import { Component, OnInit } from '@angular/core';
import { ProjectsService } from '../shared/services/projects.service';
import { Observable } from 'rxjs/Observable';
import { Project } from '../../models/project.model';
@Component({
selector: 'projects',
templateUrl: './projects.html'
})
export class Projects implements OnInit {
private projects$: Observable<Project[]>
constructor(private projectsService: ProjectsService) {}
ngOnInit() {
this.projectsService.findProjects();
}
}
projectOne.directive.ts
import { Component, OnInit } from '@angular/core';
import { Params, ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { ProjectsService } from '../../shared/services/projects.service';
import { Project } from '../../../models/project.model';
@Component({
selector: 'projectOne',
templateUrl: './projectOne.html'
})
export class ProjectOneComponent implements OnInit {
private projects$: Observable<Project[]>
constructor(private route: ActivatedRoute, private projectsService: ProjectsService) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.projectsService.findProjects(params['id'])
});
}
}
*这里要注意的一些事情:我订阅了 this.route.params ,订阅了另一个Observable,我是否需要完全扁平化或者不是?这个概念仍然困扰着我
projects.html
<section>
<article *ngFor="let project of projectsService.projects$ | async">
<p>{{project?._id}}</p>
<p>{{project?.name}}</p>
<img src="{{project?.img}}" />
<a routerLink="{{project?._id}}">See more</a>
</article>
</section>
*这里我想说明我还在使用 projectsService.projects $ | async 在迭代上打印结果,我非常积极地影响......
projects.service.ts
import { Http } from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Store } from '@ngrx/store';
import { Project } from '../../../models/project.model';
import { AppStore } from '../../app.store';
import { ADD_PROJECTS } from '../../../reducers/projects.reducer';
@Injectable()
export class ProjectsService {
public projects$: Observable<Project[]>;
constructor(private _http: Http, private store: Store<AppStore>){
this.projects$ = store.select<Project[]>('projects');
}
fetchProjects(id) {
return this._http.get(`/api/projects?id=${id}`)
.map(res => res.json())
.map(({projectsList}) => ({ type: ADD_PROJECTS, payload: projectsList }))
.subscribe(action => this.store.dispatch(action));
}
findProjects(id: Number = 0) {
this.projects$.subscribe(projects => {
if (projects.length) {
if (projects.length === 1) {
return this.fetchProjects();
}
} else {
return this.fetchProjects(id ? id : '')
}
})
}
}
*我猜每次调用“findProjects”函数时我都会订阅Observable。不好,是吗?
*此外,每当我直接进入“/ projects /:id”时,使用此当前设置,它似乎执行了两次fetchProjects函数(我通过控制台日志记录得到了很多)。本质上,findProjects中的 this.projects $ 订阅会跳入并使用相应的id获取项目,但是它会再次进入并获取其他所有项目,最后它只是“消失”了? 为什么要自己呼叫,或者第二个呼叫来自哪里?
projects.reducer.ts
import { Project } from '../models/project.model';
import { ActionReducer, Action } from '@ngrx/store';
export const ADD_PROJECTS = 'ADD_PROJECTS';
export const projects: ActionReducer<Project[]> = (state: Project[] = [], action: Action) => {
switch (action.type) {
case ADD_PROJECTS:
return action.payload;
default:
return state;
}
};
*这是减速器暂时拥有的所有因为我仍然超级坚持其余部分。
无论如何,我想提前感谢你们。如果有任何不清楚或您需要更多信息,请告诉我。我知道这不仅包括一件事,而且可能非常容易或者根本不是,但我真的很想得到尽可能多的帮助,因为我真的被困在这里......再次感谢!
答案 0 :(得分:4)
一般来说,您的代码看起来“没问题”。有一些事情,我注意到了:
allProjects: Projects[]
,而不会被重新获取{{1仅在selectedProject: Project
allProjects
,因此您可能希望查看ngrx/effects作为从服务中调度操作的替代方法 - &gt;这部分是非常可选的,但在完美的 ngrx-app中,数据服务甚至不知道有商店。话虽如此,这里有一些代码改进,使其更多地面向面向ngrx的应用程序 - 但是我仍然建议您查看官方ngrx-example-app是非常好:
<强> projects.component.ts 强>
ngrx
<强> projects.component.html 强>
@Component({
selector: 'projects',
templateUrl: './projects.html'
})
export class Projects {
private projects$: Observable<Project[]> = his.store
.select<Project[]>('projects')
.map(projects => projects.all)
constructor(private store: Store<AppStore>) {
store.dispatch({type: ProjectActions.LOAD_ALL});
}
}
<强> project.component.ts 强>
<section>
<article *ngFor="let project of projects$ | async">
<!-- you don't need to use the questionmark here (project?.name) if you have something like "undefined" or "null" in your array, then the problem lies somewhere else -->
<p>{{project._id}}</p>
<p>{{project.name}}</p>
<img src="{{project.img}}" />
<a routerLink="{{project._id}}">See more</a>
</article>
</section>
project.service.ts =&gt;不知道商店
@Component({
selector: 'projectOne',
templateUrl: './projectOne.html'
})
export class ProjectOneComponent implements OnInit {
// project$ is only used with the async-pipe
private project$: Observable<Project[]> = this.route.params
.map(params => params['id'])
.switchMap(id => this.store
.select<Project[]>('projects')
.map(projects => projects.byId[id])
.filter(project => !!project) // filter out undefined & null
)
.share(); // sharing because it is probably used multiple times in the template
constructor(private route: ActivatedRoute,
private store: Store<AppStore>) {}
ngOnInit() {
this.route.params
.take(1)
.map(params => params['id'])
.do(id => this.store.dispatch({type: ProjectActions.LOAD_PROJECT, payload: id})
.subscribe();
}
}
<强> project.effects.ts 强>
@Injectable()
export class ProjectsService {
constructor(private _http: Http){}
fetchAll() {
return this._http.get(`/api/projects`)
.map(res => res.json());
}
fetchBy(id) {
return this._http.get(`/api/projects?id=${id}`)
.map(res => res.json());
}
}
<强> projects.reducer.ts 强>
@Injectable()
export class ProjectEffects {
private projects$: Observable<Project[]> = his.store
.select<Project[]>('projects')
.map(projects => projects.all);
constructor(private actions$: Actions,
private store: Store<AppStore>,
private projectsService: ProjectsService){}
@Effect()
public loadAllProjects$: Observable<Action> = this.actions$
.ofType(ProjectActions.LOAD_ALL)
.switchMap(() => this.projectsService.fetchAll()
.map(payload => {type: ProjectActions.ADD_PROJECTS, payload})
);
@Effect()
public loadSingleProject$: Observable<Action> = this.actions$
.ofType(ProjectActions.LOAD_PROJECT)
.map((action: Action) => action.payload)
.withLatestFrom(
this.projects$,
(id, projects) => ({id, projects})
)
.flatMap({id, projects} => {
let project = projects.find(project => project._id === id);
if (project) {
// project is already available, we don't need to fetch it again
return Observable.empty();
}
return this.projectsService.fetchBy(id);
})
.map(payload => {type: ProjectActions.ADD_PROJECT, payload});
}
正如您所看到的,这可能只是稍微多一点的代码,但只能在中心位置,代码可以轻松地重复使用 - 组件变得更加精简。
在理想的应用中,您还需要一个export interface ProjectsState {
all: Project[];
byId: {[key: string]: Project};
}
const initialState = {
all: [],
byId: {}
};
export const projects: ActionReducer<ProjectsState> = (state: ProjectsState = initialState, action: Action) => {
switch (action.type) {
case ADD_PROJECTS:
const all: Project[] = action.payload.slice();
const byId: {[key: string]: Project} = {};
all.forEach(project => byId[project._id] = project);
return {all, byId};
case ADD_PROJECT:
const newState: ProjectState = {
all: state.slice(),
byId: Object.assing({}, state.byId)
};
const project: Project = action.payload;
const idx: number = newState.all.findIndex(p => p._id === project._id);
if (idx >= 0) {
newState.all.splice(idx, 1, project);
} else {
newState.all.push(project);
}
newState.byId[project._id] = project;
return newState;
default:
return state;
}
};
和ProjectsComponent
的附加图层,例如ProjectOneComponent
和ProjectsRouteComponent
,它们只包含这样的模板: SingleProjectRoute
这会让<projectOne project="project$ | async"></projectOne>
从商店或其他任何知识中解放出来,它只会包含一个简单的输入:
ProjectOneComponent