Angular 2使用ngrx + RxJS订阅多个组件/路由来填充商店

时间:2016-12-06 22:44:22

标签: angular redux rxjs ngrx

我目前正在使用Redux(ngrx)和RxJS(主要用于学习目的)构建一个Angular 2应用程序,但它仍然有点(至少可以说)让我感到困惑。

我正在尝试实现“/ projects”路由,以及“/ projects /:id”路由。在这两种情况下,我都会发出HTTP请求来获取所需的数据。

目前,如果我导航到“项目”(通过URL或通过导航调用ajax),它将从服务器返回所有15个左右的项目,并将它们添加到Redux上的“项目”商店。现在,如果我目前尝试从特定项目(例如,从浏览器的搜索栏 - >“localhost:3000 / projects / 2”)进入,它将只获取一个,这是我想要的并将其放入商店,但是如果我从那里导航到“项目”部分,它将只打印商店中的一个项目。

我想要完成的是:

  • 如果我先进入“/ projects”,然后获取并将所有结果存入商店。
  • 如果满足上述情况并且我从那里导航到特定项目,使用链接标记,我想检查商店中是否有具有该特定ID的项目并将其返回。
  • 如果我直接从“/ projects /:id”进入,我只想获取该特定项目并将其放入商店。
  • 如果紧接上述点发生,我希望能够通过我的菜单或任何其他链接导航到“/ projects”,获取所有项目并使用所有项目更新我的“项目”商店(不仅仅是一个项目)已经存在于前一点)
  • 关于上述
  • ,我可能会遗漏的任何其他逻辑场景

我希望以高效,高效和优雅的方式实现这一目标。

我相信,我目前至少从两个地方订阅了同一个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;
  }
};

*这是减速器暂时拥有的所有因为我仍然超级坚持其余部分。

无论如何,我想提前感谢你们。如果有任何不清楚或您需要更多信息,请告诉我。我知道这不仅包括一件事,而且可能非常容易或者根本不是,但我真的很想得到尽可能多的帮助,因为我真的被困在这里......再次感谢!

1 个答案:

答案 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的附加图层,例如ProjectOneComponentProjectsRouteComponent,它们只包含这样的模板: SingleProjectRoute这会让<projectOne project="project$ | async"></projectOne>从商店或其他任何知识中解放出来,它只会包含一个简单的输入:

ProjectOneComponent