繁体   English   中英

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

[英]Angular 2 using ngrx + RxJS subscribing on multiple components/routes to populate store

我目前正在使用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 $ subscription跳转并使用相应的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;
  }
};

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

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

通常,您的代码看起来“没问题”。 有一些事情,我注意到了:

  • 你正在做的事情,如检查项目长度等。 可能会保留一个休息电话 - 根据我的经验我会说,如果传输了大量数据或者如果涉及大量服务器计算或者您的应用程序有几千个用户,那么这种东西是值得的而且你真的希望优化服务器性能的最后一点,其他方面:只需再次获取数据或者将商店拆分为allProjects: Projects[] ,它不被重新获取和selectedProject: Project仅在未找到时获取在allProjects
  • 就文件命名而言,尽量避免使用大写字母并命名组件组件而不是指令
  • 由于您正在为商店使用ngrx ,您可能希望查看ngrx / effects作为从服务中调度操作的替代方法 - >这部分将是非常可选的,但在完美的 ngrx-app数据中 -服务甚至不知道有商店。

话虽这么说,这里有一些代码改进使它更多地面向面向ngrx的应用程序 - 但是我仍然建议你看一下非常好的官方ngrx-example-app

projects.component.ts

@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});
  }
}

projects.component.html

<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.component.ts

@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.service.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());
  }
}

project.effects.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});
}

projects.reducer.ts

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;
  }
};

正如您所看到的,这可能只是稍微多一点的代码,但只能在中心位置,代码可以轻松地重用 - 组件变得更加精简。

在一个理想的应用程序中,您还可以为ProjectsComponentProjectOneComponent添加一个额外的层,例如ProjectsRouteComponentSingleProjectRoute ,它们只包含这样的模板: <projectOne project="project$ | async"></projectOne>这将释放ProjectOneComponent来自商店或其他任何知识,它只包含一个简单的输入:

@Component({
  selector: 'projectOne',
  templateUrl: './projectOne.html'
})
export class ProjectOneComponent implements OnInit {
    @Input("project")
    project: Project;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM