【问题标题】:How to create a factory of constants, action creators and reducers?如何创建一个由常量、动作创建者和减速器组成的工厂?
【发布时间】:2016-02-03 16:46:42
【问题描述】:

在我的应用程序中,我有很多模块将常量、动作创建者和归约器分组在同一个文件中,以减少文件碎片,它们看起来像这样:

    /**
     * Store
     * Products
     */

    import { Api } from './api';

    // Constants

    const REQUEST_PRODUCTS = 'REQUEST_PRODUCTS';
    const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS';

    // Action creators

    export function requestProducts() {
        return { type: REQUEST_PRODUCTS };
    }

    export function receiveProducts(json) {
        return {
            type: RECEIVE_PRODUCTS,
            entities: json.results,
            receivedAt: Date.now()
        };
    }

    function fetchProducts() { // Thunk
        return function (dispatch, getState) {
            dispatch(requestProducts());
            return Api.get('/products/')
                .then(json => dispatch(receiveProducts(json)))          
        }
    }

    // Reducer

    export function reducer(state = {
        isFetching: false,
        didInvalidate: false,
        entities: []
    }, action = '') {
        switch (action.type) {
            case REQUEST_PRODUCTS:
                return Object.assign({}, state, {
                    isFetching: true,
                    didInvalidate: false
                });
            case RECEIVE_PRODUCTS:
                return Object.assign({}, state, {
                    isFetching: false,
                    didInvalidate: false,
                    entities: action.entities,
                    lastUpdated: action.receivedAt
                });
            default:
                return state
        }
    }

在我的商店配置中,我然后像这样导入减速器:

    import { combineReducers } from 'redux';
    import { routeReducer } from 'redux-simple-router'

    const rootReducer = combineReducers({
        routing: routeReducer,
        products: require('../modules/products').reducer,
        blah: require('../modules/blah').reducer,
    });

    export default rootReducer;

我从组件中导入如下操作:

    import { fetchProducts } from '../modules/products';
    dispatch( fetchProducts() );

除了动作创建函数中的常量名称和实体名称外,所有“存储”文件都完全相同。正如诫命所说:不要重复自己,这是重构的理想候选者,因此我正在尝试创建一个可以用来消除重复并且仍然对每个“商店”进行一定程度的定制的工厂。但鉴于我有限的 Javascript 经验,特别是 ES6 模块语法,我很难想出一个像样的解决方案。

我可以尝试任何想法吗?

【问题讨论】:

  • 你读过 Redux real-world 的例子吗?它解决了很多这样的问题,但需要一段时间来解析他在做什么。
  • @Mike 是的,但是他使用“api 中间件”,我真的很难理解示例的语法,更不用说它是如何工作的了。我为我的应用选择了一条更简单的路线,但以重复结束。
  • 我是从 Harry (csswizardry) 的一次谈话中听到的:DRY
  • 是的,我也会不时记住这一点 :-) 谢谢!

标签: javascript reactjs redux


【解决方案1】:

减速机的成分是这里的关键。通过抽象代码的重复部分来创建reducer。我们返回一个类似于 products 等的 reducer 函数;但是,我们传递了一个key(字符串),以便在我们的状态的根部我们可以跟踪和更新productsblah等。如果您不熟悉传播操作符,它类似于做Object.assign({}, state, {state[key]: {updates}}); .

function entities (key) {
  return function entitiesByKey (state, action) {
    switch (action.type) {
      case REQUEST:
        return {
          ...state,
          state[key]: {
            isFetching: true
          }
        };
      case RECEIVE:
        return {
          ...state,
          state[key]: {
            entities
          }
        };
    }
  }
}

然后弄清楚如何将它与您当前的根减速器一起使用:

combineReducers({
  products: entities('products'),
  blah: entities('blah')
});

这应该会产生一个与此类似的状态树(一个不完整的示例):

{
  products: {
    isFetching: false,
    entities: {}
  },
  blah: {}
}

我根本没有测试过这段代码,但概念就在那里。

就实际示例和 API 中间件而言,它实际上非常简单。他正在通过具有 API 信息(url、常量等)的动作创建者传递一个对象 ([CALL_API])。当它到达中间件时,他提取[CALL_API] 对象并初始化文字API 调用(使用fetch()),然后根据响应正确地重新格式化操作对象。 [CALL_API] 看起来令人困惑,但这是使用 Symbol() 作为对象键的语法:

export const MY_SYMBOL = Symbol('My Symbol');

{
  [MY_SYMBOL]: {
    prop: "value"
  }
}

