张欢

管理Redux操作的新方法

原文链接: medium.com

我已经使用Redux有一年了,很喜欢用它来创建一个非常大的项目。Redux在管理整个应用程序状态方面确实有很大的帮助,具有良好的可伸缩性。然而,在创建越来越多的actions/reducers之后存在一个问题,action.js和reducres.js文件变得非常大,甚至我们已经将应用程序逻辑分隔成不同的特性。

请看下面的代码片段:

它甚至需要一个屏幕才能导入常量!对于actions.js和reducer.js都会发生这种情况。每当需要更改某些操作逻辑时,定位到action或者reducer中准确的代码需要花费很多时间,因为他们真的是TL;DR...为了解决这个问题,我们尝试将一个action放在一个文件中,并且将相应的reducer放进同一个文件中。效果很好,也解决了我们的痛点。我们称之为一个action一个文件模式。

事实上,它也是我们工具包中使用的方法: http://rekit.js.org

一个action和它相应的reducer一个文件

该方法将操作分成不同的文件: 将action的名称作为文件名并且仅仅包含一个action。 以一个简单的计数器组件为例:

它需要3中类型的actions: COUNTER_PLUS_ONE, COUNTER_MINUS_ONE, COUNTER_RESET。我们创建3个actions文件 (我们通常会为actions创建一个redux文件夹): counterPlusOne.js, counterMinusOne.js, counterReset.js。

当创建一个action时,通常立即需要创建一个reducer来处理更新store。在开发过程中,我们需要频繁的接触到这两个文件。切换文件需要很长的时间。所以我们也把reducer放到同一个文件中,这也有助于避免reducer文件过长。例如,thecounterPlusOne.js 文件包含以下代码:

import {  
    COUNTER_PLUS_ONE,
    } from './constants';

export function counterPlusOne() {  
    return {    
        type: COUNTER_PLUS_ONE,  
        };
    }

export function reducer(state, action) {  
    switch (action.type) {    
        case COUNTER_PLUS_ONE:      
        return {        
            ...state,        
            count: state.count + 1,      
            };

    default:      return state;
    }
}

事实上,一个reducer通常总是对应于某些action,并且它很少在全局范围内使用。因此将reducer放到同一个文件里去时合理的。它将应用程序逻辑分组到一个地方,使得开发更容易。对于异步的actions,可能需要两个或者更多的actions,因为它需要处理错误。

你可能已经注意到此处的reducer没有初始状态,因此它不是一个标准的Redux reducer。它仅仅由一个包装的reducer导入和调用。

包装reducer

正如前面所提到的,我们将应用程序逻辑划分为功能,每个功能都是一个文件夹。例如,论坛应用程序通常具有以下功能:用户、主题、注释等。每个功能都有一个包装的reducer,我们将每一个功能放在redux文件夹中,并将其命名为reducer.js。

包装的reducer负责加载被定义在不同actions中其他的reducers,并在接受新的action时逐个调用它们以生成新的state。被包装的reducer总是如下:

import initialState from './initialState';
import { reducer as counterPlusOne } from './counterPlusOne';
import { reducer as counterMinusOne } from './counterMinusOne';
import { reducer as counterReset } from './counterReset';

const reducers = [  counterPlusOne,  counterMinusOne,  counterReset,];

export default function reducer(state = initialState, action) {  
    let newState;
    switch (action.type) {    
    // Put global reducers here    
    default:      
        newState = state;
        break;
    }  
    return reducers.reduce((s, r) => r(s, action), newState);
}

从这些代码我们可以看到:包装的reducer本身也可以写reducers。它和actions中的reducers在store中的同一个分支上运行。这是标准reducer和被定义在一个action中主要的区别。当其它的reducers是纯函数时,你可以将包装的reducer看做是一个标准的Redux reducers。

处理cross-topic的actions

如上所述,并非每个action只有一个reducer。一些reducers可能需要处理相同的action。例如,当有新消息出现是,需要使用嵌入式聊天功能:

  1. 如果聊天框打开,显示;

  2. 如果没有,显示通知图标/消息。

这里 NEW_MESSAGE action需要由不同的UI组件处理。 因此,我们不应该通过应用程序逻辑或技术结构将不同的reducer放入某个action文件中。所以正确的做法是包装reducer。如上面的代码所示,一个包装的reducer本事是一个标准的Redux reducer,我们可以在其中放置任何switch案例,以便可以处理所有cross-topic的actions。

优点

这种方法有很多优点:

  1. 易于开发:创建actions时无需在文件之间跳转。

  2. 易于维护:actions文件很小,只需按照文件名即可找到。

  3. 易于测试:一个测试文件对应一个action,其中还包括action和reducer测试。

  4. 易于创建工具:在创建生成Redux样板代码工具是无需解析代码。一个文件模板就够了。

  5. 易于分析:静态分析可以轻松找到cross-topic actions。

创建代码生成器

无论是官方的方式还是创建 Reduc actions/reducers。我们都需要针对不同技术代码结构的模板代码。手动编写时非常复杂的,容易出错。因此,我们最好为我们创建生成此类代码的工具。现在“一个action一个文件”的模式可以更容易的创建这样的工具。例如,在创建一个正规的action时,我们可以使用以下模板。

import {  ${ACTION_TYPE},} from './constants';

export function ${CAMEL_ACTION_NAME}() {  
    return {    
        type: ${ACTION_TYPE},  
        };
    }
export function reducer(state, action) {  
    switch (action.type) {    
        case ${ACTION_TYPE}:      
            return {        ...state,      };

    default:      return state;
    }
}

我们需要做的就是需要根据action的类型和action的名字创建变量,然后通过模板生成代码。为了更好, 该工具在constants.js中自动创建一个action的类型,并自动导入包装的reducer文件中的reducer。由于它是一个非常固定的代码模式, 因此工具很容易做到这一点。实际上我们已经创建了一个工具Rekit 来创建这样的模板文件。

摘要

这种方法是武断的,但是它对我们的项目很有用。根据我们在React, Redux 和 React-router上的实践, 我们创建了一个名为 Rekit (http://rekit.js.org) 的工具包,至少对我们自己而言,这种方法是很有用的,希望对你也有帮助。