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於數據庫仍是有本質區別的:後端是存取數據關係,前端是數據和組件相互訂閱關係。
寫多了就會總結出一套固定的寫法,互相學習,參考。