Best Practice for React-Native and Redux

从 6 月到现在,在 RN 上面摸爬滚打了一段时间了,目前总算找到了一个适合我们自我感觉还可以的开发模式。

一开始,我们使用的是传统的 React 的模式,然后一个 app 页面一个文件,通过 StackNavigator 组合到一起。这么做在我们的第一个 app 里面没觉得有什么问题,每个页面维护自己的数据,页面之间需要数据共享或者通讯的时候(比如从 list 到详情页面的时候,详情里面有一个改变状态的按钮,状态改变之后希望 list 的状态也跟着变化,这样用户返回之后能看到正确的数据)有 2 个方式。

  • 通过 DeviceEventEmitter。 需要数据的页面订阅,然后在其他页面 emit event 之后前面的页面就可以收到。这个时候可以只通知改变的数据的字段,前一个页面直接去修改,这样可以避免重新刷新页面。也可以发一个简单的通知让其他页面去自己获取数据。
  • 通过 navigator 提供的 params 属性。 StatckNavigator 提供了一个 params 属性。就是 this.props.navigation.state.params ,可以通过 setParams 来改变,或者通过类似 this.props.navigation.navigate('Login', {goBackToHome: true}) 的方式给,那个 goBackToHome 将来就会在 params 里面。

直到我们开始做第二个 app。

第二个 app 是一个单页面 app,登录之后就只有一个页面了,有一个大地图,有左侧 sidebar,sidebar 里面的按钮点击还会出其他页面。这个肯定不能按照我们前面的思路来做了,我们按照组件,拆分了不同的文件,然后组合起来。这个时候更加会需要页面之间的通讯,并且这个时候可选项只有第一个了 DeviceEventEmitter ,因为都没有 navigate 什么事情。

这个时候就发现一个问题,event 太多了,开始有点混乱了。emit event 之后,慢慢会发现不知道哪里有订阅,不好管理。这个 app 做完之后,就仔细研究了一下 redux。

其实写第一个 app 的时候就知道 redux,但是很多概念看的云里雾里的,当时在 react 还没有吃透的情况下,根本没有能力把 redux 搞好。所以当时放弃了 redux。

了解 redux 之后,感觉这个东西是我们的药。统一的 state 管理,这不就不用考虑状态传递了么?所以一门心思开始研究 redux。刚好我们第一个 app 需要全新改版,我们就借机把我们的第一个 app 也重构到了 redux 实现。整体过程还是蛮舒服的,自己也总结了几条我们自己的使用的思路。

目录结构安排

先大概看看我们 app 的目录结构。我们把所有的 js 文件都放到了 app 目录下面。

app
ios
android
index.js

然后 app 目录下面,分了 actionsreducerssagasselector 几个目录。

actions
images
index.js
reducers
sagas
screens
selectors
utility
  • actions 里面放的是 mapDispatchToProps 这个逻辑对应的东西。
  • reducers 里面放的是 reducers。
  • sagas 里面放的是所有网络请求相关的 actions 的处理逻辑。
  • seelctor 里面放的是 mapStateToProps 这个逻辑对应的东西。

每个目录里面也都有一个自己的 index.js 把本目录里面的内容组合起来。通过最外面的 index.js 把这几个目录的逻辑组合起来。

给页面设计一个基类

这样会比较方便你去做一些所有页面都需要做的事情。

一个 app 一个统一的 store

我们 app 还不大,所以这么设计也还好,如果页面比较复杂,我看也有组件使用自己的 store 的例子,这个还没有经验。这么做唯一一个问题就是,那个 store 里面的数据一直都在,多少会占用一些内存。不过我是觉得没啥了,其实这点内存占用不算啥。

store 设计和页面无关

Store 参考了一个文章统一设计,和页面无关。比如我们设计了 user, orders, orderDetail 这些 state,数据所有页面共享。否则如果按照页面来划分的话,某些页面之间如果有用到共享数据就要么多复制一份,那有点浪费了,要么就是会有点乱。

每个页面都使用自己的 props

不在页面间交叉使用 props ,这样不会乱。并且因为我们是一个统一的 store,所以其实每次 props 变化,所有页面都会 render。这个我使用下面的一个思路来解决了。

shouldComponentUpdate

这个就是在页面的基类里面,通过比较判断本页面的 props 是否有变化来解决前面那个 render 问题。

使用 reselect

因为只要 state 发生变化 redux 就会调用 mapStateToProps 来计算 props,这个计算有一些消耗,毕竟一般也就其中一个页面的 props 需要计算。我们用这个 reselect 解决这个问题,一个页面的 props 需要的 state 没变化的时候,reselect 就可以把 cache 的数据直接返回就好了。

适当使用页面的 state

redux 的理念是所有页面的 state 都放到了 store 里面,你不需要做 setState 动作了。但是实际上有些时候适当使用 state 会让你的开发更加方便。比如表单验证,用户输入数据之后点击提交 ,如果通过发送 action 改变 state 然后再通过 selector 返回页面,那就有点太费劲了。而直接通过 setState 设定页面 state,然后在提交表单的时候读出来做验证就简单多了。

有时候页面的一些 state 是和 props 有关系的,这个时候可以使用 componentWillReceiveProps(nextProps) 来判定,然后和 state 同步。

Android 的返回按钮处理

android 有一个实体的返回按钮,StackNavigator 给出的方案是监听一个 hardwareBackPress 事件,然后 dispatch(NavigationActions.back()) ,但是有一个问题是,有时候我们返回的时候还需要做一些自己的动作。比如清理 store 的数据,或者判断一下往哪里返什么的,比如用户刚提交了订单之后,给了一个按钮可以看订单详情,这个时候从详情返回就希望直接到首页,不要又返回新建订单的页面。

我们通过下面的思路做的

    onBackPress = () => {
        const { dispatch, nav } = this.props;

        if (nav.index === 0) {
            return false;
        }
        const {routes} = nav;
        const {params} = routes[routes.length-1]

        if(params && params.goBack) {
            params.goBack();
        } else {
            dispatch(NavigationActions.back());
        }
        return true;
    };

然后在页面的基类里面

    constructor(props) {
        super(props);
        if (this.goBack)
            this.props.navigation.setParams({ goBack: ()=>this.goBack() })
    }

然后页面里面如果有自己的特殊逻辑,那就实现一个 goBack 方法就好了。

表单的弹出页面,不一定需要使用 store

比如一个下单页面,需要填联系人信息,这个时候我们一般会到一个联系人的页面来选择联系人。这个时候在这个页面选择的联系人,如何传递给上一个页面呢?有两个类型的方法。

第一个方法自然就是 redux 的方法,在选择页面点确定的时候,触发 action 通过 reducer 设置这个页面的 store,然后通过 selector 修改上一个页面的 props,这样就达到了传递的目的。

第二个方法是在新页面打开的时候,通过 navigater 传一个 callback 过去,那边选择好的时候,调用这个回调方法把数据传回来。

第一个方法贴合 redux 的做法,但是存在一个问题,如果这个新的选择页面在多个地方出现,那么就需要有一个区分,当前这个选择是给哪个地方服务的(因为必须得在 redux 的 store 里面做好区分,否则两个页面总是相同的状态)。另外还有一个数据清理的问题,否则下次在别的页面打开这个页面,会有上次的数据残留。

第二个方法土一点,但是没有上面的问题。不过要注意的是,如果新的页面有网络请求,那这个时候还需要和 saga thunk 这些关联,那么就总是会走到 redux 的 store,所以这个方法就不适用了。