android redux 架构,在React Native中使用Redux架构

前言

Redux架构是Flux架构的一个变形,相对于Flux,Redux的复杂性相对较低,而且最为巧妙的是React应用可以看成由一个根组件连接着许多大大小小的组件的应用,Redux也只有一个Store,而且只用一个state树来管理组件的状态。随着应用逐渐变得复杂,React将组件看成状态机的优势仿佛变成了自身的绊脚石。因为要管理的状态将会越来越多,直到你搞不清楚某个状态在不知道什么时候,由于什么原因,发生了什么变化。Redux试图让状态的变化变得可预测。Redux的官方文档

如果你厌倦了官网的ToDoList例子,以及React Native上少之又少的关于应用Redux的参考(遗憾的是,仍然是ToDoList案例)。本文将结合作者的亲身经历,帮助那些初次接触Redux,并且需要将Redux架构应用到自己的React Native应用的开发者避开一些坑(反正我是被坑了T_T)。

本文将和开发者一起学习如何将Redux架构应用到React Native应用上,并且使用jmessage-sdk初步构建一个聊天应用。我们先来看看最终效果吧:

开始

在开始使用Redux架构之前,我们先来捋一捋action,reducer,以及store这三者的概念,以及他们之间的关系。尽管Redux文档已经写得相对清晰,但对于初学者来说还是不(you)太(dian)友(meng)好(bi)的。我们先来看一张图,大致了解一下它们之间的大致关系:

Action: 把数据从应用传到store的有效载荷

上面是官方给出的解释,但这个解释比较抽象,这么说吧,如果你想要获取一个数据,比如说一张图片,那么就可以定义一个action来完成这个动作。当你执行这个动作的时候,你就可以dispatch一个正在获取的action,这时Reducer就能够根据这个动作来返回一个state,这时候UI能够根据这个状态来做一些事情(比如你在拉图片时肯定不希望UI就这样卡住,可能要显示进度条之类的)。如果数据返回了,就可以再次dispatch一个action,然后Reducer就会返回新的state。下面我们来看看例子:

actions/conversationActions.js

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应用。

Middleware

Middleware其实是action抽象出来的,是对action的进一步封装,用来完成异步API调用等其它事情。本例中没有使用middleware。

Reducer:根据一个action来返回一个new state以更新state

在上面的例子中,我们dispatch的每一个action都会被reducer捕捉到。reducer可以根据action的type来返回一个新的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有以下职责:

来看下例子:

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 (

);

}

});

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 (

);

}

}

这里首先通过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

//将state,params,actions,navigator通过属性传递到Component

{...route.params}

state = { state }

actions = { action }

navigator = { navigator }

/>

}

render() {

return (

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 ?

{ conversationReducer.fetching &&

正在加载...

}

:

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(

//将state,action传到Conv Component

state = { this.props.state }

actions = { this.props.actions }

navigator = { this.props.navigator }

/>

);

另外,还有关于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。

Image

官方的Image组件功能还是蛮强大的,要显示网络图片的话提供一个uri就可以自动显示图片;如果要显示drawable文件夹下的图片,直接在uri中填写图片的名字就行了,如:

style = { styles.titlebarImage }

source = { {uri: 'msg_titlebar_right_btn'}}

/>

如果要显示在sd卡中的图片的话,可以在uri中加file:///路径就可以显示,如:

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,可以参考这个。但是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});

});

}

style = { [styles.dropDownMenu, {transform: [{translateY: this.state.y}, {scale: this.state.scaleAnimation}]}] }

visible = { this.state.showDropDownMenu }>

后记

React Native JChat目前尚未完善,之后会慢慢实现。如果你想对React Native JChat提一些改进,欢迎fork,并提交MR。有什么问题可与我联系:caiyaoguan@gmail.com

你可能感兴趣的