1. 闲扯一发
作为一个技术小宅男,周末还是适合待在家里,总结总结在前端路上遇到过的各种技术。今天呢,咱接着上周的话题,继续扯扯react-native相关的事情。在react-native的开发中,感觉最难管理,同时也最需要管理的就是各种state和router了。截止目前为止,Github上最火的state管理库非redux莫属,至于路由管理的话,还是喜欢官方推荐的由社区开发和维护的react-navigation,接下来呢,咱就一起来扯扯他俩的集成。
2. redux
redux是什么,简单点来说呢,就是一个管理state的库,但咱不能这么随便啊,还是把官网的定义给搬过来吧。
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供 超爽的开发体验,Redux 除了和 React 一起用外,还支持其它界面库。它体小精悍(只有2kB,包括依赖)。
。。。
2.1 redux的三项基本原则
- 单一数据源:整个应用的state被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个store。
- State 是只读的:惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
- 使用纯函数来执行修改:为了描述 action 如何改变 state tree ,需要编写 reducer。
2.2 Action
action 是把数据从应用传到store的有效载荷,是store数据的唯一来源。action为一个普通对象,这里建议按照Flux 标准 Action来定义。action 一般具有下面四个属性
- type: 用户动作的唯一标识符
- payload: action相关的数据,一般来自于后端接口,用于更新store,在出错的情况下为错误对象
- error 出错的情况下为true, 其他情况不需要提供
- meta 可选参数,其他可能需要的额外数据,不算作payload的一部分
// 正常
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
},
meta: {
searchParms: {}
}
}
//出错
{
type: 'ADD_TODO',
error: true,
payload: new Error('error object'),
meta: {
searchParms: {}
}
}
2.3 Action Creator
Action Creator就是生成 action 的方法,一般将action creator 的执行结果传递给 dispatch. 为了便于管理,这里建议使用第三方库redux-actions. 该库采用了Flux 标准的action定义。
import { createAction } from 'redux-actions';
let increment = createAction('INCREMENT', amount => amount);
// same as
increment = createAction('INCREMENT');
expect(increment(42)).to.deep.equal({
type: 'INCREMENT',
payload: 42
});
2.4 Reducer
Action只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。这里呢,我还是建议采用redux-actions 来生成 reducer.
import { handleAction } from 'redux-actions';
var reducer = handleAction('FETCH_DATA', {
next(state, action) {...},
throw(state, action) {...}
}, defaultState);
2.5 Store
Store 就是把action和reducer联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供 getState方法获取 state;
- 提供 dispatch方法更新 state;
- 通过 subscribe注册监听器;
- 通过 subscribe返回的函数注销监听器。
创建store的时候,我们使用 combineReducers 将多个 reducer 合并成为一个,并传递给 createStore 方法。
2.6 数据流
1 用户交互触发actionCreator的执行生成action
2 调用store的dispatch方法,触发reducer的执行
3 根据传入的action调用对应的reducer,更新store
4 触发组件props的改变从而引起组件的更新
redux的具体细节, 可以参考开发文档。
3. React Navigation
React Navigation 是react native社区主推的一个导航器方案,Facebook官方建议使用其来进行导航的维护。
React Navigation的路由写法使其非常容易扩展导航逻辑,或是整合到redux中。由于路由可以嵌套使用,因而开发者可以根据不同页面编写不同的导航逻辑,且彼此互不影响。
具体的细节,这里我就不做特别的阐述了,大家可以直接阅读官方文档。
4 redux与react-navigation的集成
前面我们已经介绍了redux和react-navigation的基本知识,接下来,就进入我们今天的主题,实现redux与react-navigation的集成。
4.1 效果预览
4.2 整体目录
在项目根目录下建立src文件夹用于存放各种scripts和styles代码。
- scripts 存放和应用相关的js代码
- less 存放less文件
- styles 根据less文件自动生成,可以参考react native样式分离
scripts目录具体细节如下
- actions 用于存放redux的action creator 和 action type
- reducers 用于存放所有的reducer
- store 用于存放redux中的store
- components 所有的业务组件都躲在这儿,哈哈
- routers 和导航相关的路由配置
- Root.js 应用的根组件
4.3 Actions
在actions目录下建立两个文件夹actionTypes和news。actionTypes用于存放action的type, news用于存放和news相关的action creator。
//actionTypes
export const FETCH_NEWS_LIST = 'FETCH_NEWS_LIST';
//news action creator
import {
FETCH_NEWS_LIST
} from '../actionTypes';
import { createAction } from 'redux-actions';
const thumbnail = 'https://facebook.github.io/react/img/logo_og.png';
// 获取news 列表数据
export var fetchNewsList = createAction(FETCH_NEWS_LIST, () => {
return [1,2,3,4,5,6,7,8,9,10].map(item => {
return {
id: item,
title: `[${item}]夏季又要到,做好防脱准备很重要`,
thumbnail: thumbnail,
desc: '室内干燥,室外气温高,春夏季对于需要带状的上班族来说就是压力山大。'
}
});
});
4.4 Reducer
在reducers目录下建立一个news文件夹,用于存放和news相关的reducer。
/**
* 这里以news的为例,还有其他的reducer,具体的可以参考样例完整代码
*/
import {
FETCH_NEWS_LIST
} from '../../actions/actionTypes';
import { handleActions } from 'redux-actions';
export default handleActions({
[FETCH_NEWS_LIST]: {
next(state, action) {
return { ret: true, data: action.payload };
},
throw(state, action) {
return { ret: false, statusText: action.payload, data: [] };
}
}
}, { ret: true, statusText: '', data: [] });
接下来在reducers目录下建立一个index.js文件,用于组合各种reducer
import { combineReducers } from 'redux';
import news from './news';
import categries from './category';
//和导航相关的reducer通过从调用出传递进来
export default function getReducers(navReducer) {
return combineReducers({
news,
categries,
nav: navReducer
});
}
4.5 Store
在store文件夹下建立一个index.js文件,用于存放store相关的数据
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import getReducers from '../reducers';
//promiseMiddleware 是异步action的一个中间件,本例子中暂时没有使用
export default function getStore(navReducer) {
return createStore(
getReducers(navReducer),
undefined,
applyMiddleware(promiseMiddleware)
);
}
4.6 components
在components目录下建立Home文件夹,用于存放首页的业务组件,除了首页之外,还提供以下业务组件。
- Category 分类组件
- Card 购物车组件
- UserCenter 用户中心
- NewsDetail 新闻详情
接下来,我们看看Home的代码
import React, { Component } from 'react';
import Icon from 'react-native-vector-icons/Ionicons';
import {
View,
Text,
Image,
FlatList,
StatusBar,
TouchableHighlight
} from 'react-native';
import { common, topicCard } from '../../../styles';
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import * as newsActions from '../../actions/news';
class Home extends Component {
//定义底部tabBar的icon和name
static navigationOptions = {
tabBarLabel: '首页',
tabBarIcon: ({ tintColor }) => (
<Icon name="md-home" size={24} color={tintColor} />
)
};
componentDidMount() {
this.props.fetchNewsList();
}
_onPressItem = (id) => {
this.props.navigation.navigate('NewsDetail', { id: id });
};
_keyExtractor = (item, index) => item.id;
_renderItem = ({item}) => (
<View style={topicCard.card}>
<Image style={topicCard.cover} source={{uri: item.thumbnail}}></Image>
<TouchableHighlight
style={topicCard.title}
onPress={this._onPressItem.bind(this, item.id)}>
<Text numberOfLines={1} style={topicCard.titleText}>{item.title}</Text>
</TouchableHighlight>
<View style={topicCard.desc}>
<Text numberOfLines={2} style={topicCard.descText}>{item.desc}</Text>
</View>
</View>
);
render() {
return (
<View style={[common.containerFixHeight, common.springWoodBg]}>
<FlatList
data={this.props.news.data}
refreshing={false}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem}
/>
</View>
);
}
}
//让业务组件和redux建立关联
export default connect(
state => ({
news: state.news
}),
dispatch => bindActionCreators(newsActions, dispatch)
)(Home);
在代码的结尾处,我们调用了react-redux的connect方法,让store, action 和 业务组件建立了关联,之后我们就可以在业务组件中通过this.props.news的方式来访问news相关的数据,以this.props.fetchNewsList() 的方式来触发请求news数据。
4.7 路由配置
和路由相关的配置,存放在routers文件夹里
import {
TabNavigator,
StackNavigator,
addNavigationHelpers
} from "react-navigation";
import Home from '../components/Home';
import Category from '../components/Category';
import Card from '../components/Card';
import UserCenter from '../components/UserCenter';
import NewsDetail from '../components/NewsDetail';
//底部的tabBar导航
const TabbarNavigator = TabNavigator({
Home: { screen: Home },
Category: { screen: Category },
Card: { screen: Card },
UserCenter: { screen: UserCenter }
}, {
initialRouteName: 'Home'
});
//整个应用的路由栈
const AppNavigator = StackNavigator({
TabBar: {
screen: TabbarNavigator,
navigationOptions: {
header: null
}
},
NewsDetail: {
path: 'news/:id',
screen: NewsDetail
}
});
export {
AppNavigator
};
4.8 根组件
在上面的一切准备就绪之后,我们需要建立一个Root组件来实现redux,react-navigation以及业务组件的衔接。
import React, { Component } from "react";
import { Provider, connect } from "react-redux";
import { addNavigationHelpers } from "react-navigation";
import getStore from "./store";
import { AppNavigator } from './routers';
const navReducer = (state, action) => {
const newState = AppNavigator.router.getStateForAction(action, state);
return newState || state;
};
const mapStateToProps = (state) => ({
nav: state.nav
});
class App extends Component {
render() {
return (
<AppNavigator
navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav
})}
/>
);
}
}
const AppWithNavigationState = connect(mapStateToProps)(App);
const store = getStore(navReducer);
export default function Root() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
接下来,我们只需要将根组件引入到整个应用的入口文件就ok啦,完美。
import {
AppRegistry,
} from 'react-native';
import Root from './src/scripts/Root.js';
AppRegistry.registerComponent('reduxReactNavigation', () => Root);
5. 总结一发
至此,所有的介绍结束了,感谢你的耐心阅读,如果你想要了解更多,你可以直接查看完整的demo,也可以留言提issue。
网友评论
BackHandler.addEventListener('hardwareBackPress', this.onBackAndroid);
}
componentUnWillMount(){
BackHandler.removeEventListener('hardwareBackPress', this.onBackAndroid);
lastBackPressed = null;
}
onBackAndroid=()=>{
console.log(this.props.navigation.state)
if(this.props.navigation.state&&this.props.navigation.state.routeName&&this.props.navigation.state.routeName != "Index") {
this.props.navigation.goBack();
return true;
}
if (lastBackPressed && lastBackPressed + 2000 >= Date.now()) {
return false;
}
lastBackPressed = Date.now();
ToastAndroid.show('再点击一次退出应用',ToastAndroid.SHORT);
return true;
}