为App根组件包裹Provider
组件,以便整个项目能使用Redux和React-Redux
上一篇文章中,我们说到为了能让项目中所有的容器组件都能顺利拿到应用State,React-Redux规定我们要在应用根组件外面包一层Provider
组件。
我们需要在项目中导入一个redux-helpers的三方库。
// AppContainer.js
import React, {Component} from 'react';
import {createAppContainer} from "react-navigation";
import SwitchNavigator from './SwitchNavigator';
import {createReactNavigationReduxMiddleware, createReduxContainer} from 'react-navigation-redux-helpers';
import {connect} from 'react-redux';
// 不使用Redux之前,我们是直接导出AppContainer,现在就不直接导出它了,而是给它包裹几层再到出去
const AppContainer = createAppContainer(SwitchNavigator);
{/* 第1步,使用react-navigation-redux-helpers,给AppContainer包裹一层ReduxContainer,下面都是固定写法,直接复制就行*/}
// 1.1 初始化react-navigation与redux的中间件
const reactNavigationReduxMiddleware = createReactNavigationReduxMiddleware(// 返回一个可用于store的middleware
// 读取应用state的nav
state => state.navigatorState,
// 这个参数对于Redux Store来说必须是唯一的,并且与createReduxContainer下面的调用一致。
'root',
);
// 1.2 把根组件先包装一层
const AppContainer_ReduxContainer = createReduxContainer(// 返回包装根导航器的高阶组件,用于代替根导航器的组件
// 我们的根导航器
AppContainer,
// 和上面createReactNavigationReduxMiddleware设置的key一致
'root',
);
{/* 第2步,使用React-Redux,创建AppContainer_ReduxContainer的容器组件,即我们最终要使用的根组件 */}
// 2.1 编写UI组件
// 即AppContainer_ReduxContainer
// 2.2 建立UI组件的props与外界的映射关系
// 你可能会问“这个UI组件是什么添加上这些props的呢”?没错,正是这个添加映射关系的过程为该UI组件添加上了这些props
// 输入逻辑类属性要和应用state里该组件state里的属性建立映射关系
function mapStateToProps(state) {
return {
state: state.navigatorState,
}
}
// 输出逻辑类属性要和dispatch(addAction)建立映射关系
function mapDispatchToProps(dispatch) {
return {
}
}
// 2.3 使用connect方法生成UI组件对应的容器组件
// 将来我们就是使用UI组件的容器组件了,而不是使用UI组件
const AppContainer_ReduxContainer_Container = connect(
mapStateToProps,
// mapDispatchToProps,
)(AppContainer_ReduxContainer);
export {AppContainer_ReduxContainer_Container, reactNavigationReduxMiddleware};
// App.js
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import {AppContainer_ReduxContainer_Container} from './js/Navigator/AppContainer';
import store from './js/store/store';
import {Provider} from 'react-redux';
export default class App extends Component {
render() {
return (
// 2.4 在根组件外面包一层Provider组件,记得一定要把store={store}传递进去,
// 这个一定要在App.js里包裹,不要想着在这里包裹完直接供外界使用了,否则因为界面的加载有先后顺序,可能会导致意外的问题
<Provider store={store}>
<AppContainer_ReduxContainer_Container/>
</Provider>
);
}
}
// store.js
import {createStore, applyMiddleware} from 'redux';
import rootReducer from '../reducer/rootReducer';
import {reactNavigationReduxMiddleware} from '../Navigator/AppContainer';
import thunk from 'redux-thunk';
import {createLogger} from 'redux-logger';
// 中间件
const logger = createLogger();
const middlewares = [
reactNavigationReduxMiddleware,
thunk,
logger,// logger一定要放在最后面
];
// 第1步:
// 创建项目唯一的store,发现需要一个reducer
// 所以接下来第2步,我们去创建一个reducer,回过头来填在这里,详见rootReducer.js文件
const store = createStore(rootReducer, applyMiddleware(...middlewares));
export default store;
更换主题色预期效果
行动前的考虑
前面的文章有说过适合使用Redux和React-Redux的场景,现在我们再列一下。
- 某个组件的
state
,需要共享- 一个组件需要改变另一个组件的
state
- 某个
state
,需要在任何地方都能拿到——即它应该是一个全局state
- 一个组件需要改变某个全局
state
现在我们要编写的更换主题色,就是这样的场景:更换主题色界面的某些操作会改变某个全局state
——即App的主题色,而App的主题色这个全局state
需要在App的各个组件内都能被拿到。因此我们考虑使用Redux和React-Redux来实现该功能。
当然要完整地实现该功能,需要考虑的东西确实比较多,所以一开始如果我们想不到那么多、那么远的话,就从最熟悉、最能上手的地方开始做吧,做一步思考一步,慢慢地也就做完了。现在我们先搭建更换主题色界面,并实现界面交互,这是最容易想到并实现的一步了。
搭建更换主题色界面,并实现界面交互
首先我们创建一个文件来专门存放App所有可更换的主题色。
-----------AllThemeColor.js-----------
const AllThemeColor = {
Default: '#4caf50',
Red: '#F44336',
Pink: '#E91E63',
Purple: '#9C27B0',
DeepPurple: '#673AB7',
Indigo: '#3F51B5',
Blue: '#2196F3',
LightBlue: '#03A9F4',
Cyan: '#00BCD4',
Teal: '#009688',
Green: '#4CAF50',
LightGreen: '#8BC34A',
Lime: '#CDDC39',
Yellow: '#FFEB3B',
Amber: '#FFC107',
Orange: '#FF9800',
DeepOrange: '#FF5722',
Brown: '#795548',
Grey: '#9E9E9E',
BlueGrey: '#607D8B',
Black: '#000000'
};
export default AllThemeColor;
接下来我们搭建更换主题色界面,项目里要求的是更换主题色界面是模态出来的。但是RN有个毛病,如果使用它的navigate
方法来做模态,就得在路由配置的一开始就设置mode: 'modal'
,但是这会导致该navigator
下所有的路由都是模态效果,而不是某个单独的路由是模态效果,因此我们得使用Modal组件来实现单个界面的模态效果。
-----------ChangeThemeColorPage.js-----------
import React, {Component} from 'react';
import {Platform, StyleSheet, View, Modal, FlatList} from 'react-native';
import AllThemeColor from '../../../Const/AllThemeColor';
import ProjectNavigationBar from '../../../View/Other/ProjectNavigationBar';
import ChangeThemeColorCell from '../../../View/My/ChangeThemeColor/ChangeThemeColorCell';
// 获取所有主题色的key,组成颜色数组
const allThemeColorArray = Object.keys(AllThemeColor);
export default class ChangeThemeColorPage extends Component {
// 这里你就感受到了,如果一个组件的state只是它自己使用,不为别人所使用的,就不用放在应用State里,那样反而使得编码复杂
constructor(props) {
super(props);
this.state = {
visible: false,
}
}
render() {
return (
<Modal// Modal组件其实是那个背景层
// 背景层是否显示,即是否显示这个界面
visible={this.state.visible}
// 背景层是否透明
transparent={true}
// 背景层显示和消失时的动画效果
animationType={'slide'}
// 在安卓上用户按下设备的后退按键时触发,该属性在安卓设备上为必填,且会在modal处于开启状态时阻止BackHandler事件
onRequestClose={() => {
this.dismiss();
}}
>
<ProjectNavigationBar/>
<View style={{backgroundColor: 'white', flex: 1}}>
<View style={styles.container}>
<FlatList// 整个界面用FlatList来实现九宫格的效果
style={styles.flatList}
data={allThemeColorArray}
renderItem={({item, index}) => this._renderItem({item, index})}
keyExtractor={(item) => item.key}
showsVerticalScrollIndicator={false}
numColumns={3}// 显示三列
/>
</View>
</View>
</Modal>
);
}
// 显示该界面
show() {
this.setState({
visible: true,
})
}
// 消失该界面
dismiss() {
this.setState({
visible: false,
})
}
_renderItem({item, index}) {
return (
<ChangeThemeColorCell
dataDict={{colorKey: item, color: AllThemeColor[item]}}
didSelectThemeColor={() => {
// 该界面消失
this.dismiss();
}}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
margin: 10,
marginTop: Platform.OS === 'ios' ? -44 : -50,
padding: 3,
borderRadius: 4,
shadowColor: 'gray',
shadowOffset: {width: 2, height: 2},
shadowOpacity: 0.5,
shadowRadius: 2,
},
flatList: {
flex: 1,
backgroundColor: 'white',
},
});
界面中用到了ChangeThemeColorCell
,如下。
-----------ChangeThemeColorCell.js-----------
import React, {Component} from 'react';
import {StyleSheet, Text, TouchableOpacity} from 'react-native';
export default class ChangeThemeColorCell extends Component {
render() {
// 这个读取数据要写在这里,写在constructor里是不行的,因为数据改变后重新渲染时只走render方法,写在上面就无法读取到最新的数据
this.dataDict = this.props.dataDict;
if (!this.dataDict) return null;
return (
<TouchableOpacity
style={[styles.container, {backgroundColor: this.dataDict.color}]}
// 暴露一个回调出去
onPress={() => this.props.didSelectThemeColor()}
>
<Text style={styles.text}>{this.dataDict.colorKey}</Text>
</TouchableOpacity>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
height: 120,
justifyContent: 'center',
alignItems: 'center',
margin: 3,
},
text: {
color: 'white',
fontWeight: '400',
},
});
然后在MyPage
的render()
方法里使用该界面。
<ChangeThemeColorPage
ref={changeThemeColorPage => this.changeThemeColorPage = changeThemeColorPage}
/>
在MyPage
里点击相应的cell
,模态出该界面就可以了。
this.changeThemeColorPage.show();
这样更换主题色界面就搭好了,而且界面也可以顺利的模态出来和消失掉。接下来我们还是先不考虑更换主题色这个效果,而是先考虑该功能里别的需要做的,因为那些比较简单一些。于是自然的就考虑到,点击更换主题色界面的每个cell
后,是需要把主题色持久化的,这样下次打开App时才能显示相应的主题色。因此接下来,我们做主题色持久化的部分。
持久化主题色
这里我们写了一个专门针对持久化主题色写入与读取的工具类,Dao
的意思是数据访问对象,而更换主题色也仅仅是App更换主题的一种(比如我们还可以更换主题图标),为了方便以后可能有的扩展,我们给这个工具类起名字为ThemeDao.js
,而不是ThemeColorDao.js
。
-----------ThemeDao.js-----------
/**
* AsyncStorage和NSUserDefaults是一样的,它们的写入和读取操作都是异步的
*/
import {AsyncStorage} from "react-native";
import AllThemeColor from "../../Const/AllThemeColor";
// 持久化时的key
const THEME_COLOR = 'themeColor';
export default class ThemeDao {
/**
* 存储主题色
* @param themeColor 本身就是个颜色字符串,所以可以直接存储
*/
static saveThemeColor(themeColor) {
AsyncStorage.setItem(THEME_COLOR, themeColor, error => {
if (error) {
console.log('更换主题色,写入数据出错:', error);
}
});
}
/**
* 读取主题色:因为读取主题色是异步操作,所以我们得用Promise把读取结果给它传出去
* @returns {Promise<any> | Promise}
*/
static getThemeColor() {
return new Promise((resolve, reject) => {
AsyncStorage.getItem(THEME_COLOR, (error, value) => {
if (error) {
reject(error);
} else {
if (!value) {// 数据库中还没有存主题色
// 那就搞个默认的主题色
value = AllThemeColor.Default;
// 存起来
this.saveThemeColor(value);
}
// 传出去
resolve(value);
}
});
});
}
}
然后我们返回到ChangeThemeColorPage
界面为点击cell
更换主题色那里添加一下持久化主题色的操作。
_renderItem({item, index}) {
return (
<ChangeThemeColorCell
dataDict={{colorKey: item, color: AllThemeColor[item]}}
didSelectThemeColor={() => {
// 持久化该主题色
ThemeDao.saveThemeColor(AllThemeColor[item]);
// 该界面消失
this.dismiss();
}}
/>
);
}
这样持久化主题色就做完了,接下来就该考虑大部头的内容了——即点击cell
的时候真正的改变一下App的主题色,于是就该Redux和React-Redux部分了。
Redux部分
第一步:确定该功能块该有哪些action
我们考虑,更换主题色只需要在更换主题色成功后,发出一个“更换主题色成功”的action
,让App中所有订阅了该功能块State的组件都重新渲染一下就可以了。
所以,我们只需要为这个功能块提供一个action
。
-----------type.js-----------
/**
* 多数情况下,action的type会被定义成字符串常量,放在单独的文件里,方便管理
*/
export default {
// 更换主题色
THEME_COLOR_DID_CHANGE: 'THEME_COLOR_DID_CHANGE',
}
编写具体的action
生成器和action
,即点击cell
更换主题色时,dispatch
出用这个生成器生成的action
就可以了。
-----------ThemeAction.js-----------
import Type from './type';
export function changeThemeColor(themeColor) {
return {type: Type.THEME_COLOR_DID_CHANGE, themeColor: themeColor};
}
然后在rootAction
里导入并导出一下这个action
。
-----------rootAction.js-----------
/**
* 项目的根aciton
*
* 因为项目中可能有很多action,所以我们统一到这个地方,外界导入使用的时候就方便了
*/
// 导入所有的Action Creator
import {changeThemeColor} from './ThemeColorAction';
// 再导出
export default {
// 更换主题色
changeThemeColor,
}
第二步:编写该功能块的reducer
,并设计该功能块State的数据结构
考虑到该功能块State只需要向外提供一个主题色,所以我们设置它的数据结构如下,并且先不给它默认值,看后面什么情况再说。
themeState = {
themeColor: 'red',
};
在reducer
收到改变主题色的action
之后,把该功能块State的主题色属性改变为最新的主题色就可以了,该功能块State一变化,那么但凡订阅了该功能块State的组件就都能收到回调触发重新渲染。
-----------themeReducer.js-----------
const defaultState = {};
const themeColorReducer = (state = defaultState, action) => {
switch (action.type) {
case Type.THEME_COLOR_DID_CHANGE:
return {
...state,
themeColor: action.themeColor,
};
default:
return state;
}
};
export default themeColorReducer;
在根reducer
里合并一下这个功能块reducer
。
-----------rootReducer.js-----------
/**
* 项目的根reducer
*
* 1、因为创建store的时候只能填写一个reducer,而项目中通常会有很多reducer,
* 所以我们就专门创建了这个reducer专门用来合并其它所有的子reducer,它不负责应用state具体的变化,只负责合并操作
* 我们把它称为根reducer了,供创建store时使用
*
* 2、请注意:
* 这个根reducer是个极其重要的东西,因为正是它合并子reducer的过程,决定了应用state里到底存放着什么东西,即什么组件的state要被放在它里面,
* 什么组件的state想要交由应用的state来统一管理,我们就为该组件编写一个对应的子reducer来描述它state具体变化的过程并返回一个state,然后把这个reducer作为value存放在应用state里(即合并子reducer的时候)
* 刚创建根reducer时,我们可能不知道将来会有那些组件的state会被放在应用state里来统一管理,所以可以先空着,什么时候需要什么时候往这里添加就可以。
*/
import {combineReducers} from 'redux';
import themeReducer from "./themeReducer.js";
// 创建根reducer,合并所有子reducer(为了方便管理,我们会把子reduce分别写在单独的文件里)
const rootReducer = combineReducers({// 这个对象就是应用的state
// 应用state里的属性名当然可以随便起,但是为了好理解,我们就起做xxxState,为什么这么起呢?
// 因为应用state的定义就是,它里面存放着项目中所有想被统一管理state的组件的state,所以我们起做xxxState,将来使用时很方便理解,比如state.counterState,就代表从应用state里取出counterState
// 而且它的值就是对应的该组件的那个子reducer嘛,而reducer函数又总是返回一个state,这样xxxState = 某个state值,也很好理解
themeState: themeReducer,
});
export default rootReducer;
这样更换主题色这个功能的Redux部分我们就编写完成了,接下来编写React-Redux部分。
React-Redux部分
因为是在ChangeThemeColorPage
里面改变App的主题色,所以它肯定是要用Redux
的,那我们就以ChangeThemeColorPage
为例,看下该功能的React-Redux部分怎么编写。
第一步:在原先ChangeThemeColorPage
(即UI组件)的基础上,用connect包裹UI组件,搞好容器组件和应用State、dispatch(action)的映射关系。
function mapStateToProps(state) {
return {
}
}
function mapDispatchToProps(dispatch) {
return {
changeThemeColor: (themeColor) => dispatch(Action.changeThemeColor(themeColor)),
}
}
const ChangeThemeColorPageContainer = connect(
mapStateToProps,
mapDispatchToProps,
null,
// 注意:这里千万要写上这句话,否则用了Redux后的组件是无法使用ref获取该组件的
{forwardRef: true}
)(ChangeThemeColorPage);
export default ChangeThemeColorPageContainer;
第二步:在需要发出action
的地方,通过调用一下props
里changeThemeColor
方法(本质就是发出一个action
,因为调用方法和发出action
建立了映射关系)就可以了,这里我们再次修改下点击cell
修改主题色的方法。
_renderItem({item, index}) {
return (
<ChangeThemeColorCell
dataDict={{colorKey: item, color: AllThemeColor[item]}}
didSelectThemeColor={() => {
// 修改主题色
this.props.changeThemeColor(AllThemeColor[item]);
// 持久化该主题色
ThemeDao.saveThemeColor(AllThemeColor[item]);
// 该界面消失
this.dismiss();
}}
/>
);
}
经过以上操作,ChangeThemeColorPage
的React-Redux部分就编写完毕了,很简单吧!这是我们点击cell,其实已经可以完成主题色的更换了,但是因为没有别的组件订阅更换主题色功能块State,所以我们看不出变化,那接下来我们就以ProjectNavigationBar
为例,看看App中其它的组件怎么来相应主题色的更换的。
其实和ChangeThemeColorPage
一样的,我们也只需要对ProjectNavigationBar
做React-Redux的部分。在ProjectNavigationBar
(即UI组件)的基础上,用connect包裹UI组件,搞好容器组件和应用State、dispatch(action)的映射关系。
function mapStateToProps(state) {
return {
themeState: state.themeState,
}
}
function mapDispatchToProps(dispatch) {
return {
}
}
const ProjectNavigationBarContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(ProjectNavigationBar);
export default ProjectNavigationBarContainer;
上面代码中,因为ProjectNavigationBar
只需要读取ThemeState的主题色,而不需要修改ThemeState的主题色,所以它只需要实现mapStateToProps
函数就可以了,mapDispatchToProps
空着就行,这正好和ChangeThemeColorPage
相反。
接下来我们只需要在render()
方法里,设置导航栏的背景色从映射的ThemeState里读取就可以了,很简单吧。
return (
// 注意:this.props.style是外界传进来的style,一定要放在styles.container我们内部定义的style后面,否则外面设置的覆盖不了前面的,用户设置的就没效果了
<View style={[styles.container, this.props.style, {backgroundColor: AllThemeColor[this.props.themeColorState.themeColor]}]}>
{/* 状态栏 */}
{statusBar}
{/* 导航栏 */}
{navigationBar}
</View>
);
只要做到这一步,你就可以实现切换主题色,导航栏实时地跟着变化了,可以去试一试。
补充初始化主题色的功能
做完了上面的步骤,更换主题色时没什么的问题了,但是更换后,每次重新打开App,我们还没有去读取持久化的主题色呢,不读导航栏也是一片白。
那我们第一眼的考虑是,是不是可以把持久化的主题色读取出来赋值给themeReducer.js
里的defaultState
,想法很好,但是没法实现,因为AsyncStorage
的读取操作是异步的,根本没法给defaultState
赋值,themeReducer.js
界面里的代码就执行完了。因此,我们只能为初始化主题色功能新增一个action
,文件变化依次如下。
-----------type.js-----------
/**
* 多数情况下,action的type会被定义成字符串常量,放在单独的文件里,方便管理
*/
export default {
// 初始化主题色
INIT_THEME_COLOR: 'INIT_THEME_COLOR',
// 更换主题色
THEME_COLOR_DID_CHANGE: 'THEME_COLOR_DID_CHANGE',
}
-----------ThemeAction.js-----------
import Type from './type';
import ThemeDao from "../expand/dao/ThemeDao";
export function changeThemeColor(themeColor) {
return {type: Type.THEME_COLOR_DID_CHANGE, themeColor: themeColor};
}
export function initThemeColor() {
return dispatch => {
ThemeDao.getThemeColor()
.then(themeColor => {
dispatch({type: Type.INIT_THEME_COLOR, themeColor: themeColor});
})
}
}
-----------rootAction.js-----------
/**
* 项目的根aciton
*
* 因为项目中可能有很多action,所以我们统一到这个地方,外界导入使用的时候就方便了
*/
// 导入所有的Action Creator
import {initThemeColor, changeThemeColor} from './ThemeAction';
// 再导出
export default {
// 初始化主题
initThemeColor,
// 更换主题色
changeThemeColor,
}
-----------themeReducer.js-----------
import Type from '../action/type';
const defaultState = {};
const themeReducer = (state = defaultState, action) => {
switch (action.type) {
case Type.THEME_COLOR_DID_CHANGE:
return {
...state,
themeColor: action.themeColor,
};
case Type.INIT_THEME_COLOR:
return {
...state,
themeColor: action.themeColor,
};
default:
return state;
}
};
export default themeReducer;
这就补充完毕了。接下来,我们考虑应该在哪里发出初始化主题色这个action
呢?越早越好吧!在我们项目中,组件的加载顺序为AppContainer.js
、SwitchNavigator.js
、StackNavigator.js
、DynamicBottomNavigator.js
,因为前三个都没有做Redux,因为主题色变了它们也不需要跟着变,而DynamicBottomNavigator.js
(即TabBar)它正好需要订阅ThemeState,所以我们索性就在它这儿初始化主题色吧,也犯不着专门去前三个组件里初始化了。
class TabBarComponent extends Component<Props> {
render() {
return (
<BottomTabBar
{...this.props}
// BottomTabBar选中时的颜色
activeTintColor={this.props.themeState.themeColor}
/>
);
}
componentDidMount() {
// 初始化主题色
this.props.initThemeColor();
}
}
function mapStateToProps(state) {
return {
themeState: state.themeState,
}
}
function mapDispatchToProps(dispatch) {
return {
initThemeColor: () => dispatch(Action.initThemeColor()),
}
}
const TabBarComponentContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(TabBarComponent);
这样,主题色就初始化好了,TabBar也能响应主题色的变化了。
额外
至此使用Redux和React-Redux实现更换主题色的功能就基本实现了,接下来就是让App中该订阅ThemeState的组件使用React-Redux订阅订阅就行了,这里额外提醒一点App中有那么多组件都需要相应主题色的变化,那我们是不是都让它们订阅呢?这样可以,但是没必要,因为每个组件都订阅的话,项目确实显得有点臃肿。我们推荐的做法是:
- 导航栏和TabBar自己订阅,因为它们要及时地刷新成最新颜色。
- App第一级界面自己订阅,因为它们要及时地刷新成最新颜色,而二级、三级界面则通过第一级界面给他们传递过去。
- 父组件自己订阅,因为它们要及时地刷新成最新颜色,而子组件则通过父组件给他们传递过去。
- 当然,针对二级、三级界面、子组件,我们也可以不通过传递的方式做,搞个单例就可以了嘛!
此外,项目中的自定义标签、标签排序、自定义语言、语言排序功能也都是通过Redux和React-Redux实现的,原理同上。