这是因为符号是唯一值,因此无法像字符串一样覆盖[CALL_API]

【讨论】:

  • 老实说,我不太确定我记得{...state, state[key]} 的哪个组合对我有用。我在某个时候改用{...state, entities[key]}。您将不得不使用它,但它确实有效。
  • 感谢您的解决方案!我最终使用了@Bergi 的变体,因为它更符合我想要做的事情,而且我已经完成了一半,但随着我的应用程序的增长,我会记住这种减速器组合技术! :)
【解决方案2】:

鉴于您有多个这样的东西,我建议将它们设为对象而不是模块。这只是让它们更容易以编程方式操作(比说模块代码),并且也更容易存储在数据结构中:

const REQUEST_PRODUCTS = 'REQUEST_PRODUCTS';
const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS';

export default {
    // Action creators
    requestProducts() {
        return { type: REQUEST_PRODUCTS };
    },
    receiveProducts(json) {
        return {
            type: RECEIVE_PRODUCTS,
            entities: json.results,
            receivedAt: Date.now()
        };
    },
    fetchProducts() { // Thunk
         return (dispatch, getState) => {
            dispatch(this.requestProducts());
            return Api.get('/'+type+'/')
                .then(json => dispatch(this.receiveProducts(json)))          
        }
    },

    // Reducer
    reducer(state = {
        isFetching: false,
        didInvalidate: false,
        entities: []
    }, action = '') {
        switch (action.type) {
            case REQUEST_PRODUCTS:
                return Object.assign({}, state, {
                    isFetching: true,
                    didInvalidate: false
                });
            case RECEIVE_PRODUCTS:
                return Object.assign({}, state, {
                    isFetching: false,
                    didInvalidate: false,
                    entities: action.entities,
                    lastUpdated: action.receivedAt
                });
            default:
                return state
        }
    }
};

嗯,这很简单。但是现在我们有了一个 thing - 一个 JavaScript 值,我们可以很容易地将它包装在一个返回 this 的函数中。

然后我们需要做的就是删除特定于实例的部分(在这种情况下,“产品”就足够了),并将它们替换为传递给函数的参数:

function moduleFactory(type) {
    const REQUEST = 'REQUEST_'+type.toUpperCase();
    const RECEIVE = 'RECEIVE_'+type.toUpperCase();

    return {
        // Action creators
        request() {
            return { type: REQUEST };
        },
        receive(json) {
            return {
                type: RECEIVE,
                entities: json.results,
                receivedAt: Date.now()
            };
        },
        fetch() { // Thunk
            return (dispatch, getState) => {
                dispatch(this.request());
                return Api.get('/'+type+'/')
                    .then(json => dispatch(this.receive(json)))          
            }
        },

        // Reducer
        reducer(state = {
            isFetching: false,
            didInvalidate: false,
            entities: []
        }, action = '') {
            switch (action.type) {
                case REQUEST:
                    return Object.assign({}, state, {
                        isFetching: true,
                        didInvalidate: false
                    });
                case RECEIVE:
                    return Object.assign({}, state, {
                        isFetching: false,
                        didInvalidate: false,
                        entities: action.entities,
                        lastUpdated: action.receivedAt
                    });
                default:
                    return state
            }
        }
    };
}

你就完成了!现在您当然可以优化它,例如通过在多个模块之间共享不依赖于类型的方法,但这并不重要。

import moduleFactory from '…';

const rootReducer = combineReducers({
    routing: routeReducer,
    products: moduleFactory('products').reducer,
    blah: moduleFactory('blah').reducer,
});

【讨论】:

  • 不幸的是,原来的例子有点做作。在实际的 Redux 应用程序中,这些功能大部分都存在于单独的模块中,因此将整个东西包装在一个对象中并不是一个好主意。
  • @Mike:好吧,如果所有这些模块看起来都彼此相似,那么它们不应该是单独的模块,这就是我想说的。相反,应该只有一个具有创建“模块对象”的工厂函数的模块。当然,根据每个模块需要的“定制”量,它可能仍会放在一个额外的文件中,但只在那里调用工厂函数。
  • 原始示例与我在实际应用程序中所做的非常相似,我试图将所有内容都保留在 this proposal 之后的同一模块中,这在当时对我来说是有意义的。跨度>
  • 谢谢@Bergi,我最终实现了类似的东西!我已经做了很长时间的 JS 开发,但我仍然觉得自己像个新手。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-21
  • 1970-01-01
  • 2018-09-19
相关资源
最近更新 更多