Blog Logo

React-Redux基本用法

写于2017-08-26 14:57 阅读耗时12分钟 阅读量


React Redex

上一篇讲解了Redux的基础知识和概念,也很简单,本篇将重点讲解Redux在React Native上的使用。


1.真实项目为什么需要使用Redux

说实话,我其实也不怎么想使用Redux,因为感觉学习的东西太多,概念啦、API啦,体系啦,流程啦等等,觉得太麻烦。 但在我正在研发的这款APP中发现,有些地方必须使用到Redux或是其他什么框架才能解决一些问题。

真印证了Redux 的创造者 Dan Abramov 说过的那句话: "只有遇到 React 实在解决不了的问题,你才需要 Redux 。"

那么我当时到底遇到了些什么问题必须要用Redux呢? 使用场景是这样的:


1. 引导页、登录页、登录成功后的首页

这三个页面看似简单,其实里面的逻辑暗藏杀机:

1.引导页只会出现一次
2.引导页点击后需要进入登录页
3.登录页必须认证成功后才会进入首页
4.登录过的就直接跳首页

那么问题来了,用户打开APP,可能是老用户、也有可能是新用户,流程的情况可能有以下几种情况:

1.直接跳引导页:
新用户默认先进入引导页
2.直接跳登录页:
新用户在引导页点击进入登录页后退出APP,他不想登录了,等他下次想使用APP的时候,直接跳登录页,跳过了引导页
3.直接跳首页:
新用户跳引导页也登录认证成功后,下次进来的时候他就无需再次登录,可以直接开始APP

如何处理和控制这些情况呢? 在没有使用Redux之前,我的处理方式是: 引导页为一个页面,登录页和首页在同一个页面,使用缓存来传递和读取用户情况进行显隐页面

笨方法就不细讲了,这有个致命的地方:状态多变,到底哪种情况是跳哪个页面,改变缓存里的哪个变量,获取缓存的变量后怎么改变state渲染页面等等,太麻烦了。


2.获取头像和昵称

我从登录页登录成功后,我需要在登录成功后的接口里获取到用户的头像和昵称,然后通过react-native-navigation的API传递参数到首页。 但是下次进去首页的时候,是没有参数传递到首页的,只能通过获取用户信息的接口里重新获取到用户的头像和昵称。

那么获取用户的头像和昵称这一状态,有两个地方获取,一个是从登录页面传过来的,一个是从获取用户信息的接口获取的。但是它们却控制着相同的View。

简单讲,有多个逻辑会去改变相同状态的时候,就显得很麻烦了。如何解决这两个问题的,后续章节会详细讲到。


总结一下:

第一个问题属于交互复杂,因为用户的不同情况会给予他不同的页面。 第二个问题属于某个组件的状态,需要共享,因为用户的头像和昵称可能是从页面传递的,也可能是从接口获取的。


2.redux+react-native-navigation

在使用react-native-navigation的情况下,可以将redux安装和整合到项目中。步骤如下:

1.安装redux、react-redux、redux-thunk

npm install --save redux
npm install --save react-redux
npm install --save redux-thunk

为什么使用react-redux? React专用的Redux库,能提供Provider组件和connect方法。

为什么使用react-thunk? store.dispatch方法的增强,正常情况下,参数只能是对象,不能是函数。而使用react-thunk后store.dispatch参数可以是函数


2.配置App.js

APP

import {createStore, applyMiddleware, combineReducers} from "redux";
import {Provider} from "react-redux";
import thunk from "redux-thunk";

import * as reducers from "./reducers";
import {registerScreens} from "./screens";
...
// 创建reducer和store
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const reducer = combineReducers(reducers);
const store = createStoreWithMiddleware(reducer);

// 注册所有页面并注入Provider
registerScreens(store, Provider);

createStore():用来生成Store applyMiddleware():用来添加各种中间件,完成store.dispatch()功能的增强 combineReducers():用来合并子Reducer Provider:Provider在根组件外面包一层,这样一来,App的所有子组件就默认都可以拿到state。


3.配置reducers和registerScreens

注意看引入的reducersregisterScreens,其两个都是对应reducers/index.jsscrens/index.js。 首先是reducers:

