ngRx 官方示例分析 - 3. reducers

上一篇:ngRx 官方示例分析 - 2. Action 管理

这里我们讨论 reducer.

如果你注意的化,会看到再不同的 Action 定义文件中,导出的 String Literal Type 名称都是 Actions ,在导入的时候,同时导入同名的类型就是问题了。这里首先使用了 import as 语法进行重命名。

import * as book from ‘../actions/book‘;
import * as collection from ‘../actions/collection‘;

这样我们就可以区分来自不同定义文件的 Actions 了。

对于每个 reducer  来说,状态虽然一直在变,但是所管理的状态的基本的结构是不变的,我们通过接口进行约束。

比如 Book,我们定义如下状态的接口。注意这个接口的名称没有 Book,是统一的 State,在导入这个定义的时候,使用  import as 语法进行重命名。

export interface State {
  ids: string[];
  entities: { [id: string]: Book };
  selectedBookId: string | null;
};

Book 的初始状态当然需要实现这个接口的。

export const initialState: State = {
  ids: [],
  entities: {},
  selectedBookId: null,
};

辅助函数

在 Reducer 中提供了辅助函数,以便于数据访问。

/**
 * Because the data structure is defined within the reducer it is optimal to
 * locate our selector functions at this level. If store is to be thought of
 * as a database, and reducers the tables, selectors can be considered the
 * queries into said database. Remember to keep your selectors small and
 * focused so they can be combined and composed to fit each particular
 * use-case.
 */

export const getEntities = (state: State) => state.entities;

export const getIds = (state: State) => state.ids;

export const getSelectedId = (state: State) => state.selectedBookId;

export const getSelected = createSelector(getEntities, getSelectedId, (entities, selectedId) => {
  return entities[selectedId];
});

export const getAll = createSelector(getEntities, getIds, (entities, ids) => {
  return ids.map(id => entities[id]);
});

这个 createSelector 函数来自 reselect

Simple “selector” library for Redux inspired by getters in NuclearJSsubscriptions in re-frame and this proposal from speedskater.

  • Selectors can compute derived data, allowing Redux to store the minimal possible state.
  • Selectors are efficient. A selector is not recomputed unless one of its arguments change.
  • Selectors are composable. They can be used as input to other selectors.

源代码

下面是 Book 的 reducer 的全部代码。/app/reducers/books.ts

import { createSelector } from ‘reselect‘;
import { Book } from ‘../models/book‘;
import * as book from ‘../actions/book‘;
import * as collection from ‘../actions/collection‘;

export interface State {
  ids: string[];
  entities: { [id: string]: Book };
  selectedBookId: string | null;
};

export const initialState: State = {
  ids: [],
  entities: {},
  selectedBookId: null,
};

export function reducer(state = initialState, action: book.Actions | collection.Actions): State {
  switch (action.type) {
    case book.ActionTypes.SEARCH_COMPLETE:
    case collection.ActionTypes.LOAD_SUCCESS: {
      const books = action.payload;
      const newBooks = books.filter(book => !state.entities[book.id]);

      const newBookIds = newBooks.map(book => book.id);
      const newBookEntities = newBooks.reduce((entities: { [id: string]: Book }, book: Book) => {
        return Object.assign(entities, {
          [book.id]: book
        });
      }, {});

      return {
        ids: [ ...state.ids, ...newBookIds ],
        entities: Object.assign({}, state.entities, newBookEntities),
        selectedBookId: state.selectedBookId
      };
    }

    case book.ActionTypes.LOAD: {
      const book = action.payload;

      if (state.ids.indexOf(book.id) > -1) {
        return state;
      }

      return {
        ids: [ ...state.ids, book.id ],
        entities: Object.assign({}, state.entities, {
          [book.id]: book
        }),
        selectedBookId: state.selectedBookId
      };
    }

    case book.ActionTypes.SELECT: {
      return {
        ids: state.ids,
        entities: state.entities,
        selectedBookId: action.payload
      };
    }

    default: {
      return state;
    }
  }
}

/**
 * Because the data structure is defined within the reducer it is optimal to
 * locate our selector functions at this level. If store is to be thought of
 * as a database, and reducers the tables, selectors can be considered the
 * queries into said database. Remember to keep your selectors small and
 * focused so they can be combined and composed to fit each particular
 * use-case.
 */

export const getEntities = (state: State) => state.entities;

export const getIds = (state: State) => state.ids;

