react+redux项目已是很常见了,
React已经有了成熟的书写规范:React规范-airbnb
可是redux书写规范目前比较少见,
这里分享一种我司 薪人薪事 的redux书写习惯。javascript
上图摘自阮一峰老师的博客,关键点 React Component / Actions(Action Creators, Action ) / Reducers。
咱们须要对每一个点都要坐下规范,具体规范包括:css
目录结构规范html
redux数据源规范前端
redux相关文件名称规范vue
action type变量名称规范java
action/action creator书写顺序,export顺序规范node
reducer书写规范react
模块无状态组件规范ios
数据和业务分离规范git
公共组件使用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
模块为例,其余各个模块均跟此模块相似。
一般状况下,每一个页面都有本身的数据源,各个页面的数据源是平级的。
因此在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文件。
总共应该有 actionTypes
actions
reducers
index
四个文件,actionTypes
文件表示action的type,文件里都是常量;actions
文件表示action和action creator;reducers
文件表示模块下的全部reducer的一个集合;index
文件只干一件事,import reducers 而后暴露出去,为的是遵循规范。
在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';
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 };
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节点,和单独的reducer
。
项目中,为了规范你们的代码,使用到了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于数据库仍是有本质区别的:后端是存取数据关系,前端是数据和组件相互订阅关系。
写多了就会总结出一套固定的写法,互相学习,参考。