上一篇讲解了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 npm install npm install
|
为什么使用react-redux?
React专用的Redux库,能提供Provider组件和connect方法。
为什么使用react-thunk?
store.dispatch方法的增强,正常情况下,参数只能是对象,不能是函数。而使用react-thunk后store.dispatch参数可以是函数
。
2.配置App.js
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"; ...
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); const reducer = combineReducers(reducers); const store = createStoreWithMiddleware(reducer);
registerScreens(store, Provider);
|
createStore():用来生成Store
applyMiddleware():用来添加各种中间件,完成store.dispatch()功能的增强
combineReducers():用来合并子Reducer
Provider:Provider在根组件外面包一层,这样一来,App的所有子组件就默认都可以拿到state。
3.配置reducers和registerScreens
注意看引入的reducers
和registerScreens
,其两个都是对应reducers/index.js
和screns/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';
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';
|
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';
const initialState = Immutable({ root: undefined, ... });
export default function app(state = initialState, action = {}) { switch (action.type) { case types.ROOT_CHANGED: let {root} = action; return { ...state, root, }; } case ... }
|
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';
export function changeAppRoot(root) { return {type: types.ROOT_CHANGED, root}; }
export function appInitialized() { return async (dispatch, getState) => { ... AsyncStorage.getItem('user').then((data) => { if (data) { if(user.nickname == '默认名'){ dispatch(changeAppRoot('login')); }else{ dispatch(changeAppRoot('after-login')); } }else{ dispatch(changeAppRoot('launch')); } }); }; }
|
4.直接在你需要去改变root的值的页面,引入该actions即可
1 2
| import * as appActions from "./reducers/app/actions";
|
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);
export default class App { constructor() { store.subscribe(this.onStoreUpdate.bind(this)); store.dispatch(appActions.appInitialized()); }
onStoreUpdate() { const {root} = store.getState().app; if (this.currentRoot != root) { this.currentRoot = root; this.startApp(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: { tabBarSelectedButtonColor: '#000000' }, appStyle: { tabBarSelectedButtonColor: '#000000' }, drawer: { left: { screen: 'xjs.SideMenu' }, disableOpenGesture:true, style: { 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
| export function appInitialized() { return async (dispatch, getState) => { WeChat.registerApp('xxx'); SplashScreen.hide(); AsyncStorage.getItem('user').then((data) => { if (data) { let user = JSON.parse(data); dispatch(userActions.addUser(user)); if(user.nickname == '默认名'){ dispatch(changeAppRoot('login')); }else{ dispatch(changeAppRoot('after-login')); } }else{ 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) { AsyncStorage.setItem('user', JSON.stringify(rst.data)); this.props.dispatch(userActions.addUser(rst.data)); Toast.hide(); 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){ AsyncStorage.setItem('user',JSON.stringify(data.data)); 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> ); } }
|