Redux书写习惯

react+redux项目已经是很常见了,
React已经有了成熟的书写规范:React规范-airbnb
但是redux书写规范目前比较少见,
这里分享一种我司 薪人薪事 的redux书写习惯。

redux的流程图


上图摘自阮一峰老师的博客,关键点 React Component / Actions(Action Creators,Action ) / Reducers。
我们需要对每个点都要坐下规范,具体规范包括

  1. 目录结构规范

  2. redux数据源规范

  3. redux相关文件名称规范

  4. action type变量名称规范

  5. action/action creator书写顺序,export顺序规范

  6. reducer书写规范

  7. 模块无状态组件规范

  8. 数据和业务分离规范

  9. 公共组件使用redux规范

  10. 搭配 eslint-react 使用

这里对每个点都做详细的规范介绍,最后展示完整的demo。

一、目录结构规范

项目使用的是碎片化目录结构,适合大型项目。
componets目录存放的公共组件
containers目录存放项目的公共容器
routers目录存放不同路由下的不同模块(一级路由区分模块,每个一级路由一个模块)
routers/List目录下零散的文件是对应的路由文件(chunk)
routers/List/components目录下是List模块的公共组件
routers/List/containers目录下是List模块的页面组件
routers/List/redux目录下是List模块的跟redux相关的文件(action、reducer等)
routers/List/style目录下是List模块的公共样式和各个页面的样式
routers/List/util目录下是List模块的公共无状态组件

上述是以List模块为例,其他各个模块均跟此模块类似。

二、redux数据源规范

通常情况下,每个页面都有自己的数据源,各个页面的数据源是平级的。
所以在react-router里配置的时候,当路由走到对应页面路由的时候,动态注入该数据源。

import { injectReducer } from '../../store/reducers'

export default (store) => ({
     path: 'user',getComponent (nextState,cb) {
            require.ensure([],(require) => {
                // 拿到reducer和store 动态注入节点
     const {infoReducer} = require('./redux/index').default;
     injectReducer(store,{ key: 'info',reducer: infoReducer });
    
     const Info = require('./containers/info').default;
    
     cb(null,Info);
     })
    }
})

上述代码的大致意思就是:当路由走到user页面的时候,到对应模块下的redux文件获取对应reducer,
创建一个obj,key是数据源的名称,reducer是写好的reducer,注入到全局的store中。
路由结构:

.
├── list
│   ├── list
│   └── detail 
└── user
    └── info

对应的redux的结构:

{
    list: {},detail: {},info: {}
}

各个页面在redux中的数据源是平铺的,这样各个数据源互不干涉,不影响。
每个页面的数据源单独维护。

之前还想过另一种方式,参考了一个vuex的多页应用设计。
就是一个模块一个数据源,每个数据源下对应页面数据源。
还是上面那个例子,这样的思想下redux数据结构就是:

{
    list: {
        list: {},},user: {
        info: {}
    }
}

根路由结构一样,这样的话,以模块为单位,页面数据源是模块下的一个属性
这样路由走到模块级路由的时候,注入reducer。
最后放弃了这种方案,原因很多,比如:

  1. 对于这种深层次的对象嵌套是不推荐的(两层级以上),这样很容易出现,改了list中的一个属性页面没有重绘的问题。

  2. 进入到一个页面,这时候每个页面的数据源已经初始化了,这样造成性能浪费和开发过程中产生一定的问题。

  3. 更新了detail中的一个属性,redux判断整个list改变,从而替换,触发很多不必要的重绘。性能浪费。

所以,还是应该使用节点平铺的方式。

三、redux相关文件名称规范

项目目录按照模块划分的,
redux文件都应该放到模块目录下的redux目录下。
该目录下包含了包含了该模块下的所有redux文件
总共应该有 actionTypes actions reducers index 四个文件
actionTypes 文件表示action的type,文件里都是常量;
actions 文件表示action和action creator;
reducers 文件表示模块下的所有reducer的一个集合;
index 文件只干一件事,import reducers 然后暴露出去,为的是遵循规范。

四、action type变量名称规范

在redux中,所有的action type是唯一的,全局不可重复。
所以,按理说整个项目应该有一个actionTypes文件,存储的是全局的action type。
但是考虑到这样的话 actionTypes文件内容会特别多,不便于维护。
所以,每个模块各自创建一个actionTypes文件,通过命名来避免重名问题。

