STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

React-Redux基本用法

React Redex

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


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

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

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

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


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

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

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

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

1
2
3
4
5
6
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

1
2
3
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
9
import app from './app/reducer';
import user from './user/reducer';
import city from './city/reducer';
//整合不同业务的子reducer
export {
app,
user,
city,
};

接着是screen:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

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

actionType


2.添加reducer

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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方法,该方法可以直接使用,也可以再次进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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即可

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

APP


4.如何获取使用state

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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的值,初始化对应的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
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的展示:

1
2
3
4
5
6
7
8
class SideMenu extends Component {
render() {
return(
<Image source={{uri:this.props.user.user.avatar}}/>
<Text>{this.props.user.user.nickname}</Text>
);
}
}