export const getSelectedId = (state: State) => state.selectedBookId;

export const getSelected = createSelector(getEntities, getSelectedId, (entities, selectedId) => {
  return entities[selectedId];
});

export const getAll = createSelector(getEntities, getIds, (entities, ids) => {
  return ids.map(id => entities[id]);
});

/app/reducers/collection.ts

import * as collection from ‘../actions/collection‘;

export interface State {
  loaded: boolean;
  loading: boolean;
  ids: string[];
};

const initialState: State = {
  loaded: false,
  loading: false,
  ids: []
};

export function reducer(state = initialState, action: collection.Actions): State {
  switch (action.type) {
    case collection.ActionTypes.LOAD: {
      return Object.assign({}, state, {
        loading: true
      });
    }

    case collection.ActionTypes.LOAD_SUCCESS: {
      const books = action.payload;

      return {
        loaded: true,
        loading: false,
        ids: books.map(book => book.id)
      };
    }

    case collection.ActionTypes.ADD_BOOK_SUCCESS:
    case collection.ActionTypes.REMOVE_BOOK_FAIL: {
      const book = action.payload;

      if (state.ids.indexOf(book.id) > -1) {
        return state;
      }

      return Object.assign({}, state, {
        ids: [ ...state.ids, book.id ]
      });
    }

    case collection.ActionTypes.REMOVE_BOOK_SUCCESS:
    case collection.ActionTypes.ADD_BOOK_FAIL: {
      const book = action.payload;

      return Object.assign({}, state, {
        ids: state.ids.filter(id => id !== book.id)
      });
    }

    default: {
      return state;
    }
  }
}

export const getLoaded = (state: State) => state.loaded;

export const getLoading = (state: State) => state.loading;

export const getIds = (state: State) => state.ids;

/app/reducers/layout.ts

import * as layout from ‘../actions/layout‘;

export interface State {
  showSidenav: boolean;
}

const initialState: State = {
  showSidenav: false,
};

export function reducer(state = initialState, action: layout.Actions): State {
  switch (action.type) {
    case layout.ActionTypes.CLOSE_SIDENAV:
      return {
        showSidenav: false
      };

    case layout.ActionTypes.OPEN_SIDENAV:
      return {
        showSidenav: true
      };

    default:
      return state;
  }
}

export const getShowSidenav = (state: State) => state.showSidenav;

/app/reducers/search.ts

import * as book from ‘../actions/book‘;

export interface State {
  ids: string[];
  loading: boolean;
  query: string;
};

const initialState: State = {
  ids: [],
  loading: false,
  query: ‘‘
};

export function reducer(state = initialState, action: book.Actions): State {
  switch (action.type) {
    case book.ActionTypes.SEARCH: {
      const query = action.payload;

      if (query === ‘‘) {
        return {
          ids: [],
          loading: false,
          query
        };
      }

      return Object.assign({}, state, {
        query,
        loading: true
      });
    }

    case book.ActionTypes.SEARCH_COMPLETE: {
      const books = action.payload;

      return {
        ids: books.map(book => book.id),
        loading: false,
        query: state.query
      };
    }

    default: {
      return state;
    }
  }
}

export const getIds = (state: State) => state.ids;

export const getQuery = (state: State) => state.query;

export const getLoading = (state: State) => state.loading;

管理 Recuders

组合器

compose 函数用来将一组函数组合成单个单个函数。

/**
 * The compose function is one of our most handy tools. In basic terms, you give
 * it any number of functions and it returns a function. This new function
 * takes a value and chains it through every composed function, returning
 * the output.
 *
 * More: https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch5.html*/
import { compose } from ‘@ngrx/core/compose‘;

ngrx-store-freeze

@ngrx/store meta reducer that prevents state from being mutated. When mutation occurs, an exception will be thrown. This is useful during development mode to ensure that no part of the app accidentally mutates the state. Ported from redux-freeze

/**
 * storeFreeze prevents state from being mutated. When mutation occurs, an
 * exception will be thrown. This is useful during development mode to
 * ensure that none of the reducers accidentally mutates the state.
 */
import { storeFreeze } from ‘ngrx-store-freeze‘;

combineReducers

随着应用变得复杂,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分。

combineReducers 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore

合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定

通过为传入对象的 reducer 命名不同来控制 state key 的命名。例如,你可以调用 combineReducers({todos: myTodosReducer, counter: myCounterReducer }) 将 state 结构变为 { todos, counter }