写了一段时间,发现一个急于要解决的问题,就是action type的命名。
每个人的命名都按照自己的想法命名,不便于其他人阅读。
所以总结出action type的命名规则:
MODULE_PAGE_ACTION_OTHER
模块名_页面名_操作名_其他

前两个名字是为了避免变量重名,
ACTION表示具体操作名称
数据库有增删改查(CRUD)
但是redux的store基本不会存在增删查的情况,所以对改(U)做了细分:

  1. INIT 页面第一次进入获取数据的时候(这种情况通常会对很多数据进行填充,比较复杂,单独算作一种)

  2. UPDATE 更新某些数据

  3. RECOVER 某些页面卸载的时候 需要还原成初始化的数据

例如:

// 更新化列表数据
export const LIST_LIST_UPDATE_LIST = 'LIST_LIST_UPDATE_LIST';
// 初始化详情页数据
export const LIST_DETAIL_INIT = 'LIST_DETAIL_INIT';
// 还原详情页数据
export const LIST_DETAIL_RECOVER = 'LIST_DETAIL_RECOVER';

五、action/action creator书写顺序,export顺序规范

actions里面是整个模块的action和action creator。
这里面有很多情况,
比如里面有正常的 action:

const updateList = (data) => ({
    type: actionType.LIST_LIST_UPDATE_LIST,payload: data
});

还有发请求,请求数据的:

