公司打算用
react-native
开发APP,初始RN遇到了很多坑,搭建了一个小的项目框架,结合redux
根据公司现成的接口写了几个小demo,接触前端半年多,之前是做iOS的,感觉转起来很困难,希望有大神可以给出些优化的建议!
项目介绍
- 这是一个利用
react-native
配合redux
开发的小框架。 - 项目主要结构
-
android
安卓原生代码 -
ios
iOS原生代码 -
src
RN代码-
actions
处理事件,是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。 -
components
子组件 -
constants
常量,actions
中用到的状态,action
内必须使用一个字符串类型的type
字段来表示将要执行的动作。多数情况下,type
会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放action
。 -
containers
父组件,类似于MVC中的控制器, -
reducers
改变状态,action
描述事件发生,通过reducers
改变状态state
-
statics
资源(例如图片) -
utils
工具方法 -
store
统一管理状态 -
index.html
html加载DOM -
root.js
RN入口
-
-
index.android.js
安卓程序的入口 -
index.ios.js
苹果程序入口 -
package.json
配置文件
项目启动流程
-
index.html
通过加载<div id="react-root"></div>
进入root.js
<!DOCTYPE html>
<meta charset="utf-8">
<title>React Native for Web</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="react-root"></div>
<script src="/bundle.js"></script>
root.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import App from './containers/App';
import configureStore from './store/configureStore';
class Root extends Component {
render() {
return (
<Provider store={configureStore()}>
<App />
</Provider>
);
}
}
export default Root;
-
App.js
设置一些启动时需要做的事情,renderScene
添加路由
import React, {Component} from 'react';
import { Navigator } from 'react-native';
import MainTabsView from './MainTabsView';
import BroswerView from './BroswerView';
import LoginView from './LoginView';
import CarDetailView from './CarDetailView'
import FindCarView from './FindCarView'
import FreePriceView from './FreePriceView'
import CarLifeView from './CarLifeView'
import SpecialCarView from './SpecialCarView'
const ROUTES = {
main_tabs_view: MainTabsView,
login_view:LoginView,
broswer_view: BroswerView,
car_detail_view:CarDetailView,
find_car_view:FindCarView,
free_price_view:FreePriceView,
car_life_view:CarLifeView,
special_car_view:SpecialCarView,
}
class App extends Component {
renderScene = (route, navigator) => {
let Scene = ROUTES[route.name];
console.log("app renderscene");
switch (route.name){
case 'main_tabs_view':
return <Scene navigator={navigator} tab={2}/>;
case 'login_view':
return <Scene navigator={navigator}/>;
case 'broswer_view':
return <Scene
url={route.url}
navigator={navigator}/>;
case 'car_detail_view':
return <Scene {...route.params} navigator={navigator}/>;
case 'find_car_view':
return <Scene navigator={navigator}/>;
case 'free_price_view':
return <Scene navigator={navigator}/>;
case 'car_life_view':
return <Scene navigator={navigator}/>;
case 'special_car_view':
return <Scene navigator={navigator}/>;
}
}
configureScene = (route, routeStack) => {
switch (route.name){
default:
return Navigator.SceneConfigs.PushFromRight;
}
}
render() {
return <Navigator
initialRoute={{name: 'login_view'}}
renderScene={this.renderScene}
configureScene={this.configureScene}/>
}
}
export default App;
效果展示
-
登录
登录 - 首页 (redux数据流程的简单介绍)
- 我们在组件生命周期
componentDidMount()
中进行数据请求。this.props.actions.getBannerSource({});
根据redux
在action
中处理事件。
componentDidMount() {
this.props.actions.getBannerSource({});
this.props.actions.getHotCarSource({});
this.props.actions.getFreePriceListSource({});
this.props.actions.getCarShowBannerSource({});
this.props.actions.getSpecialCarSource({});
}
- 此时数据流将会来到
action
中,根据状态常量DATA_BANNER
,处理事件,action中只处理事件,要想改变状态,通过receiveBannerPosts(banner, json)
方法去reducers
中改变状态
//轮播图
export function getBannerSource(item) {
console.log("getBannerSource");
return dispatch => {
//dispatch(requestPosts(item));
return fetch(API_BASE_URL+'/mobile/ad/findAllShowAdByMobile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'sessionid': "hanwuqia"
},
body: JSON.stringify({"query":{"pagenum":10,"page":1}})
})
.then((response) => response.json())
.then((responseJson) => {
//console.log(responseJson);
if(responseJson.code == 0){
dispatch(receiveBannerPosts(responseJson.data.rows));
}
})
.catch((error) => {
console.error(error);
});
}
}
function receiveBannerPosts(banner, json) {
return {
type: DATA_BANNER,
banner,
}
}
- 当数据流来到
reducer
时会通过刚才的状态常量给状态banner
赋值,这样请求下来的数据进入到状态树中。
export default function secondView(state ={
banner: [],
}, action) {
switch (action.type) {
case types.DATA_BANNER :
return Object.assign({}, state, {
banner: action.banner
});
default:
return state
}
}
- 这时在组件中得知状态改变从而重修刷新
render
方法,填充数据,这样就获得了轮播图的数据,这里轮播图我用了一个叫做react-native-viewpager
的第三方组件
function mapStateToProps(state) {
return {
banner: state.secondView.banner,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(SecondView);
- RN 中所有布局全部包在一个大的
View
中就好像h5中的盒子布局的div
类似,如果有两个大盒子会报错const { } = this.props;
是es6语法获得到这个组件的状态,通过这样的方法<Banner banner={banner}></Banner>
传给子组件,每当状态改变时会自动刷新,进而子组件内容也进行刷新。
render() {
const { banner ,hotCarList,freePriceList,carShowBanner,specialCar,navigator} = this.props;
return (
<View style={{ flex: 1, backgroundColor:'#EBEBEB'}}>
<TitleBar title="react-native-mobile"></TitleBar>
<ScrollView >
<Banner banner={banner}></Banner>
<ToolBar navigator={navigator}></ToolBar>
<Text style={styles.title}>免费看低价</Text>
<FreePriceList navigator={navigator} freePriceList={freePriceList}></FreePriceList>
<Text style={styles.title}>车主秀</Text>
<CarShowBanner carShowBanner={carShowBanner}></CarShowBanner>
<Text style={styles.title}>热门推荐</Text>
<HotCarList hotCarList={hotCarList} navigator={navigator}></HotCarList>
</ScrollView>
</View>
)
}
首页
首页
- 找车模块
- 找车的热门选车模块用到了RN中核心组件
ListView
的使用,这是一个分组的listView,dataSource
就是列表的数据源,根据官方文档不同样式的列表有不同的初始化传参方式,这里只介绍分组的列表,项目中用到了很多各种各样的listView
列表!所以说他是核心组件。renderRow
返回每行样式,renderSectionHeader
返回每组组头,renderHeader
返回列表表头,像我一样做过iOS开发的朋友可能会觉得不得理解,就像我们的UITableView中的各种代理方法一样,官网还有很多,很实用。
render(){
const { brand} = this.props;
var Arr = brand ,
sectionIDs =[],//所有区ID的数组
rowIDs =[];//行ID数组
for (let i in brand ) {
sectionIDs.push(i);
rowIDs.push(brand[i])
}
return(
<ListView//创建表,并设置返回section和cell的方法
dataSource={this.dataSource.cloneWithRowsAndSections(Arr,sectionIDs,rowIDs)}
renderRow={this.renderRow}
renderSectionHeader={this.renderSectionHeader}
renderHeader={this.renderHeader }
/>
)
}
//返回头部视图
renderHeader(){
var rowWidth = screenWidth/5;
return(
<View style={{width:screenWidth,flexDirection:'row',flexWrap:'wrap',height:160}}>
{
this.hotBrndArr.map((dic, i) => <TouchableOpacity key={i} style={{width:rowWidth,height:80}} onPress={() => {
this.selectBrand(dic)}}>
<View style={{backgroundColor:'white',justifyContent:'center',alignItems:'center',width:rowWidth,height:80}}>
<Image style={styles.imageStyle} source={{ uri: dic[1]}}/>
<Text>{dic[2]}</Text>
</View>
</TouchableOpacity>) // 单行箭头函数无需写return
}
</View>
)
}
//返回cell的方法
renderRow(rowData,sectionID,rowID,highlighRow){
return(
<TouchableOpacity key={rowID} onPress={() => {
this.selectBrand(rowID)}}>
<View style={styles.cellStyle}>
<Image style={styles.imageStyle} source={{ uri: rowID[2]}}/>
<Text style={{marginLeft:20}}>{rowID[1]}</Text>
</View>
</TouchableOpacity>
)
}
//返回section的方法
renderSectionHeader(sectionData,sectionID){
return(
<View style={styles.sectionStyle}>
<Text style={{marginLeft:10}}>{sectionID}</Text>
</View>
)
}
找车
- 筛选的时候有个一个点击侧滑弹出的功能,这是纠结我最久的一块,一开始我选择使用了官方API中的
Animated
,动画实现方式很简单,但是动画加上大量的数据请求,大量的UI操作,导致程序在安卓机上卡的一逼,毕竟只有半年前端经验,js更是菜的自己都不想说,谷歌了好几天,安卓机的性能优化的总是不理想,没办法,只能采用第三方组件react-native-drawer
。使用后再无性能问题。
<Drawer
side="right"
type="overlay"
ref={(ref) => this._drawer = ref}
content={...}
tapToClose={true}
openDrawerOffset={0.2}
tweenHandler={(ratio) => ({main: { opacity:(2-ratio)/2 }})}
onClose={()=>{this.maskDidClose()}}
styles={{
drawer: { shadowColor: '#000000', shadowOpacity: 0.5, shadowRadius: 3},
main: {backgroundColor:"#EBEBEB"},
}}
>
-
react-native-drawer
文档中给出了各种方向,各种样子的侧滑,感觉非常好用,下面是代码滑进滑出的方法
closeControlPanel = () => {
this._drawer.close()
};
openControlPanel = () => {
this._drawer.open()
};
找车筛选
- 免费看低价
- 这个模块中让我也遇到了很多的困难,首先类似
UICollectionView
中的布局其实也是个listView
,通过设置contentContainerStyle
让他变成方形向下平铺,运行时总是报警告说我的组头是空的,然后enableEmptySections = {true}
设置这个属性解决,最蛋疼的是有时候数据虽然请求下来但是现实空白页,用手碰一下就会现实数据,通过设置这个属性来解决removeClippedSubviews={false}
render(){
const { car,isRefreshing} = this.props;
return(
<ListView //创建ListView
dataSource={this.dataSource.cloneWithRows(car)} //设置数据源
renderRow={this.renderRow} //设置cell
contentContainerStyle={styles.listViewStyle}//设置cell的样式
onEndReached={ this._toEnd }
onEndReachedThreshold={10}
renderFooter={ this._renderFooter }
enableEmptySections = {true}
removeClippedSubviews={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={this._onRefresh}
tintColor="gray"
title="Loading..."
titleColor="gray"
colors={['#ff0000', '#00ff00', '#0000ff']}
progressBackgroundColor="#ffff00"
/>}
/>)
}
- 这个是RN自带的下拉刷新,通过设置
title,colors
等设置下拉刷新的样子,通过设置refreshing
这个bool属性来控制刷新开始结束
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={this._onRefresh}
tintColor="gray"
title="Loading..."
titleColor="gray"
colors={['#ff0000', '#00ff00', '#0000ff']}
progressBackgroundColor="#ffff00"
/>}
- 当你发现你的cell点击事件无效果时,或者你的下拉刷新的方法没有走,是因为你赋给
ListView
的方法需要声明一下,这样才能进入到点击事件中如:
constructor(props) {
super(props);
//返回cell样式的方法
this.renderRow = this.renderRow.bind(this);
//下拉刷新的方法
this._onRefresh = this._onRefresh.bind(this);
//上滑刷新的方法
this._toEnd = this._toEnd.bind(this);
//返回底部视图的方法
this._renderFooter = this._renderFooter.bind(this);
//数据源初始化
this.dataSource = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
});
}
免费看低价
免费看低价筛选
- 车生活
- 这个模块是我们很常用的一个架构,类似与网易新闻架构,是APP开发中非常常见的的一中架构,首先当然我还是尝试着自己写,根据按钮点解设置
scrollView
的contentOffset
,根据contentOffset
设置应该选中的按钮。以及根据切换设置橙色view
线的滑动,结果遇到了两个问题,一个是向上面那样,大量数据加载,大量的UI创建导致界面卡顿非常厉害,当然是在安卓机上,我大苹果肯定没有这个问题,还有一个就是有些方法只有iOS可以试用,想写一个两端通用的更是难上加难,所以我引入了react-native-scrollable-tab-view
这个第三方组件
render(){
const {data,refresh,actions}=this.props;
return(
<ScrollableTabView style = {{width:screenWidth,height:screenHeight-64,backgroundColor:'#EBEBEB'}}
initialPage={0}
tabBarTextStyle={{fontSize: 14}}
tabBarUnderlineStyle={{backgroundColor: 'orange'}}
tabBarInactiveTextColor = "#999999" tabBarBackgroundColor = "white" tabBarActiveTextColor = "#333333"
onChangeTab={(obj) => this.changeTab(obj)}
>
<CarLifeList tabLabel='全部' actions={actions} pid={2} index={0} refresh ={refresh} dataArr={data}></CarLifeList>
<CarLifeList tabLabel='新车' actions={actions} pid={2} index={1} refresh ={refresh} dataArr={data}></CarLifeList>
<CarLifeList tabLabel='装饰' actions={actions} pid={2} index={2} refresh ={refresh} dataArr={data}></CarLifeList>
<CarLifeList tabLabel='改装' actions={actions} pid={2} index={3} refresh ={refresh} dataArr={data}></CarLifeList>
<CarLifeList tabLabel='自驾' actions={actions} pid={2} index={4} refresh ={refresh} dataArr={data}></CarLifeList>
</ScrollableTabView>
)
}
- 在这个界面我还遇到了一个我至今还无法相同的问题,我用
redux
管理状态,我把一个数组的数据当成一个状态,每次请求数据的时候对数组进行操作,上滑的时候向数组中添加数据,我认为数组中数据增加了就是状态改变了,render
理应重新渲染才对,但是显然不是这样的,我感觉有时候数组作为状态发生改变时,redux
并不能检测到,通过打印日志,数据确实发生改变了,但是确实没有重新刷新。之前我做vue
使用vuex
时也遇到过。我采用的方法不知道是不是规范的方法,我是在状态中又添加了一个布尔的loading
,刷新时设置为ture
请求我数据设置为false
,通过这个布尔值刷新列表进行重新渲染。
//通过loading 的改变刷新
case types.CAR_SHOW_FETCH :
return Object.assign({}, state, {
carShowDataArr: action.carShowDataArr,
loading:action.loading
});
- 这个模块还遇到个性能问题是有时候状态太多,有的状态改变并不需要刷新,这时候基础很重要了,组件的生命周期有这么个方法
shouldComponentUpdate(nextProps, nextState) {}
当这个方法的返回值是YES
时会刷新,NO
时不会刷新,render
每次刷新之前都会调用这个方法,只有返回值是YES
时才会刷新,这个方法中我们可以做很多事情,解决很多性能问题
车生活 - 车款详情
- 这个界面算是比较复杂的界面,想想要是让我用原生写,我估计要好久可能也写不好,尤其是下面颜色选择,这就体现出H5布局的优势了,简单,快,
react-native
使用flexBox
布局,真心简单方便,不过这个界面也遇到了一点性能的小问题,每当每个色块点击时要重新请求数据,改变上面的图片,也就是车的颜色,当我的安卓机每次点击切换时,TouchableOpacity
按钮自带的淡入淡出的效果变得非常卡顿了,在onPress执行了一个setState的操作,这个操作需要大量计算工作并且导致了掉帧。对此的一个解决方案是将onPress处理函数中的操作封装到requestAnimationFrame中:
requestAnimationFrame(() => {
this.setState({
selectInteriorColor :index,
});
const { interiorColorList,exteriorColorList,actions ,id,detailType} = this.props;
let inId = interiorColorList[index].id;
let outId = exteriorColorList[this.state.selectExteriorColor].id;
actions.getCarInfo({outId,inId,id,detailType});
});
车款详情
- RN 中加载web
-
这是个webView,这是我纯属显得蛋疼想用reactNative加载个h5页面看看啥效果,请见谅
WebView
性能优化
- 学习了一段时间马上就要开发了,感觉当前对于我来说最大的问题还是关于性能的问题,因为前端基础比较差,很多很简单的东西需要好久才能找到合适的解决方案,总结了几个常见的性能问题,之后仔细又阅读了一遍官方的文档,发现很多问题文档上都有讲,所以文档还是要常看啊
- console.log语句
- 在运行打好了离线包的应用时,控制台打印语句可能会极大地拖累JavaScript线程
- 解决方案发,自己封装一个打印语句,根据debug是0还是1选择是否打印,方便我们上线时把打印全都去掉,推荐一个好用的第三方
redux-logger
- 开发模式和生产模式是大不相同的,有时候开发模式下会很卡,但生产模式就不一样了。
-
Navigator
导航切换,这个是非常常见的卡顿,可以选择的解决方法也有很多,这是我的解决方法,利用官方推荐的这个组件InteractionManager
- 首先设置这样一个状态
onstructor(props) {
super(props);
this.state={
renderPlaceholderOnly:true,
}
}
-
render
方法中进行判断,renderPlaceholderOnly
为ture时给他一个加载页面显示false时显示界面
render() {
if (this.state.renderPlaceholderOnly) {
//loading页
return this._renderPlaceholderView();
}
return (
//界面
)
}
_renderPlaceholderView() {
return (
<View style={{flex:1,backgroundColor:'white',justifyContent:'center',alignItems:'center',}}>
<Text>Loading...</Text>
</View>
);
}
- 最后将请求数据的方法封装到下面的方法中
InteractionManager.runAfterInteractions
是在js动画结束时会走里面的回调。
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.setState({renderPlaceholderOnly: false});
this.props.actions.getBrandSource({});
this.props.actions.getCarTypesource();
});
}
- 关于
listView
的性能优化 -
initialListSize
这个属性定义了在首次渲染中绘制的行数。如果我们关注于快速的显示出页面,可以设置initialListSize
为1,然后我们会发现其他行在接下来的帧中被快速绘制到屏幕上。而每帧所显示的行数由pageSize
所决定 -
pageSize
在初始渲染也就是initialListSize被使用之后,ListView将利用pageSize来决定每一帧所渲染的行数。默认值为1 —— 但是如果你的页面很小,而且渲染的开销不大的话,你会希望这个值更大一些。稍加调整,你会发现它所起到的作用。 -
scrollRenderAheadDistance
在将要进入屏幕区域之前的某个位置,开始绘制一行,距离按像素计算。如果我们有一个2000个元素的列表,并且立刻全部渲染出来的话,无论是内存还是计算资源都会显得很匮乏。还很可能导致非常可怕的阻塞。因此scrollRenderAheadDistance允许我们来指定一个超过视野范围之外所需要渲染的行数。 -
removeClippedSubviews
当这一选项设置为true的时候,超出屏幕的子视图(同时overflow
值为hidden)会从它们原生的父视图中移除。这个属性可以在列表很长的时候提高滚动的性能。默认为false。这是一个应用在长列表上极其重要的优化。Android上,overflow值总是hidden的,所以你不必担心没有设置它。而在iOS上,你需要确保在行容器上设置了overflow: hidden。 - 如果你正在使用一个ListView,你必须提供一个rowHasChanged函数,它通过快速的算出某一行是否需要重绘,来减少很多不必要的工作。如果你使用了不可变的数据结构,这项工作就只需检查其引用是否相等。同样的,你可以实现
shouldComponentUpdate
函数来指明在什么样的确切条件下,你希望这个组件得到重绘。 - 关于动画
Animated
的使用,尽量使用LayoutAnimation
,Animated
的接口一般会在JavaScript
线程中计算出所需要的每一个关键帧,而LayoutAnimation
则利用了Core Animation
,使动画不会被JS线程和主线程的掉帧所影响。尤其是在动画过程中有大量的数据请求,状态改变。 - 当具有透明背景的文本位于一张图片上时,或者在每帧重绘视图时需要用到透明合成的任何其他情况下,这种现象尤为明显。设置shouldRasterizeIOS或者renderToHardwareTextureAndroid属性可以显著改善这一现象。 注意不要过度使用该特性,否则你的内存使用量将会飞涨。在使用时,要评估你的性能和内存使用情况。如果你没有需要移动这个视图的需求,请关闭这一属性。
- 在iOS上,每次调整Image组件的宽度或者高度,都需要重新裁剪和缩放原始图片。这个操作开销会非常大,尤其是大的图片。比起直接修改尺寸,更好的方案是使用transform: [{scale}]的样式属性来改变尺寸。比如当你点击一个图片,要将它放大到全屏的时候,就可以使用这个属性。
- Touchable系列组件不能很好的响应
- 有些时候,如果我们有一项操作与点击事件所带来的透明度改变或者高亮效果发生在同一帧中,那么有可能在onPress函数结束之前我们都看不到这些效果。比如在onPress执行了一个setState的操作,这个操作需要大量计算工作并且导致了掉帧。对此的一个解决方案是将onPress
处理函数中的操作封装到requestAnimationFrame
中
- Demo地址:
- git( https://github.com/heiheiLqq/RNBaseApp.git )
网友评论