Redux 架構是 Flux 架構的一個變形,相對於 Flux,Redux 的複雜性相對較低,並且最爲巧妙的是 React 應用能夠當作由一個根組件鏈接着許多大大小小的組件的應用,Redux 也只有一個 Store,並且只用一個 state 樹來管理組件的狀態。隨着應用逐漸變得複雜,React 將組件當作狀態機的優點彷彿變成了自身的絆腳石。由於要管理的狀態將會愈來愈多,直到你搞不清楚某個狀態在不知道何時,因爲什麼緣由,發生了什麼變化。Redux 試圖讓狀態的變化變得可預測。Redux 的官方文檔javascript
若是你厭倦了官網的 ToDoList 例子,以及 React Native 上少之又少的關於應用 Redux的參考(遺憾的是,仍然是 ToDoList 案例)。本文將結合做者的親身經歷,幫助那些初次接觸 Redux,而且須要將 Redux 架構應用到本身的 React Native 應用的開發者避開一些坑(反正我是被坑了T_T)。php
本文將和開發者一塊兒學習如何將 Redux 架構應用到 React Native 應用上,而且使用jmessage-sdk 初步構建一個聊天應用。咱們先來看看最終效果吧:html
本例的源碼地址java
在開始使用 Redux 架構以前,咱們先來捋一捋 action,reducer,以及 store 這三者的概念,以及他們之間的關係。儘管 Redux 文檔已經寫得相對清晰,但對於初學者來講仍是不(you)太(dian)友(meng)好(bi)的。咱們先來看一張圖,大體瞭解一下它們之間的關係:react
上面是官方給出的解釋,但這個解釋比較抽象,這麼說吧,若是你想要獲取一個數據,好比說一張圖片,那麼就能夠定義一個 action 來完成這個動做。當你執行這個動做的時候,你就能夠 dispatch 一個正在獲取的 action,這時 Reducer 就可以根據這個動做來返回一個 state,這時候 UI 可以根據這個狀態來作一些事情(好比你在拉圖片時確定不但願 UI 就這樣卡住,可能要顯示進度條之類的)。若是數據返回了,就能夠再次dispatch 一個 action,而後 Reducer 就會返回新的 state。下面咱們來看看例子:android
actions/conversationActions.jsgit
export function addFriend(username) { return dispatch => { //先直接 dispatch 一個類型爲 adding 的 action type: types.ADDING_FRIEND, JMessageHelper.addFriend(username, (result) => { dispatch ({ //數據返回後再 dispatch 一個 action,而且也返回了這個數據 type: types.ADD_FRIEND_SUCCESS, conversation: JSON.parse(result) }); }) } }
上面的代碼就是一個 action 建立函數。根據傳過來的 username,dispatch 一個正在添加好友的 action,這樣 Reducer 就能夠根據這個 action 返回一個正在添加的 state。當數據返回後,再 dispatch 一個添加成功的 action,而且同時返回數據。這裏沒有 dispatch 一個添加失敗的 action,由於在 Native 中 catch住了,嚴格來講,是要處理請求失敗的響應。在官網中的例子是使用了 fetch 方法,但本例是使用了 jmessage-sdk 來請求數據,所以是個混合的 React Native 應用。github
Middleware 實際上是 action 抽象出來的,是對 action 的進一步封裝,用來完成異步 API調用等其它事情。本例中沒有使用 middleware。spring
在上面的例子中,咱們 dispatch 的每個 action 都會被 reducer 捕捉到。reducer 能夠根據 action 的t ype 來返回一個新的 state。在 Redux 架構中,全部的 state 都保存在一個單一的對象中。這個對象會隨着應用的複雜而變得愈來愈龐大,state 的結構也會變得更加複雜。這個時候就須要拆分 reducer,使得每一個 reducer 只負責改變一小部分 state。來看看例子:編程
reducers/conversationReducer.js
export default function conversationList(state, action) { state = state || { type: types.INITIAL_CONVERSATION_LIST, dataSource: [], fetching: true, adding: true, error: false, } switch(action.type) { //正在添加 case types.ADDING_FRIEND: return { //使用擴展運算符返回繼承以前的狀態,若是不更新狀態的話這樣寫就好了, //在 Component 中經過 this.props.state 就能夠獲得整個 state,在 Component 中能夠看到具體使用場景 ...state, //在這裏也返回了 action,這樣能夠在 Component 經過 this.props.action 直接調用某個 action ...action, adding: true } break; //添加成功 case types.ADD_FRIEND_SUCCESS: var convList = [...state.convList]; convList.unshift(action.conversation); dataSource = state.dataSource.cloneWithRows(convList); console.log('add success convList: ' + convList.toString()); return { ...state, ...action, convList: convList, dataSource, adding: false } default: return { ...state } } }
這是一個 reducer 函數,根據 action.type 來返回 new state,reducer 應該是一個純函數,這裏不容許對數據作額外的處理:
reducer 就是一個函數,接收舊的 state 和 action,返回新的 state:
(previousState, action) => newState
改變了 state 後,就會觸發 Component 的 render 方法,從新進行渲染。
上面是官方的描述,簡而言之,能夠理解爲 store 負責綁定 action 和 reducer。一個 Redux 應用只用一個 store。store 有如下職責:
來看下例子:
store/configureStore.js
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from '../reducers/index'; //使用 Redux 提供的 createStore 方法便可,thunk 提供了異步功能有興趣的能夠了解一下,這裏不許備深刻 const createStoreWithMiddleware = applyMiddleware( thunk ) (createStore); //initialState 能夠設置初始狀態,能夠用於把服務器端生成的 state 轉變後傳給應用 export default function configureStore(initialState) { const store = createStoreWithMiddleware(rootReducer, initialState); if (module.hot) { module.hot.accept('../reducers', () => { const nextReducer = require('../reducers'); store.replaceReducer(nextReducer); }); } return store; } ``` 須要注意的是上面的 import 的 reducers/index 是對 reducer 的合併,後面會提到。上面的代碼是 store 的一種寫法,能夠支持熱更新。關於熱更新,目前筆者還沒有研究(hahaha)。 ##配置 如今咱們已經知道 action,reducer 以及 store 的基本概念和做用了,接下來就要把它們鏈接起來。先來看看咱們的目錄結構: ``` |---index.android.js |---react-native-android |---actions |---containers |---reducers |---store ``` 先來看看JS入口 index.android.js: >index.android.js ``` 'use strict'; import React from 'react-native'; import ReactJChat from './react-native-android/containers/ReactJChat'; var MyAwesomeApp = React.createClass({ render() { return ( <ReactJChat /> ); } }); React.AppRegistry.registerComponent('JChatApp', () => MyAwesomeApp); ``` 能夠看到這裏僅僅是把入口交給了 React JChat。這裏再次提醒一下,儘可能使用 ES6 的語法 import,而不是 require,不然會出現莫名其妙的錯誤(T_T)。接下來是ReactJChat.js。 >containers/ReactJChat.js ``` import React, { Component } from 'react-native'; import { Provider } from 'react-redux'; import BaseApp from './BaseApp'; import configureStore from '../store/configureStore'; const store = configureStore(); export default class ReactJChat extends Component { render() { return ( <Provider store = { store }> <BaseApp /> </Provider> ); } } ``` 這裏首先經過 configureStore()獲得 store,而後經過 Provider 把 store 傳給真正的入口 BaseApp。 >containers/BaseApp.js ``` class BaseApp extends Component { renderScene(route, navigator) { _navigator = navigator; let Component = route.component; const { state, dispatch } = this.props; const action = bindActionCreators(actions, dispatch); return <Component //將 state,params,actions,navigator 經過屬性傳遞到 Component {...route.params} state = { state } actions = { action } navigator = { navigator } /> } render() { return ( <Navigator initialRoute = { this.initialRoute } configureScene = { this.configureScene } renderScene = { this.renderScene } /> ); } } function mapSateToProps(state) { return { state: state } } export default connect(mapSateToProps) (BaseApp) ``` 這裏咱們主要看一下 renderScene() 這個函數,const { state, dispatch } = this.props;這句經過 this.props 能夠從 store 中獲得 state 及 dispatch,而後經過 bindActionCreators() 將咱們定義的全部 actions 經過 dispatch 關聯到 state,這樣reducer 就可以接收 dispatch 的 action,而後返回新的 state 了。在 renderScene() 的返回中,將 state、action 及 navigator 都做爲屬性傳遞到 Component 了。接下來還定義了一個函數 mapStateToProps(),這個方法便是將 state 做爲 props 能夠在所傳遞的 Component 中經過 this.props 獲得 state,也就是包含全部數據的對象。最後經過 react-redux 提供的 connect 方法,將使得 BaseApp 能夠經過 this.props 獲得 state 。到此爲止,咱們已經完成了鏈接了 action、reducer 以及 store。接下來就能夠在 Component 中發起從 Component->Action->Reducer->(Store->)Component 的數據流了。 ##使用 若是你完成了上面的步驟,咱們就能夠在 Component 中發起一個 action 來發動數據流。在本例中,咱們從一個會話列表開始講解數據是如何流動的。先來看一下咱們的會話列表片斷: >containers/conv_fragment.js ``` render() { //這裏經過 this.props.state 獲得 conversationReducer 對象,之因此能獲得,是由於在 main_activity 中將 //state 做爲屬性傳過來了 const { conversationReducer } = this.props.state; //經過 conversationReducer 獲得 convList 數組 _convList = conversationReducer.convList; var content = conversationReducer.dataSource.length === 0 ? <View style = { styles.container }> { conversationReducer.fetching && <View style = { {alignItems: 'center', justifyContent: 'center'} }> <Text style = { {fontSize: 24, }}> 正在加載... </Text> </View> } </View> : <ListView style = { styles.listView } ref = 'listView' //經過獲得 conversationReducer 的 dataSource 做爲 ListView 的 dataSource dataSource = { conversationReducer.dataSource } renderHeader = { this.renderHeader } renderRow = { this.renderRow } enableEmptySections = { true } keyboardDismissMode="on-drag" keyboardShouldPersistTaps={ true } showsVerticalScrollIndicator={ false }/>; ``` 以上咱們經過 reduer 對象,來得到渲染 ListView 所需的數據。而 reducer 又是根據 action 來返回 state (也就是數據)的,這樣以來咱們只要發起一個 action 就可使得數據流通起來。 - **定義 Action** 在發起一個 action 以前,咱們先來想象一下,這個 action 的內容。一個用戶登陸後,將但願看到本身的會話列表,這個時候咱們須要先拉取一下本地的會話列表記錄。這樣一來,咱們的 action 就誕生了(這裏我經過一個例子但願你們理解,把需求轉換爲具體的 action 的過程)。來看一下具體的代碼: ``` export function loadConversations() { return dispatch => { //這裏我在另外一個文件中聲明瞭 type,也可直接在 action 中聲明 type: types.INITIAL_CONVERSATION_LIST, JMessageHelper.getConvList((result) => { dispatch({ //返回數據後,定義一個 action 類型用來在 reducer 中進行相關操做 type: types.LOAD_CONVERSATIONS, convList: JSON.parse(result), }); }, () => { dispatch({ type: types.LOAD_ERROR, convList: [] }) }); } } ``` 上面是一個獲取本地全部會話列表的 action 聲明函數。在開始獲取數據以前,直接 dispatch 了一個 action,即 INITIAL_CONVERSATION_LIST,在數據返回後,又 dispatch 了一個成功和一個失敗的 action。接着咱們在 reducer 中來實現一下捕獲這幾個 action 的函數: - **定義 reducer** >reducers/conversationReducer.js ``` export default function conversationList(state, action) { //若是 state == 上一個 state,或者爲 INITIAL_CONVERSATION_LIST 的 action,那麼返回空的數據以及一些 boolean 值 state = state || { type: types.INITIAL_CONVERSATION_LIST, dataSource: [], fetching: true, adding: true, error: false, } switch(action.type) { case types.LOAD_CONVERSATIONS: var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }); var convList = action.convList; dataSource = dataSource.cloneWithRows(convList); return { ...state, ...action, convList: convList, dataSource, fetching: false, } case types.LOAD_ERROR: var dataSource = action.convList; return { ...state, ...action, dataSource, } ... ``` 上面聲明瞭一個名爲 conversationList 的 reducer 函數,根據參數 (state, action) 來返回一個新的 state。switch 語句中根據 action 的類型分別進行了處理。當 action 被 dispatch 後,與之綁定的 reducer 函數就會執行。能夠看到,上面 action 的類型爲 LOAD_CONVERSATIONS 時,代表獲得了全部本地會話列表的數據。這樣 reducer 要作的就很是明確了:把 action 中的數據取出來,返回這個數據就好了。再次強調, reducer 中不可以出現複雜的操做,以及非純函數的調用。因爲咱們的 ListView 要展現的數據來源於 dataSource,這樣咱們就直接聲明瞭 dataSource,而後把數據放進去。至於返回時,要帶哪些參數,能夠取決於本身的需求。注意,這裏的 ...state 以及 ...action,這是爲了在返回新的 state 後,仍然能夠在 Component 中經過 this.props 獲得 state 以及 action,不然會出現 undefined 的錯誤。 - **發起 action** 其實人的慣性思惟是先發起 action,而後再去考慮 action 裏面須要什麼內容,而後是 reducer 應該怎樣處理 action,並返回 state。這裏我之因此沒有按照這種順序,是由於,站在編程的角度上來講,我從一個功能需求出發,把需求轉化成 action,而後再去決定在一個恰當的時候發起這個 action。這兩種想法並無褒貶之意,你們見仁見智,但願起到一個拋磚引玉的做用。咱們回到會話列表界面: >containers/conv_fragment.js ``` componentDidMount() { //獲得 loadConversations 這個 action const { loadConversations } = this.props.actions; const { conversationList } = this.props.state; //發起 action loadConversations(); } ``` 這裏我在組件的生命週期函數 componentDidMount 中發起了拉取本地全部會話列表的 action,由於 componentDidMount 是在組件 render 之後立刻會執行的函數。經過 this.props 獲得以前在 conversationActions 中定義的 loadConversations 函數就能夠經過直接調用這個函數來發起 action 了。這樣一來,一旦數據返回,就會執行 action->reducer 這個流程,因爲在 reducer 中返回的 state 改變了,就會觸發 render 從新對界面進行渲染,會話列表也就可以在 ListView 中展現了。 ###注意事項 若是你的界面使用了 Fragment(好比本例),在使用 Redux 架構時,必定要注意將 state 或者 action 做爲屬性傳遞到 Component 中,這樣才能在 Component 中經過 this.props 獲得 state 及 action。 >containers/main_activity.js ``` pages.push( <View key = { 0 } style = { pageStyle } collapsable = { true }> <Conv //將 state,action 傳到 Conv Component state = { this.props.state } actions = { this.props.actions } navigator = { this.props.navigator } /> </View> ); ``` 另外,還有關於 actions 及 reducers 的合併,能夠參考本例源碼中的 actions/index.js 以及 reducers/index.js。在作好這些後,就能夠盡情使用 Redux 了(就跟作填空題同樣)。還有一點,在 Redux 應用中,全部的 state 都放在一個單一的對象也就是 state 樹中,官網建議,UI 相關的 state 與數據相關的 state 儘可能分開,即 UI 相關的 state 不要放在 state 樹裏面。 ###關於一些坑 - **ListView** 接下來我來講一下關於 ListView 的坑點吧。ListView 中的數據來源於 dataSource,dataSource 的通常寫法是這樣的: ``` var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }); ``` rowHasChanged 的做用是當 r1 和 r2 **引用不一樣**時,就會刷新 ListView。這是一個坑點,只是數據不一樣是不能刷新 ListView 的。下面用一個刪除會話做爲示例: >reducers/conversationReducer.js ``` case types.DELETE_CONVERSATION: var selected = action.selected; var convList = [...state.convList]; convList.splice(selected, 1); var newList = new Array(); newList = convList; dataSource = state.dataSource.cloneWithRows(newList); return { ...state, ...action, convList: newList, dataSource } ``` 這是 reducer 中的一段代碼,action 的類型爲 DELETE_CONVERSATION 時,從 dataSource 中刪除一個選中的元素,並返回。這裏先在 convList 中刪除掉被選中的會話,而後經過 var newList = new Array(); 新建的數據來複制數據。這實際上是一種效率不高的作法,能夠[參考 Immutable JS](https://facebook.github.io/immutable-js/)。 - **Image** 官方的 Image 組件功能仍是蠻強大的,要顯示網絡圖片的話提供一個 uri 就能夠自動顯示圖片;若是要顯示 drawable 文件夾下的圖片,直接在 uri 中填寫圖片的名字就好了,如: ``` <Image style = { styles.titlebarImage } source = { {uri: 'msg_titlebar_right_btn'}} /> ``` 若是要顯示在 sd 卡中的圖片的話,能夠在 uri 中加 file:/// 路徑就能夠顯示,如: ``` <Image style = { styles.titlebarImage } source = { {uri: 'file:///data/jchat/images/head_icon.png'}} /> ``` 可是若是要顯示應用包名下的圖片(放在 sd 卡有可能被其餘應用修改),那就要費一些周折了。這裏本例的作法是把圖片在 native 中轉成 base64 字符串(都是小頭像),而後傳到 JS。Image 控件也是支持 base64 顯示的,不過要加上前綴: ``` //**注意加上前綴** avatar = "data:image/png;base64," + getBinaryData(avatarFile); private String getBinaryData(File file) { try { FileInputStream fis = new FileInputStream(file); byte[] byteArray = new byte[fis.available()]; fis.read(byteArray); fis.close(); byte [] encode = Base64.encode(byteArray, Base64.DEFAULT); return new String(encode); } catch (Exception e) { e.printStackTrace(); return "head_icon"; } } ``` - **Modal** 因爲 React Native 中沒法自定義 dialog(目前只有 AlertDialog),因此使用了 Modal 來替代 dialog,能夠[參考這個](https://github.com/magicismight/react-native-root-modal)。可是 Modal 其實是 Root View 的一個 sibling View,也就是說 Modal 會始終懸浮在最前面。儘管 Modal 有這個「缺陷」,可是爲一個 Modal 添加各類動畫在 React Native 中是很是容易的。 >containers/conv_fragment.js ``` showDropDownMenu() { if (!this.state.showAddFriendDialog && !this.state.showDelConvDialog) { if (this.state.showDropDownMenu) { this.dismissDropDownMenu(); } else { this.state.y.setValue(-600); this.state.scaleAnimation.setValue(1); //spring 是一個彈跳物理模型,這裏讓y的值從 -600 減到 0,就是從上面掉下來的動畫 Animated.spring(this.state.y, { toValue: 0 }).start(); this.setState({ showDropDownMenu: true }); } } } dismissDropDownMenu() { Animated.timing(this.state.y, { toValue: -600 }).start(() => { this.setState({ showDropDownMenu: false}); }); } <Animated.Modal style = { [styles.dropDownMenu, {transform: [{translateY: this.state.y}, {scale: this.state.scaleAnimation}]}] } visible = { this.state.showDropDownMenu }> </Animated.Modal> ``` ###後記
做者:KenChoi
連接:https://www.jianshu.com/p/4139babc6d5e
來源:簡書
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
引用原文:https://www.jianshu.com/p/4139babc6d5e
寫博客是爲了記住本身容易忘記的東西,另外也是對本身工做的總結,文章能夠轉載,無需版權。但願盡本身的努力,作到更好,你們一塊兒努力進步!
若是有什麼問題,歡迎你們一塊兒探討,代碼若有問題,歡迎各位大神指正!