在 ngrx 效果中获取 json 数据的问题

problem with getting json data inside ngrx effect

过去 2 天我在尝试获取本地 json 数据 (posts) 以在视图 (PostsComponent) 中显示它时遇到了问题。 在控制台中我得到这个错误:

ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.

但我认为它不是一个对象,我正在尝试 return 它作为一个数组。

还有一件事,当我尝试记录结果时,它显示如下:{ posts: [] } 所以它是一个包含空数组的对象。

我在一个名为 BaseModule 的模块中。

这里是 post 的形状和 BaseModule 状态:

export interface BaseState {
  posts: Post[];
}

export class Post {
  id: number;
  title: string;
  content: string;
  imageSrc: string;
  author: {name: string, account: string, imageSrc: string};
  date: string;
  categories: string[];
}

我创建了 posts.actions.ts 文件:

import { createAction, props } from "@ngrx/store";
import { Post } from "src/app/types";

export const getAllPosts = createAction('[PostsComponent] Load Posts');
export const getAllPostsSuccessed = createAction(
  '[[PostsComponent] Load Posts Successed',
  props<{posts: Post[]}>()
);
export const getAllPostsError = createAction(
  '[PostsComponent] Load Posts Error',
  props<{error: any}>()
);

然后 posts.reducer.ts 文件:

import { createReducer, on } from "@ngrx/store";
import * as postsActions from "./posts.actions";
import { BaseState } from "src/app/types";

const initialState: BaseState = {
  posts: []
}

const _postsReducer = createReducer(
  initialState,
  on(postsActions.getAllPosts, state => state),
  on(postsActions.getAllPostsSuccessed, (state, { posts }) => {
    console.log(posts); return {...state, posts: posts};
  }),
  on(postsActions.getAllPostsError, (state, { error }) => { console.log(error); return state})
);

export function postsReducer(state: any, action: any){
  return _postsReducer(state, action);
}

和 posts.selectors.ts 文件:

import { createFeatureSelector, createSelector } from "@ngrx/store";
import { BaseState } from "src/app/types";

const selectBase = createFeatureSelector<BaseState>('base');

export const posts = createSelector(
  selectBase,
  (state: BaseState) => state.posts
);

最后 posts.effects.ts 文件:

import { Actions, createEffect, ofType } from "@ngrx/effects";
import { HttpClient } from "@angular/common/http";
import { catchError, map, mergeMap } from "rxjs/operators";
import { Post } from "src/app/types";
import { Injectable } from "@angular/core";
import { of } from "rxjs";

@Injectable()
export class PostsEffects {

  getPosts = createEffect(() => this.actions.pipe(
    ofType('[PostsComponent] Load Posts'),
    mergeMap(() => this.http.get<{posts: Post[]}>('assets/posts.json').pipe(
      map(posts => ({type: '[PostsComponent] Load Posts Successed', posts: posts})),
      catchError(error => of({type: '[PostsComponent] Load Posts Error', error: error}))
    ))
  ));

  constructor(
    private http: HttpClient,
    private actions: Actions
  ) { }

}

这是 posts.component.ts 文件:

import { Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { BaseState, Post } from "src/app/types";
import { getAllPosts } from "./state/posts.actions";
import { posts } from "./state/posts.selector";

@Component({
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.scss']
})
export class PostsComponent implements OnInit {

  posts: Post[];
  limit = 6;

  constructor(private store: Store<BaseState>) { }

  ngOnInit(): void {
    this.store.dispatch(getAllPosts());
    this.store.select(posts).subscribe(posts => this.posts = posts);
    console.log(this.posts);
  }

  get getPosts(){
    return this.posts;
  }

  showMore(){
    this.limit = this.limit + 6;
  }

}

和 posts.component.html 文件:

<div class="post" *ngFor="let post of getPosts">
  <div class="title"><a [routerLink]="'/post/' + post.id.toString()">{{ post.title }}</a></div>
  <div class="content">
    {{ post.content | slice:0:30 }}<a [routerLink]="'/post/' + post.id.toString()">...Read more</a>
  </div>
  <div class="image">
    <img [src]="post.imageSrc">
  </div>
  <div class="author" routerLink="#">
    <img [src]="post.author.imageSrc">
    {{ post.author.name }}
  </div>
  <div class="date">{{ post.date }}</div>
  <div class="categories">
    <div class="category" *ngFor="let category of post.categories">
      {{ category }}
    </div>
  </div>
</div>
<div class="show-more" (click)="showMore()">Show More</div>

我是 angular 和 ngrx 的新手,所以请让初学者清楚地回答。

除非你有充分的理由使用 ngrx,否则我建议放弃它并使用标准服务。与使用 ngrx 尝试做最基本的事情相比,您可能需要围绕事情做的少量实施要少得多,也容易得多。

例如您的问题有 7 段代码来进行简单的 post 检索。 这是事件的简化版本,使用了基本的内置功能。

组件:

export class SomeComponent implements OnInit, OnDestroy {
  private subs: Subscription[];

  private _posts: Post[];

  constructor(private readonly service: PostService) {
    this.subs = [];
  }

  public get posts(): Post[] { return this._posts; }

  public ngOnInit(): void {
    this.subs.push(this.service.posts$
      .subscribe((x: Post[]) => this.onPostsResponse(x)));

    this.service.getPosts();
  }

  public ngOnDestroy(): void {
    this.subs.forEach((x: Subscription) => x.unsubscribe());
  }

  private onPostsResponse(posts: Post[]): void {
    this._posts = posts;
  }
}

服务:

@Injectable()
export class PostService {
  private posts: Post[];

  private postsObs: Observer<Post[]>;

  private _posts$: Observable<Post[]>;

  constructor(private readonly http: HttpClient) {
    this._posts$ = new Observable<Post[]>((x: Observable<Post[]>) => {this.postsObs = x;}).pipe(share());
  }

  public get posts$(): Observable<Post[]> { return this._posts$; }

  public getPosts(): void {
    const sub = this.http.get('.../posts')
      .subscribe((response: Post[]) => {
        sub.unsubscribe();
        this.posts = response;
        this.emitPosts();
      });
  }

  private emitPosts(): void {
    if (this.postsObs) this.postsObs.next(this.posts);
  }
}

仅此而已,减去了您希望它具有的任何其他功能。我将 post 响应直接保存到 this.posts 变量,但我实际上并没有使用它,只是每次都覆盖它。

我看到你有更多的展示:它很容易扩展,这样你就可以包装你的 getPosts 方法,以便它 return 从变量中尽可能多地获取。一旦请求的 'more' post 超过了您缓存的 post 的数量,您就可以发出实际的 API 请求并 add 到 post,而不是直接覆盖它们,return 新的集合。

如果您将 post 请求保持在用户可以执行的任何操作之前一两页,则用户也不会有任何加载时间。当您在后台获取下一页时,他们将立即收到 post 的下一页。

直到你有一个 API 到位,你可以 return 任何你想要的虚拟数据(甚至可以将它放在 setTimeout 中以模拟一两秒的加载时间):

  public getPosts(): void {
    this.posts = dummyData;

    setTimeout(() => {this.emitPosts();}, 2000}; // 2s delay

    //const sub = this.http.get('.../posts')
    //  .subscribe((response: Post[]) => {
    //    sub.unsubscribe();
    //    this.posts = response;
    //    this.emitPosts();
    //  });
  }

  private dummyData: Post[] = [
    {...},
    {...},
    {...}
  ];

现在你有了一个可以让你的应用程序模块提供的服务(因此是一个应用程序范围的单例),它正在获取、存储和发出任何组件都可以订阅和接收数据的帖子(或本身)调用 getPosts - 就像存储模式和本质上 ngrx 所做的一样,但没有所有这些效果和缩减器以及可怕的复杂性。

问题是:

第一个:

动作文件中误加了一个字符“[”: '[[PostsComponent] Load Posts Successed'

第二个:

json 数据的类型有误,例如:{posts: [...]}