import app from './app/reducer';
import user from './user/reducer';
import city from './city/reducer';
//整合不同业务的子reducer
export {
  app,
  user,
  city,
};

接着是screen:

import {
  Navigation
} from 'react-native-navigation';

import Home from './Home';
import ProfileV1 from './social/ProfileV1';
...
// register all screens of the app (including internal ones)
export function registerScreens(store, Provider) {
  Navigation.registerComponent('xjs.ProfileV1', () => ProfileV1);
  Navigation.registerComponent('xjs.Home', () => Home, store, Provider);
  ...
}

整合完毕


3.如何设置state

每个页面都会有不同的状态,上一篇提到过,状态可以是更新页面的数据,也可以是本地生成尚未持久化到服务器的数据,比如请求参数,页面传参等。 下面我们来实现这个小功能,通过root的值直接更改初始化的页面。


1.添加actionType

添加一个改变root状态的常量,表示方法的定义。 actionTypes.js:

export const ROOT_CHANGED = 'xjs.app.ROOT_CHANGED';//root改变,切换页面

actionType


2.添加reducer

添加一个type是ROOT_CHANGED的判断,如果执行了ROOT_CHANGED,则将传来的值设置到全局的root变量中。

import * as types from './actionTypes';
import Immutable from 'seamless-immutable';
//Immutable.is用来减少 React 重复渲染,提高性能,在使用集合的时候

//初始化state,设置了root
const initialState = Immutable({
  root: undefined, // 'login' / 'after-login'
  ...
});
//导出app方法
export default function app(state = initialState, action = {}) {
    //如果type是ROOT_CHANGED,root改变,修改state root
  switch (action.type) {
    case types.ROOT_CHANGED:
      let {root} = action;
      return {
        ...state,
        root,
      };
   }
   case ...
}

reducer


3.添加actions

想在首页这个页面去改变root的值,那么肯定是需要一个方法的,这是最后一步,添加changeAppRoot方法,该方法可以直接使用,也可以再次进行封装。

import * as types from './actionTypes';

//改变root
export function changeAppRoot(root) {
  return {type: types.ROOT_CHANGED, root};
}

//app初始化方法
export function appInitialized() {
  return async (dispatch, getState) => {
    ...
    AsyncStorage.getItem('user').then((data) => {
      if (data) { //改变root成登录页
        if(user.nickname == '默认名'){
          dispatch(changeAppRoot('login'));
        }else{
          dispatch(changeAppRoot('after-login'));
        }
      }else{ //改变root成引导页
        dispatch(changeAppRoot('launch'));
      }
    });
  };
}

actions


4.直接在你需要去改变root的值的页面,引入该actions即可

import * as appActions from "./reducers/app/actions";

APP


4.如何获取使用state

获取state特别简单,在你需要引用的页面,通过connect注入到该页面,在该页面使用props属性能获取到注入的state。

import {connect} from 'react-redux';
class SideMenu extends Component {
  //获取天气
  let temp = this.props.city.temp || '';
  let cityName = this.props.city.name || '';
  ...
  render() {
      return(
        ...
        <Image source={{uri:this.props.user.user.avatar}}/>
        <Text>{this.props.user.user.nickname}</Text>
        ....
      );
  }
}
//注入属性到全局state 
function mapStateToProps(state) {
  return {
    user: state.user,
    city:state.city.city
  };
}
export default connect(mapStateToProps)(SideMenu);

5.解决引导页、登录页、登录页跳转问题

1. 在App.js中注册事件用来监听root的变化

import * as appActions from "./reducers/app/actions";
const store = createStoreWithMiddleware(reducer);
// 注意这里只是一个简单的类,并不是一个React组件
export default class App {
  constructor() {
    // 由于react-redux只作用于组件,我们需要手动订阅发布这些类
    //订阅:设置一个监听函数,处理root的变化
    store.subscribe(this.onStoreUpdate.bind(this));
    //发布:设置一个触发函数,执行app初始化,跳登录页面
    store.dispatch(appActions.appInitialized());
  }