function fetchList() {
     return (dispatch,getState) => {
     // 这里使用axios发送请求
     // 此处可以通过getState()获取到整个store的数据
     // 发送请求前处理数据
    
     // return axios.get('/ajax/xxxxxxx')
     // .then(response => response.data)
     // .catch(response => response.data)
    
     // 模拟接口
     return new Promise(()=>{
         const array = [
             {id: 'a1',title: 'this is title',content: 'this is content'},{id: 'b2',title: 'this is b2 title',content: 'this is content,文章内容文章内容文章内容文章内容文章内容文章内容文章内容文章内容'},{id: 'c3',title: 'this is c3 title',content: 'this is 文章内容文章内容文章内容文章内容文章内容文章内容文章内容文章内容'},{id: 'd4',title: 'this is title d4',{id: 'e5',title: 'this is e5 title',content: 'this is content 文章内容文章内容文章内容文章内容文章内容文章内容文章内容文章内容'}
         ];

         dispatch(updateList(array));

         return array;
     });
}

所以需要区分

  1. 所以发送请求的都以fetch开头,名字与接口一致;

  2. action与命名规则将actionApplyOther,比如initDetail,updateList等;

  3. action文件暴露出去的时候,按照一定顺序排列;

export default {
    fetchList,getDetailById,initDetail,updateDetailStatus,recoverDetail
};

六、reducer书写规范

reducers文件包含该模块下的所有页面的reducer,
文件里可能有一些公用方法,写在最前面。
每个页面会有一个初始化页面的state和reducer。

detailState = {};

function detailReducer (state ={...detilState},action) {
    switch (action.type) {
        case actionTypes.LIST_DETAIL_INIT: {
            const { title,content } = action.payload;

            state.title = title;
            state.content = content;

            return { ...state };
        }
        case actionTypes.LIST_DETAIL_RECOVER: {
            state = detailState;

            return { ...state };
        }
        default:
            return state;
    }
};

七、模块无状态组件规范

每个模块都会有些可重用的HTML代码段,这些代码段里通常还有变量,
将这些变量提取出来,做成公共的无状态组件,提高代码复用率。
util目录下的文件就是模块下的无状态组件的集合。
这里无状态组件的命名规范:
get + 模块名 + 具体片段 + DOM

import React from 'react'
import { Link } from 'react-router'

export function getListListDOM({ list }) {
    const result = [];
    list.map((item) => {
        result.push(
            <li key={item.id}>
                <Link to={`/list/detail/${item.id}`}>{item.title}</Link>
            </li>
        );
    });

    return result;
}

页面使用的时候:

import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import actions from '../redux/actions'
import { getListListDOM } from '../util/'
import '../style/index.scss'
import '../style/list.scss'

class List extends React.Component {
    constructor (props) {
        super(props);
        this.state = {};
    }
    componentDidMount () {
        this.props.dispatch(actions.fetchList())
            .then((result) => {
                // 在业务层里进行报错提示等业务操作
                if (result) {
                    console.log('获取数据成功');
                }
            });
    }
    render () {
        const listDOM = getListListDOM(this.props.list);

        return (
            <div>
                <ul>
                    {listDOM}
                </ul>
            </div>
        );
    }
}

const mapStatetoProps = state => ({
    list: state.list,});

export default connect(mapStatetoProps,dispatch => ({ ...bindActionCreators(actions,dispatch),dispatch }))(List)

八、数据和业务分离规范

引入redux就是帮我们管理数据的,所以在redux的相关文件里面不要做view层能做的事
比如: 操作成功提示 报错提示 等等。
数据层里action creator 发送请求,action creator中负责简单的数据发送前处理,返回数据的简单处理,涉及到更改store都交给reducer
然后将请求返回结果return给view层 view层再做相关操作 。
例子可以见详细的demo。

九、公共组件使用redux规范

公共组件面临着在多个页面使用的场景,需要的参数虽然相同,但是可能来自不同的数据节点,
这样绑定数据节点的话不好区分,所以公共组件尽量使用父子组件参数传递。
如果该组件实在需要redux数据节点,为其建立单独的redux节点,和单独的reducer

十、搭配 eslint-react 使用

项目中,为了规范大家的代码,使用到了eslint,并且引入了针对react规范的包
该包分别针对react使用和jsx使用设定了规范,
我们阅读了所有规范,选出了适合我们的配置方案:

{
    "rules": {
    "comma-dangle": 0,"no-console": 0,"react/default-props-match-prop-types": 2,// 有认值的属性必须在propTypes中指定
    "react/no-array-index-key": 2,// 遍历出来的节点必须加key
    "react/no-children-prop": 2,// 禁止使用children作为prop
    "react/no-direct-mutation-state": 2,// 禁止直接this.state = 方式修改state 必须使用setState
    "react/no-multi-comp": 2,// 一个文件只能存在一个组件
    "react/no-set-state": 2,// 不必要的组件改写成无状态组件
    "react/no-string-refs": 2,// 禁止字符串的ref
    "react/no-unescaped-entities": 2,// 禁止'<','>'等单标签
    "react/no-unkNown-property": 2,// 禁止未知的DOM属性
    "react/no-unused-prop-types": 2,// 禁止未使用的prop参数
    "react/prefer-es6-class": 2,// 强制使用es6 extend方法创建组件
    "react/require-default-props": 2,// 非require的propTypes必须制定认值
    "react/self-closing-comp": 2,// 没有children的组件和html必须使用自闭和标签
    "react/sort-comp": 2,// 对组件的方法排序
    "react/sort-prop-types": 2,// 对prop排序
    "react/style-prop-object": 2,// 组件参数如果是style,value必须是object

    "react/jsx-boolean-value": 2,// 属性值为true的时候,省略值只写属性名
    "react/jsx-closing-bracket-location": 2,// 强制闭合标签的位置
    "react/jsx-closing-tag-location": 2,// 强制开始标签闭合标签位置
    "react/jsx-equals-spacing": 2,// 属性赋值不允许有空格
    "react/jsx-first-prop-new-line": 2,// 只有一个属性情况下单行
    "react/jsx-key": 2,// 强制遍历出来的jsx加key
    "react/jsx-max-props-per-line": [2,{ "maximum": 2 }],// 每行最多几个属性
    "react/jsx-no-comment-textnodes": 2,// 检查jsx注释
    "react/jsx-no-duplicate-props": 2,// 检查属性名重名
    "react/jsx-no-target-blank": 2,// 检查jsx是否被引入和使用
    "react/jsx-no-undef": 2,// 检查jsx引用规范
    "react/jsx-pascal-case": 2,// 检查jsx标签名规范
  }
}

总结

其实redux引入相当于是前端引入了一个数据库,全局可以使用,但是不可持久化。
同时也引入分层概念,与后端框架操作数据库类似,
不过redux数据库还是有本质区别的:后端是存取数据关系,前端是数据和组件相互订阅关系。
写多了就会总结出一套固定的写法,互相学习,参考。

完整的项目:https://github.com/Aus0049/re...

相关文章

react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接...
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc ...