在 React Native 中使用 Redux 架構

前言

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: 把數據從應用傳到 store 的有效載荷

上面是官方給出的解釋,但這個解釋比較抽象,這麼說吧,若是你想要獲取一個數據,好比說一張圖片,那麼就能夠定義一個 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

Middleware 實際上是 action 抽象出來的,是對 action 的進一步封裝,用來完成異步 API調用等其它事情。本例中沒有使用 middleware。spring

Reducer:根據一個 action 來返回一個 new state 以更新 state

在上面的例子中,咱們 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 應該是一個純函數,這裏不容許對數據作額外的處理:

  • 修改傳入參數;
  • 執行有反作用的操做,如API請求和路由跳轉;
  • 調用非純函數,如 Date.now()或 Math.random()等。

reducer 就是一個函數,接收舊的 state 和 action,返回新的 state:

(previousState, action) => newState

改變了 state 後,就會觸發 Component 的 render 方法,從新進行渲染。

Store:把 Action 和 Reducer 聯繫到一塊兒的對象

上面是官方的描述,簡而言之,能夠理解爲 store 負責綁定 action 和 reducer。一個 Redux 應用只用一個 store。store 有如下職責:

  • 維持應用的 state;
  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 經過 subscribe(listener) 註冊監聽器;
  • 經過 subscribe(listener) 返回的函數註銷監聽器。

來看下例子:

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> ``` ###後記
React Native JChat 目前還沒有完善,以後會慢慢實現。若是你想對 [React Native JChat] (https: //github.com/jpush/jmessage-react-plugin)提一些改進,歡迎 fork,並提交 PR。有什麼問題可與我聯繫:caiyaoguan@gmail.com


做者:KenChoi
連接:https://www.jianshu.com/p/4139babc6d5e
來源:簡書
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

 

引用原文:https://www.jianshu.com/p/4139babc6d5e

 

寫博客是爲了記住本身容易忘記的東西,另外也是對本身工做的總結,文章能夠轉載,無需版權。但願盡本身的努力,作到更好,你們一塊兒努力進步!

若是有什麼問題,歡迎你們一塊兒探討,代碼若有問題,歡迎各位大神指正!

相關文章
相關標籤/搜索