美文网首页reactWeb前端之路让前端飞
Redux进阶系列1:React+Redux项目结构最佳实践

Redux进阶系列1:React+Redux项目结构最佳实践

作者: 艾特老干部 | 来源:发表于2017-08-21 17:39 被阅读5254次

    React + Redux 是React生态中使用最频繁的技术栈,但关于如何组织React+Redux的项目结构,一直都有多种声音。本文将讨论其中最常用的3种项目结构,并给出个人的最佳实践。

    1. 按照类型

      这里的类型指的是一个文件在项目中充当的角色类型,即这个文件是一个component,还是一个container,或者是一个reducer等,充当component、container、action、reducer等不同角色的文件,分别放在不同的文件夹下,这也是Redux官网示例所采用的项目结构。这种结构如下所示:

      actions/
        a.js
        b.js
      components/
        a1.js
        a2.js
        b1.js
      constainers/
        a.js
        b.js
      reducers/
        a.js
        b.js
      index.js
      

      使用这种结构组织项目,每当增加一个新功能时,需要在containers和components文件夹下增加这个功能需要的组件,还需要在actions和reducers文件夹下,分别添加Redux管理这个功能使用到的action和reducer,如果action type是放在另外一个文件夹的话,还需要在这个文件夹下增加新的action type文件。所以,开发一个功能时,你需要频繁的切换路径,修改不同的文件。当项目逐渐变大时,这种项目结构是非常不方便的。

    2. 按照功能

      一个功能模块对应一个文件夹,这个功能所用到的container、component、action、reducer等文件,都存放在这个文件夹下。如下所示:

      feature1/
        components/
        actions.js
        container.js
        index.js
        reducer.js
      feature2/
        components/
        actions.js
        container.js
        index.js
        reducer.js
      index.js
      rootReducer.js
      

      这种项目结构的好处显而易见,一个功能中使用到的组件、状态和行为都在同一个文件夹下,方便开发,易于功能的扩展,Github上很多脚手架也选择了这种目录结构,如https://github.com/react-boilerplate/react-boilerplate。但这种结构也有一个问题,Redux会将整个应用的状态作为一个store来管理,不同的功能模块之间可以共享store中的部分状态(项目越复杂,这种场景就会越多),于是当你在feature1的container中dispatch一个action,很可能会影响feature2的状态,因为feature1和feature2共享了部分状态,会响应相同的action。这种情况下,不同模块间的功能被耦合到了一起。

    3. Ducks

      Ducks其实是对一种新的Redux项目结构的提议。它提倡将相关联的reducer、action types和action写到一个文件里。本质上是以应用的状态作为模块的划分依据,而不是以界面功能作为划分模块的依据。这样,管理相同状态的依赖都在同一个文件中,不管哪个容器组件需要使用这部分状态,只需要在这个组件中引入这个状态对应的文件即可。这样的一个文件(模块)如下:

      // widget.js
      
      // Actions
      const LOAD   = 'widget/LOAD';
      const CREATE = 'widget/CREATE';
      const UPDATE = 'widget/UPDATE';
      const REMOVE = 'widget/REMOVE';
      
      const initialState = {
        widget: null,
        isLoading: false,
      }
      
      // Reducer
      export default function reducer(state = initialState, action = {}) {
        switch (action.type) {
          LOAD: 
            //...
          CREATE:
            //...
          UPDATE:
            //...
          REMOVE:
            //...
          default: return state;
        }
      }
      
      // Action Creators
      export function loadWidget() {
        return { type: LOAD };
      }
      
      export function createWidget(widget) {
        return { type: CREATE, widget };
      }
      
      export function updateWidget(widget) {
        return { type: UPDATE, widget };
      }
      
      export function removeWidget(widget) {
        return { type: REMOVE, widget };
      }
      

      整体的目录结构如下:

      components/  (应用级别的通用组件)
      containers/  
        feature1/
          components/  (功能拆分出的专用组件)
          feature1.js  (容器组件)
          index.js     (feature1对外暴露的接口)
      redux/
        index.js (combineReducers)
        module1.js (reducer, action types, actions creators)
        module2.js (reducer, action types, actions creators)
      index.js
      
      

      在前两种项目结构中,当container需要使用actions时,可以通过import * as actions from 'path/to/actions.js'方式,一次性把一个action文件中的所有action creators都引入进来。但在使用Ducks结构时,action creators和reducer定义在同一个文件中,import *的导入方式会把reducer也导入进来(如果action types也被export,那么还会导入action types)。我们可以把action creators和action types定义到一个命名空间中,解决这个问题。修改如下:

      // widget.js
      
      // Actions
      export const types = {
        const LOAD   : 'widget/LOAD',
        const CREATE : 'widget/CREATE',
        const UPDATE : 'widget/UPDATE',
        const REMOVE : 'widget/REMOVE'
      }
      
      const initialState = {
        widget: null,
        isLoading: false,
      }
      
      // Reducer
      export default function reducer(state = initialState, action = {}) {
        switch (action.type) {
          types.LOAD: 
            //...
          types.CREATE:
            //...
          types.UPDATE:
            //...
          types.REMOVE:
            //...
          default: return state;
        }
      }
      
      // Action Creators
      export actions = {
        loadWidget: function() {
          return { type: types.LOAD };
        },
        createWidget: createWidget(widget) {
          return { type: types.CREATE, widget };
        },
        updateWidget: function(widget) {
          return { type: types.UPDATE, widget };
        },
        removeWidget: function(widget) {
          return { type: types.REMOVE, widget };
        }
      }
      

      这样,我们在container中使用actions时,可以通过import { actions } from 'path/to/module.js'引入,避免了引入额外的对象,也避免了import时把所有action都列出来的繁琐。

      现在的Ducks结构就是我项目中正在使用的项目结构,用起来还是很顺畅的,欢迎大家提出改进建议!


    欢迎关注我的公众号:老干部的大前端,领取21本大前端精选书籍!

    image

    相关文章

      网友评论

      • 42bddd8bb2c5:的确,我也觉得ducks是最方便的方式。方式2最不好,因为这样的设计,是将 redux 的 state 绑定在 ui 层面了,正如你的一篇博客上说的,state 最佳设计,应该是作为数据库来设计,所以方式2是最坏的。duck模式弥补了方式1的代码过于分散的缺点。说到redux 的state作为数据库设计,我觉得还有一个东西可以补充,就是用reselect,可以把它当做访问state的api来使用(它还能缓存)。这样写reducer后,提供相应的reselect函数,供给mapStateToProps使用,相当于后端给前端提供数据api了。
        艾特老干部:@陈凯_1b08 赞!reselect确实功能强大,selector也是建议和reducer、action放到一个文件中。但selector对一般使用者来说,相对陌生些,故而隐去了selector的概念。
        42bddd8bb2c5:我的使用体验是,reselect 可以隔离 state 的结构模型,在取state时,不需要关注state的结构问题,这部分职责交给 reselector 函数。这样前端开发时,可以有人专门设计 state,有人根据 state 写 selector,有人调用 selector 绑定数据到 ui。:clap:
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/i5lyrq 欢迎点赞支持!
        欢迎订阅《苍山沭河大前端》https://toutiao.io/subjects/11003

      本文标题:Redux进阶系列1:React+Redux项目结构最佳实践

      本文链接:https://www.haomeiwen.com/subject/kjlhdxtx.html