通常的做法是命名 reducer,然后 state 再去分割那些信息,因此你可以使用 ES6 的简写方法:combineReducers({ counter, todos })。这与 combineReducers({ counter: counter, todos: todos })一样。

http://www.redux.org.cn/docs/api/combineReducers.html

/**
 * combineReducers is another useful metareducer that takes a map of reducer
 * functions and creates a new reducer that stores the gathers the values
 * of each reducer and stores them using the reducer‘s key. Think of it
 * almost like a database, where every reducer is a table in the db.
 *
 * More: https://egghead.io/lessons/javascript-redux-implementing-combinereducers-from-scratch*/
import { combineReducers } from ‘@ngrx/store‘;

组合 Recuders

这是函数式编程中的方法,为了方便,被放到了 Redux 里。 当需要把多个 store 增强器 依次执行的时候,需要用到它。

http://www.redux.org.cn/docs/api/compose.html

将各个状态和 reducer 组合为单个的 reducer。

/**
 * As mentioned, we treat each reducer like a table in a database. This means
 * our top level state interface is just a map of keys to inner state types.
 */
export interface State {
  search: fromSearch.State;
  books: fromBooks.State;
  collection: fromCollection.State;
  layout: fromLayout.State;
  router: fromRouter.RouterState;
}

/**
 * Because metareducers take a reducer function and return a new reducer,
 * we can use our compose helper to chain them together. Here we are
 * using combineReducers to make our top level reducer, and then
 * wrapping that in storeLogger. Remember that compose applies
 * the result from right to left.
 */
const reducers = {
  search: fromSearch.reducer,
  books: fromBooks.reducer,
  collection: fromCollection.reducer,
  layout: fromLayout.reducer,
  router: fromRouter.routerReducer,
};

const developmentReducer: ActionReducer<State> = compose(storeFreeze, combineReducers)(reducers);
const productionReducer: ActionReducer<State> = combineReducers(reducers);

在 compose 的时候,已经传递了当前的 reducers .

最后导出了 reducer 函数

export function reducer(state: any, action: any) {
  if (environment.production) {
    return productionReducer(state, action);
  } else {
    return developmentReducer(state, action);
  }
}

注册 reducer

在 /app/app.module.ts 中,注册到 store 中。注意 ‘./reducers‘ 实际上导入的是 /app/reducers/index.ts。

import { reducer } from ‘./reducers‘;

    /**
     * StoreModule.provideStore is imported once in the root module, accepting a reducer
     * function or object map of reducer functions. If passed an object of
     * reducers, combineReducers will be run creating your application
     * meta-reducer. This returns all providers for an @ngrx/store
     * based application.
     */
    StoreModule.provideStore(reducer),

一些辅助函数

获取当前图书列表的辅助函数。

export const getBooksState = (state: State) => state.books;

源码

在 /app/reducers/index.ts 中,

import { createSelector } from ‘reselect‘;
import { ActionReducer } from ‘@ngrx/store‘;
import * as fromRouter from ‘@ngrx/router-store‘;
import { environment } from ‘../../environments/environment‘;

/**
 * The compose function is one of our most handy tools. In basic terms, you give
 * it any number of functions and it returns a function. This new function
 * takes a value and chains it through every composed function, returning
 * the output.
 *
 * More: https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch5.html
 */
import { compose } from ‘@ngrx/core/compose‘;

/**
 * storeFreeze prevents state from being mutated. When mutation occurs, an
 * exception will be thrown. This is useful during development mode to
 * ensure that none of the reducers accidentally mutates the state.
 */
import { storeFreeze } from ‘ngrx-store-freeze‘;

/**
 * combineReducers is another useful metareducer that takes a map of reducer
 * functions and creates a new reducer that stores the gathers the values
 * of each reducer and stores them using the reducer‘s key. Think of it
 * almost like a database, where every reducer is a table in the db.
 *
 * More: https://egghead.io/lessons/javascript-redux-implementing-combinereducers-from-scratch
 */
import { combineReducers } from ‘@ngrx/store‘;

/**
 * Every reducer module‘s default export is the reducer function itself. In
 * addition, each module should export a type or interface that describes
 * the state of the reducer plus any selector functions. The `* as`
 * notation packages up all of the exports into a single object.
 */
import * as fromSearch from ‘./search‘;
import * as fromBooks from ‘./books‘;
import * as fromCollection from ‘./collection‘;
import * as fromLayout from ‘./layout‘;

