React-Native With Redux
轉載請注明出處 http://richard-cao.github.io/
經過上次的react-native小模塊完成之后,發現不少缺點,而且基本沒什么擴展性。這次正好又增加一個react-native模塊————我的等級特權,于是動手重構了項目里整個react-native的部分,隨著今晚項目發布上線,動手記錄下來這次重構的經驗。
本文目錄
- 為什么要做這次重構
- Flux模式與Redux
- React-Native With Redux
- 代碼規范和語法糖
- 重構過程中遇到的坑
- 總結
撰寫本文時筆者的相關環境如下
- 操作系統:OS X 10.11.2
- npm中react-native版本:0.17.0
- Android studio中react-native版本:0.17.1
為什么要做這次重構
之前的初次踩坑文章是在做第一個react-native需求——通知中心的時候寫的,當時為的是功能沒問題然后上線,并沒有考慮擴展、封裝、數據流等問題。當又要添加其他react-native模塊的時候,就必須要解決這樣的問題了,于是這次重構應運而生。
Flux模式與Redux
Flux模式
首先,我們知道,react-native根據什么render UI呢?答案就是state和props。那么可以預料到,當模塊增多、代碼量增加的話,如果沒有一套數據流規范,那么就會遇到state或props不統一導致刷新錯亂等問題。react是遵循Flux架構的,那么什么是Flux呢?這里我們看一張圖:

