从ngrx/store获取单个项目

16

我编写了以下的Reducer以在我的Angular 2应用程序中存储状态项目。这些项目是金融工具的价格提供(例如股票/货币)。

我的Reducer实现如下:

export const offersStore = (state = new Array<Offer>(), action:Action) => {
switch(action.type){

    case "Insert":
        return [
            ...state, action.payload
        ];
    case "Update":
            return state.map(offer => {
                    if(offer.Instrument === action.payload.Instrument)
                    {
                        return Object.assign({}, action.payload);
                    }
                    else return offer;
            });
    case "Delete":
        return state.filter(offer => offer.Instrument !== action.payload )
    default:
        return state;
    }

我已经成功地让插入、更新和删除操作起作用了,虽然这并不容易。我觉得Redux是一种范式转变,与我多年来的编码方式有所不同。

我的应用程序上有一个乐器组件/页面,显示指定乐器的所有可用信息,由InstrumentId(例如“EUR/USD”)指示(存储在payload.Instrument属性中)。

我的问题是,我不确定如何高效地搜索特定的乐器并从存储中获取它。不仅如此,我还希望抓取的乐器在存储中更新时也会更新,因为它们经常通过websocket从服务器推送。因此,我真的需要搜索存储中的特定乐器,并将其作为Observable返回,该Observable将基于推送到存储中的新数据继续更新View组件。

我该如何实现这个目标?


2
当我开始使用redux/ngrxStore时,遇到了许多相同的问题。我从ngrx示例应用程序中获得了很大的灵感。https://github.com/ngrx/example-app 如果您查看它们如何组合其状态并检索值,它可以帮助您。在Redux中,鼓励您将状态规范化类似于数据库表。我的第二个障碍是学习如何有效地使用observables和rxjs。如果做得好,您可以真正简化代码并使其非常可读(假设您理解rxjs运算符),使您的应用程序非常快速和可测试。 - wiredprogrammer
1
这些迁移至使用ngrx/store的React/Redux示例可能也会对一些人有所帮助。 https://github.com/btroncone/ngrx-examples - wiredprogrammer
1
他们基本上是针对状态构建索引。 const newBookEntities = newBooks.reduce((entities: { [id: string]: Book }, book: Book) => { return Object.assign(entities, { [book.id]: book }); }, {}); - wiredprogrammer
1
然后使用该ID直接获取对象。state$.select(s => s.entities[id]); - wiredprogrammer
1
这有帮助吗? - wiredprogrammer
显示剩余3条评论
2个回答

14

对于调用reducer的每个操作,都会返回新状态。

从问题中的示例代码可以看出,state只是一个乐器列表。
没有索引,因此唯一的方法是搜索整个列表来检查乐器是否在列表中。

但是如果您的状态是一个字典呢?而且,如果您将索引列表与字典分开保存呢?

你的状态类型是这个:

export interface OfferState {
  ids: string[];
  entities: { [id: string]: IOffer };
};

每当执行一个操作时,都会返回新状态。这是 Redux 中的一个重要概念,因为状态不能直接被改变。当你组合你的 reducer 时,最好严格执行这个概念:比如说你有一个 "offers reducer" 和另一个 reducer,你可以使用 compose 把它们合并成一个:

> export default compose(storeFreeze, combineReducers) ({   oether:
> otherReducer,   offers: offersReducer });

在Redux中很容易做错事情,但是使用storeFreeze,如果您试图直接改变状态,它会抛出一个错误。关键在于操作会改变状态,并创建一个新的状态,而不是改变现有的状态,这让我们能够撤消/重做等操作。

以你上面的例子为例,我会将其用作我的Offer reducer:

export interface OfferState {
  ids: string[];
  entities: { [id: string]: IOffer };
};

export default function(state = initialState, action: Action): OfferState {
    switch(action.type){
        case OfferActions.INSERT:
        const offer : IOffer = action.payload;
        return {
            ids: [ ...state.ids, action.payload.Instrument ],
            entities: Object.assign({}, state.entities, { [action.payload.Instrument]: action.payload})
        };
        case OfferActions.UPDATE:
            return {
                ids: [...state.ids],
                entities: Object.assign({}, state.entities,  { [action.payload.Instrument]: action.payload})
            }

        default:
            return state;
    }
}

请注意,通过object.assign(深度复制)对临时状态进行更改,然后返回新状态。 另一个回答有点令人困惑。它详细介绍了如何组合不同的reducer,但对我来说没有多大意义。 在reducers/index.ts中应该有一种类型:
export interface AppState {
  otherReducer: OtherReducer;
  offers: fromOffersReducer.OfferState;
}

在index.ts文件中,您应该拥有获取reducers的函数:

export function getOfferState() {
  return (state$: Observable<AppState>) => state$
    .select(s => s.offers);
}

export function getOtherReducer() {
  return (state$ : Observable<AppState>) => state$
    .select(s => s.otherReducer)
}

在 offerReducer 和 otherReducer 中,我们定义了可以查询所需数据的函数。这些是匿名函数,目前尚未与任何内容链接,但稍后我们将把它们链接到 getReducerFunctions。

这些函数的示例:

export function getOfferEntities() {
  return (state$: Observable<OfferState>) => state$
    .select(s => s.entities);
};

export function getOffer(id: string) {
  return (state$: Observable<OfferState>) => state$
    .select(s => s.entities[id]);
}

这段代码本身并不会产生任何作用,直到我们将其应用到一些有用的数据上(例如 offersReducer),然后像这样将两者组合起来:

import offersReducer, * as fromOffersReducer from './OffersReducer';

 export function getOfferEntities() {
   return compose(fromOffersReducer.getOfferEntities(), getOfferState());
 }

  export function getOffer(instrument:string) {
   return compose(fromOffersReducer.getOffer(instrument), getOfferState());
 }

更详细一些吗?也许他能更好地了解你不理解的部分,但他重申了我已经写了很多的内容。 - wiredprogrammer
1
但是您已经回答了自己的问题。 - Ced
不,我绝对没有。这个问题上有赏金,我不能把答案授予自己……我不知道为什么会列出我回答了它。 - reach4thelasers
过去6个月中我所有的问题和回答都消失了。我真是太难过了。 - reach4thelasers
我认识一个在StackExchange工作的开发者。我已经在Twitter上私信过他了。我并不是很在意被分配我的答案,但我想要今年所有我的问题和答案。 - reach4thelasers
显示剩余2条评论

8
好的,我会尝试解释一种设置方法,希望能以一种你和其他人都能理解的方式来表达。如果您正在存储一个对象数组,并需要通过特定的ID或键访问行,则ngrx示例应用程序显示的方法是创建一个包含您的对象的对象,它将使用(在其书籍应用程序的情况下)书籍的ID作为属性键。您可以将任何字符串设置为属性名称。因此,假设您的状态具有名为“entities”的属性,其值为一个空对象。然后,您可以在“entities”对象上创建属性来处理数组。
export interface BooksState {
  entities: { [id: string]: Book };
};

让我们从书籍对象数组中的一行开始。要将此行添加到“entities”对象中,可以按照以下方式执行。
 reducerState.entities[book.id] = book;

以上操作将使得书籍的id成为“entities”对象上的一个属性。
如果您在控制台或调试工具中进行检查,它可能在创建后看起来像这样。
 reducerState.entities.15: { id: 15, name: "To Kill a Mockingbird" }

ngrx示例应用程序使用javascript数组上的reduce运算符对完整数组进行操作,将其添加到状态中。
   const newBookEntities 
              = newBooks.reduce(
                  (entities: { [id: string]: Book }, book: Book) => 
                           { 
                              return Object.assign(entities, { [book.id]: book }); 
                           }, {});

然后创建特殊函数来获取此状态的部分,稍后可以将它们组合在一起以获取所需的特定行。
export function getBooksState() {
  return (state$: Observable<AppState>) => state$
    .select(s => s.books);
}


export function getBookEntities() {
  return (state$: Observable<BooksState>) => state$
   .select(s => s.entities);
};
export function getBooks(bookIds: string[]) {
  return (state$: Observable<BooksState>) => state$
              .let(getBookEntities())
              .map(entities => bookIds.map(id => entities[id]));
   }

他们在示例中使用 index.ts 桶来组合这些内容。
import { compose } from '@ngrx/core/compose';


export function getBooks(bookIds: string[]) {
   return compose(fromBooks.getBooks(bookIds), getBooksState());
}

这个函数会将调用链接在一起,从右到左进行。首先调用getBooksState获取reducer状态,然后调用fromBooks.getBooks并传入你想要的bookid以及上一个“getBooksState”的状态,允许你对所需的选定状态进行映射。
在你的组件中,你可以导入这个函数并使用它来获取你想要的书籍。
myBooks$: Observable<Books>  =  this.store.let(getBooks(bookIds)));

