如何创建常量工厂、action creators 和 reducers?

How to create a factory of constants, action creators and reducers?

在我的应用程序中,我有很多模块将常量、action creators 和 reducers 分组在同一个文件中以减少文件碎片,它们看起来像这样:

    /**
     * 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() );

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

我有什么想法可以尝试吗?

Reducer 组合是这里的关键。通过抽象代码的重复部分来创建一个 reducer。我们 return 一个类似于已经用于 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]

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

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 值,我们可以很容易地将它包装在一个 returns this.

的函数中

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

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