Store包含了應用的所有數據,Dispatcher替換了原來的Controller,當Action觸發時,決定了Store如何更新。當Store變化后,View同時被更新,還可以生成一個由Dispatcher處理的Action。這確保了數據在系統組件間單向流動。當系統有多個Store和View時,仍可視為只有一個Store和一個View,因為數據只朝一個方向流動,并且不同的Store和View之間不會直接影響彼此。(這段話引用自 非死book:MVC不適合大規模應用,改用Flux )
Redux
那么Redux是什么呢?Redux是javascript狀態容器,提供可預測化的狀態管理,可以構建一致化的應用,除了和React一起用外,還支持其他界面庫,體積小(只有2kb)而且沒有任何依賴。
Redux由Flux演變而來,但是避開了Flux的復雜性,上手快,使用簡單,而且社區活躍,是目前主流的Flux數據流框架。
從這里開始,默認讀者已經閱讀過Redux文檔,有Redux基礎。
React-Native With Redux
我的 package.json 中引用的模塊有:
"dependencies": { "immutable": "^3.7.5", "react": "^0.14.3", "react-native": "^0.17.0", "react-redux": "^3.1.0", "redux": "^3.0.5", "redux-thunk": "^1.0.2" }
</div>
redux目前的最新版本3.0.5是基于react 0.14的,所以同時加入 react 和 redux , react-redux 是Redux的react綁定庫, redux-thunk 是為了實現異步Action Creator引入的。
下面我以 請求用戶等級特權數據并刷新UI 為例梳理一遍整個數據流,包含 Action , Store , Reducer 三個重要概念。
首先,定義請求用戶等級特權數據的ActionType:
react-native/constants/ActionTypes.jsexport const FETCH_RANK_LIST = 'FETCH_RANK_LIST';
</div>
那么 FETCH_RANK_LIST 就代表了要執行請求等級特權數據的動作類型。然后開始定義Action:
react-native/actions/rank.js'use strict'; import * as types from '../constants/ActionTypes'; import {LEVEL_PRIVILEGES} from '../constants/Urls'; import {request} from '../utils/RequestUtils'; import {ToastShort} from '../utils/ToastUtils'; export function fetchLevelPrivileges() { return dispatch => { dispatch(fetchRankList()); request(LEVEL_PRIVILEGES, 'get') .then((rankList) => { dispatch(receiveRankList(rankList)); }) .catch((error) => { dispatch(receiveRankList([])); if (error != null) { ToastShort(error.message) } }) } } function fetchRankList() { return { type: types.FETCH_RANK_LIST, } } function receiveRankList(rankList) { return { type: types.RECEIVE_RANK_LIST, rankList: rankList } }
</div>
這里的Action是異步的,因為請求是異步的。其實意思很簡單,通過 fetchLevelPrivileges 請求了后端數據,異步獲取了數據之后進行數據的接收,觸發了接收數據的Action: RECEIVE_RANK_LIST ,請求和接收其實是一個連續的動作。
那么定義完Action之后,就需要定義 Reducer 了:
react-native/reducers/rank.js'use strict'; import * as types from '../constants/ActionTypes'; const initialState = { loading: false, rankList: [] } export default function rank(state = initialState, action) { switch (action.type) { case types.FETCH_RANK_LIST: return Object.assign({}, state, { loading: true }); case types.RECEIVE_RANK_LIST: return Object.assign({}, state, { loading: false, rankList: action.rankList }) default: return state; } }
</div>
可以看到 initialState 是初始的狀態,然后通過不同的type來更新state。這里state是全新的state,并不是在已有state的引用上改變數據,關于這點Redux的文檔中有詳細的解釋,這里不再贅述。簡單的reducer定義好之后,我們要開始定義 Store 了:
react-native/store/configure-store.js'use strict'; import {createStore, applyMiddleware} from 'redux'; import thunkMiddleware from 'redux-thunk'; import rootReducer from '../reducers/index'; const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore); export default function configureStore(initialState) { const store = createStoreWithMiddleware(rootReducer, initialState); return store; }
</div>
這里使用了 redux-thunk 來支持異步Action, Middleware 提供的是位于action發起之后,到達reducer之前的擴展點,這是一個比較重要的概念,具體請看redux文檔理解。 rootReducer 是最終合并后的reducer:
react-native/reducers/index.js'use strict'; import {combineReducers} from 'redux'; import notice from './notice'; import rank from './rank'; const rootReducer = combineReducers({ notice, rank }) export default rootReducer;
</div>
這里用到了redux的 combineReducers 函數,將多個模塊的reducer合并成一個。
最后我們需要串通整套數據流,我們需要做的是:
react-native/root.jsimport React from 'react-native' import {Provider} from 'react-redux/native' import configureStore from './store/configure-store' import App from './containers/app' const store = configureStore(); class Root extends React.Component { render() { return ( <Provider store={store}> {() => <App />} </Provider> ) } } export default Root;
</div>
這非常關鍵,root.js是index.android.js注冊的唯一入口,通過 Provider 組件講store注入進整個app,至此,整套數據流就串通起來了。
那么串通起來是怎么樣的呢?我來描述一下:用戶點擊進入等級特權頁面,通過action中 fetchLevelPrivileges 做了請求數據的動作,然后dispatch了 FETCH_RANK_LIST 這個動作,觸發了reducer更改state,刷新UI(此時應該是loading界面);然后當數據請求完成之后dispatch了 RECEIVE_RANK_LIST 這個動作,接收到請求獲取的數據,觸發了reducer更改state,再刷新UI(此時應該展示完整頁面)。這樣數據流就非常清晰了: Action => Dispatcher => Store => View 。當用戶進行其他操作時,由View發起Action,繼續這個單向的數據流,這樣就完成將Flux單向數據流的思想通過Redux融入React-Native項目當中了。
將 Flux的思想 應用于項目之中,確實感覺思路清晰,寫起來心里踏實。
代碼規范和語法糖
由于我是菜鳥,所以我在寫的時候嚴格遵循了 Airbnb React/JSX Style Guide ,相信大廠應該沒錯的。
語法糖我全部使用了ES6,因為react-native已經使用了Babel完全支持了 ES6語法糖 ,可以使用ES6的新特性,而且我感覺ES6對于我來說更容易理解,因為我是個寫Java的Android Developer……
重構過程中遇到的坑
這里我要說明一點: 使用Chrome調試react-native 非常重要!在重構的過程中,我都是通過debug來觀察數據流,看哪一環出現了問題再去解決。
還有一個 大坑!
當手機開啟 手勢觸摸 選項之后,在react-native頁面,同時用三個或三個以上手指觸摸上去你就會發現……crash了。iOS我沒測試過,這個是我在Android機器上發現的問題,然而 官方并沒有解決辦法 ,我安裝了react-native官方的showcase案例的一個app,發現該問題同樣存在。。。所以我只好 提了issue 。
還好這種情況很少,目前沒有接到線下類似這樣的crash反饋,估計是很多手機是不帶手勢觸摸的,而且估計很多用戶不會開啟手勢觸摸,其實我用Android手機的時候一直沒開過……在我寫這篇文章的時候 react-native 0.18.0-rc 已經發布了,但是并沒有看到修復類似的bug,不過0.18應該是一個相對較大的更新, react-native-cli 也更新到了0.1.10,Android里react-native的依賴庫也更新到了0.18.0版本,我打算等react-native發布0.18.0 release版本之后進行一次整體更新,繼續踩坑……
總結
對于我這個菜鳥來說,這次重構+新功能開發確實是有驚無險。總結一下:
所有action的定義都放在action包中,reducer放在reducers包中,store放在store包中,入口依然是index.android.js,只不過注冊的時候直接指向root.js,通過root將store注入到app當中,所有的模塊都包一層containers,在這里進行connect:
react-native/containers/RankContainer.js'use strict'; import React from 'react-native'; import {connect} from 'react-redux/native'; import Rank from '../components/Rank'; class RankContainer extends React.Component{ render() { return ( <Rank {...this.props} /> ) } } function mapStateToProps(state){ const {rank} = state; return { rank } } export default connect(mapStateToProps)(RankContainer);
</div>
這里可以看到,所有的頁面都是 組件 ,這里的 <Rank /> 就是等級特權頁面組件,包括自定義控件等組件全部放入components包中,于是整個工程被組件化了,更容易與iOS進行融合。然后在utils包中定義utils,constants包中寫了ActionTypes和Urls。在新增模塊的時候,思路已經非常清晰了,其實就是做 填空題 :在ActionTypes中添加動作定義,在actions中定義Action,在reducers中定義reducer,然后在containers中寫好容器外殼,最后在components中寫組件,個人感覺是 可擴展的彈性小架構 ,思路、封裝、數據流、組件等等都比較清晰,目前這就是我重構之后的樣子了。因為這些都是我一個人摸索的,等與公司的web工程師們交流時他們或許會給出更好的建議,期待ing~~
最后附上我的工程目錄(IDE: Sublime Text 3):

至此,本文結束。歡迎大家互相交流討論。我只是菜雞,拋磚引玉~
來自: http://richard-cao.github.io/2016/01/12/React-Native-With-Redux/