这个返回的可观察对象会在每次存储更新时更新。 补充:因此,存储对象上的“let”运算符将当前可观察对象传递给由“getBooks”函数返回的函数,该可观察对象保存存储状态对象。
让我们更仔细地看一下。
export function getBooks(bookIds: string[]) {
   return compose(fromBooks.getBooks(bookIds), getBooksState());
}

在这里,将两个函数传递给compose函数——fromBooks.getBooks(bookIds)和getBooksState()。compose函数允许将值传递到右侧的第一个函数中,并将该函数的结果传递到左侧的函数中,以此类推。因此,在这种情况下,我们将获取“getBooksState”返回的函数的结果,并将其传递到由函数“fromBooks.getBooks(bookIds)”返回的函数中,最终返回该结果。
这两个函数都接受一个Observable。对于由getBooksState()返回的函数,它将作为参数接受存储状态observable对象,从中映射/选择(map和select是彼此的别名)到我们关心的存储片段并返回新的映射observable。该映射observable将被传递到由fromBooks.getBooks(bookIds)函数返回的函数中。该函数期望状态片段observable。该函数通过仅提取我们关心的实体来进行新的映射。这个新的observable就是“store.let”调用最终返回的内容。
如果您需要更多澄清,请提问,我会尽力澄清。

https://github.com/ngrx/example-app


这是TypeScript的类型声明。它仅表示entities对象应具有由字符串组成的属性名称,其值为book对象或符合book对象接口的值。在JavaScript中没有等价物,并且在TypeScript中编译时,它不会传递到JavaScript中。它只存在于对TypeScript代码进行linting以确保正确类型被传递(在这种情况下传递给函数)的过程中。 - wiredprogrammer
1
好的,我正在接近目标!有几个问题。在 this.store.let(getBooks()) 中 let 是什么意思?获取状态的特殊函数返回 observables。如何立即获取状态而不必等待更改发布到 Observable? - reach4thelasers
我在答案底部添加了进一步的解释。 - wiredprogrammer
对于 this.store.let(getBooks(bookIds)),'每当 store 更新时,这个返回的 observable 也会更新。' 我可以在我的 store 中实现这一点。但是,是否可能创建一个针对特定实体(通过其 id)进行更新的 observable? - Christian Benseler
如果我理解你的问题正确的话,那么你可能想尝试使用distinctUntilChanged rxjs操作符。https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/distinctuntilchanged.md - wiredprogrammer
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接