/**
 * As mentioned, we treat each reducer like a table in a database. This means
 * our top level state interface is just a map of keys to inner state types.
 */
export interface State {
  search: fromSearch.State;
  books: fromBooks.State;
  collection: fromCollection.State;
  layout: fromLayout.State;
  router: fromRouter.RouterState;
}

/**
 * Because metareducers take a reducer function and return a new reducer,
 * we can use our compose helper to chain them together. Here we are
 * using combineReducers to make our top level reducer, and then
 * wrapping that in storeLogger. Remember that compose applies
 * the result from right to left.
 */
const reducers = {
  search: fromSearch.reducer,
  books: fromBooks.reducer,
  collection: fromCollection.reducer,
  layout: fromLayout.reducer,
  router: fromRouter.routerReducer,
};

const developmentReducer: ActionReducer<State> = compose(storeFreeze, combineReducers)(reducers);
const productionReducer: ActionReducer<State> = combineReducers(reducers);

export function reducer(state: any, action: any) {
  if (environment.production) {
    return productionReducer(state, action);
  } else {
    return developmentReducer(state, action);
  }
}

/**
 * A selector function is a map function factory. We pass it parameters and it
 * returns a function that maps from the larger state tree into a smaller
 * piece of state. This selector simply selects the `books` state.
 *
 * Selectors are used with the `select` operator.
 *
 * ```ts
 * class MyComponent {
 *     constructor(state$: Observable<State>) {
 *       this.booksState$ = state$.select(getBooksState);
 *     }
 * }
 * ```
 */
export const getBooksState = (state: State) => state.books;

/**
 * Every reducer module exports selector functions, however child reducers
 * have no knowledge of the overall state tree. To make them useable, we
 * need to make new selectors that wrap them.
 *
 * The createSelector function from the reselect library creates
 * very efficient selectors that are memoized and only recompute when arguments change.
 * The created selectors can also be composed together to select different
 * pieces of state.
 */
 export const getBookEntities = createSelector(getBooksState, fromBooks.getEntities);
 export const getBookIds = createSelector(getBooksState, fromBooks.getIds);
 export const getSelectedBookId = createSelector(getBooksState, fromBooks.getSelectedId);
 export const getSelectedBook = createSelector(getBooksState, fromBooks.getSelected);

/**
 * Just like with the books selectors, we also have to compose the search
 * reducer‘s and collection reducer‘s selectors.
 */
export const getSearchState = (state: State) => state.search;

export const getSearchBookIds = createSelector(getSearchState, fromSearch.getIds);
export const getSearchQuery = createSelector(getSearchState, fromSearch.getQuery);
export const getSearchLoading = createSelector(getSearchState, fromSearch.getLoading);

/**
 * Some selector functions create joins across parts of state. This selector
 * composes the search result IDs to return an array of books in the store.
 */
export const getSearchResults = createSelector(getBookEntities, getSearchBookIds, (books, searchIds) => {
  return searchIds.map(id => books[id]);
});

export const getCollectionState = (state: State) => state.collection;

export const getCollectionLoaded = createSelector(getCollectionState, fromCollection.getLoaded);
export const getCollectionLoading = createSelector(getCollectionState, fromCollection.getLoading);
export const getCollectionBookIds = createSelector(getCollectionState, fromCollection.getIds);

export const getBookCollection = createSelector(getBookEntities, getCollectionBookIds, (entities, ids) => {
  return ids.map(id => entities[id]);
});

export const isSelectedBookInCollection = createSelector(getCollectionBookIds, getSelectedBookId, (ids, selected) => {
  return ids.indexOf(selected) > -1;
});

/**
 * Layout Reducers
 */
export const getLayoutState = (state: State) => state.layout;

export const getShowSidenav = createSelector(getLayoutState, fromLayout.getShowSidenav);

总结

通过 Reducer 建立对 store 状态的维护,示例中还提供了一些辅助函数来帮助简化数据访问。

参考资源:

时间: 2024-12-14 18:08:20

ngRx 官方示例分析 - 3. reducers的相关文章

ngRx 官方示例分析 - 5. components