  //监听root的变化
  onStoreUpdate() {
    const {root} = store.getState().app;
    // 如果你的app在运行时没有改变root,你完全可以将onStoreUpdate()删除
    if (this.currentRoot != root) {
      this.currentRoot = root;
      this.startApp(root);
    }
  }

  //根据root的值启动对应的页面,如:引导页、登录,登录后
  startApp(root) {
    switch (root) {
      case 'launch':
          //引导页
          Navigation.startSingleScreenApp({
            screen: {
              screen: 'xjs.Launch',
              title: '引导页',
              navigatorStyle: {}
            }
          });
        return;
      case 'login':
          //登录页
          Navigation.startSingleScreenApp({
            screen: {
              screen: 'xjs.Login',
              title: '登录',
              navigatorStyle: {}
            }
          });
        return;
      case 'after-login':
        //首页
        Navigation.startTabBasedApp({
          tabs:[
            {
              label: '健身',
              screen: 'xjs.Articles4',
              icon: require('../img/jingxuan_default.png'),
              selectedIcon: require('../img/jingxuan_choose.png'),
              title: '去健身'
            },
            {
              label: '教练',
              screen: 'xjs.SwipeDecker',
              icon: require('../img/boke_default.png'),
              selectedIcon: require('../img/boke_choose.png'),
              title: '找教练'
            },
            {
              label: '干货',
              screen: 'xjs.Blogposts',
              icon: require('../img/set_default.png'),
              selectedIcon: require('../img/set_choose.png'),
              title: '寻文章'
            }
          ],
          tabsStyle: {  //IOS
            tabBarSelectedButtonColor: '#000000'
          },
          appStyle: { //Android
            tabBarSelectedButtonColor: '#000000'
          },
          drawer: {
            left: {
              screen: 'xjs.SideMenu'
            },
            disableOpenGesture:true,
            style: {
              //leftDrawerWidth: 80,
              drawerShadow: 'NO'
            }
          }
        });
        return;
      default:
        console.error('Unknown app root');
    }
  }
}

2.在app/actions中导出appInitialized方法,根据不同情况来改变root的值,初始化对应的页面。

//app初始化方法
export function appInitialized() {
  return async (dispatch, getState) => {
    // 因为所有业务逻辑都应该在redux操作中
    // 这是放置你app初始化代码的好地方
    WeChat.registerApp('xxx');//注册微信
    SplashScreen.hide();//隐藏启动页
    AsyncStorage.getItem('user').then((data) => {
      if (data) { //改变root成登录页
        let user = JSON.parse(data);
        dispatch(userActions.addUser(user));
        if(user.nickname == '默认名'){
          dispatch(changeAppRoot('login'));
        }else{
          dispatch(changeAppRoot('after-login'));
        }
      }else{ //改变root成引导页
        dispatch(changeAppRoot('launch'));
      }
    });
  };
}

缓存里面没有用户对象,则跳引导页; 如果有,但是用户名是默认名,则跳登录页; 如果有,但是用户名不是默认名,则跳首页。


6.解决获取头像和昵称问题

1.在登录成功后,设置user到全局的state

post(url, params, (rst) => {
  if (rst.success && rst.data.accessToken) {
    //1.将数据保存到本地
    AsyncStorage.setItem('user', JSON.stringify(rst.data));
    //2.将数据放进state里
    this.props.dispatch(userActions.addUser(rst.data));
    Toast.hide();
    //3.跳转到首页
    this.props.dispatch(appActions.login());
  } else {
    Toast.showShortCenter(rst.msg);
  }
}, (rst) => {
  Toast.showShortCenter(rst);
});

2.在首页获取用户信息成功后,设置user到全局的state

post(url,params,(data)=>{
  if(data.success){
    //1.将数据保存到本地
    AsyncStorage.setItem('user',JSON.stringify(data.data));
    //2.将数据放进state里
    that.props.dispatch(userActions.addUser(data.data));
  }
},(e)=>{console.log(e)})

3.通过this.props.xxx获取全局的state,用于视图View的展示:

class SideMenu extends Component {
  render() {
      return(
        <Image source={{uri:this.props.user.user.avatar}}/>
        <Text>{this.props.user.user.nickname}</Text>
      );
  }
}
Headshot of Maxi Ferreira

怀着敬畏之心,做好每一件事。