react+redux项目已经是很常见了,
React已经有了成熟的书写规范:React规范-airbnb
但是redux书写规范目前比较少见,
这里分享一种我司 薪人薪事 的redux书写习惯。
redux的流程图
上图摘自阮一峰老师的博客,关键点 React Component / Actions(Action Creators, Action ) / Reducers。
我们需要对每个点都要坐下规范,具体规范包括:
目录结构规范
redux数据源规范
redux相关文件名称规范
action type变量名称规范
action/action creator书写顺序,export顺序规范
reducer书写规范
模块无状态组件规范
数据和业务分离规范
公共组件使用redux规范
搭配 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: {},
detail: {},
},
user: {
info: {}
}
}
根路由结构一样,这样的话,以模块为单位,页面数据源是模块下的一个属性。
这样路由走到模块级路由的时候,注入reducer。
最后放弃了这种方案,原因很多,比如:
对于这种深层次的对象嵌套是不推荐的(两层级以上),这样很容易出现,改了list中的一个属性,页面没有重绘的问题。
进入到一个页面,这时候每个页面的数据源已经初始化了,这样造成性能浪费和开发过程中产生一定的问题。
更新了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)做了细分:
INIT
页面第一次进入获取数据的时候(这种情况通常会对很多数据进行填充,比较复杂,单独算作一种)UPDATE
更新某些数据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', content: 'this is 文章内容文章内容文章内容文章内容文章内容文章内容文章内容文章内容'},
{id: 'e5', title: 'this is e5 title', content: 'this is content 文章内容文章内容文章内容文章内容文章内容文章内容文章内容文章内容'}
];
dispatch(updateList(array));
return array;
});
}
所以需要区分
所以发送请求的都以fetch开头,名字与接口一致;
action与命名规则将actionApplyOther,比如initDetail,updateList等;
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于数据库还是有本质区别的:后端是存取数据关系,前端是数据和组件相互订阅关系。
写多了就会总结出一套固定的写法,互相学习,参考。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。