组件通过标准的 Input 和 Output 进行操作,并不直接访问 store. /app/components/book-authors.ts import { Component, Input } from '@angular/core'; import { Book } from '../models/book'; @Component({ selector: 'bc-book-authors', template: ` <h5 md-subheader>Written By:<

RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想

RocketMQ4.3.0版本开始支持事务消息,后续分享将开始将剖析事务消息的实现原理.首先从官方给出的Demo实例入手,以此通往RocketMQ事务消息的世界中. 官方版本未发布之前,从apache rocketmq第一个版本上线后,代码中存在与事务消息相关的代码,例如COMMIT.ROLLBACK.PREPARED,在事务消息未开源之前网上对于事务消息的"声音"基本上是使用类似二阶段提交,主要是根据消息系统标志MessageSysFlag中定义来推测的: TRANSACTION_P

DotNetBar for Windows Forms 12.7.0.10_冰河之刃重打包版原创发布-带官方示例程序版

关于 DotNetBar for Windows Forms 12.7.0.10_冰河之刃重打包版 --------------------11.8.0.8_冰河之刃重打包版---------------------------------------------------------基于 官方原版的安装包 + http://www.cnblogs.com/tracky 提供的补丁DLL制作而成.安装之后,直接就可以用了.省心省事.不必再单独的打一次补丁包了.本安装包和补丁包一样都删除了官方自

DotNetBar for Windows Forms 12.5.0.2_冰河之刃重打包版原创发布-带官方示例程序版

关于 DotNetBar for Windows Forms 12.5.0.2_冰河之刃重打包版 --------------------11.8.0.8_冰河之刃重打包版--------------------------------------------------------- 基于 官方原版的安装包 + http://www.cnblogs.com/tracky 提供的补丁DLL制作而成. 安装之后,直接就可以用了. 省心省事.不必再单独的打一次补丁包了. 本安装包和补丁包一样都删除了

DotNetBar for Windows Forms 12.2.0.7_冰河之刃重打包版原创发布-带官方示例程序版

关于 DotNetBar for Windows Forms 12.2.0.7_冰河之刃重打包版 --------------------11.8.0.8_冰河之刃重打包版---------------------------------------------------------基于 官方原版的安装包 + http://www.cnblogs.com/tracky 提供的补丁DLL制作而成.安装之后,直接就可以用了.省心省事.不必再单独的打一次补丁包了.本安装包和补丁包一样都删除了官方自带

Unity Surface Shader 示例分析

对于Unity中的表面着色器(Surface Shader),它的代码整体结构如下所示: Shader "name" { Properties { // 第一部分 } SubShader { // 第二部分 } Fallback "Diffuse" // 第三部分 } 第一部分 Properties 数据块 它的作用是充当数据的接口,将外部的数据(资源)引入进来,以供着色器内部使用.在这里,我们可以定义的数据类型如下所示: (1) _MainTex ( "

三层浅析及示例分析

什么是三层结构?所谓三层结构,不是物理上的三层划分,也不是简单的模块划分,而是逻辑上的三层,是在客户端和数据库访问之间加入了一个中间层,形成逻辑三层结构. 三层都是哪三层?它们的作用是什么?三层结构包含:表示层UI,业务逻辑层BLL,数据访问层DAL.1 显示层,就是软件的显示部分,主要是客户端,通常表现为WEB或窗体.主要功能:接受用户输入信息.显示系统输出信息.为用户提供一个交互界面. 2 业务逻辑层,系统主要功能部分,主要处理软件的业务逻辑,处理数据. 3 数据访问层,用于对数据库的操作,

水晶报表官方示例

原文:水晶报表官方示例 使用 C# 和 C++.NET 开发的 .NET 应用程序实例列表---------------------------------- 概述 本文档列出了 Crystal Decisions 技术支持网站上所有可用的,使用 C# 和 C++.NET 开发的 .NET 应用程序实例列表.本文档还给出了每一个程序的描述和下载链接.随着新程序加入我们的支持站点,本文档将不断更新.---------------------------------- 目录 VISUAL C# .N

html5游戏引擎phaser官方示例学习

首发:个人博客,更新&纠错&回复 phaser官方示例学习进行中,把官方示例调整为简明的目录结构,学习过程中加了点中文注释,代码在这里. 目前把官方的完整游戏示例看了一大半, breakout是敲砖块,gemmatch是钻石消除,invaders是小蜜蜂,matching是配对,simon是记忆游戏,sliding是拼图,starstruck类似超级马里奥,tanks是坦克游戏. 游戏场面上看,敲砖块.小蜜蜂是竖版,超级马里奥是横版,坦克游戏是俯瞰,钻石.配对.记忆.拼图这四个都是棋盘.