初识ReactNative

公司打算用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.htmlhtml加载DOM
    • root.jsRN入口
  • 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({});根据reduxaction中处理事件。
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开发中非常常见的的一中架构,首先当然我还是尝试着自己写,根据按钮点解设置scrollViewcontentOffset,根据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

推荐阅读更多精彩内容

  • 开发环境配置 参照这里 页面渲染 组件的渲染需要在自定义 class 中进行,每个自定义视图class中都包含一个...
    cocoawork丶阅读 255评论 0 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 167,473评论 24 707
  • 本人小白,也是最近才开始学习ReactNative,只是想记录自己的学习历程,供日后查阅复习,文中有任何错误或者不...
    小唐羽锋阅读 1,269评论 0 51
  • 初识React Native 2015年3月26日,FaceBook公司正式对外发布了React Native 更...
    koala_阅读 3,002评论 0 7
  • 事情没到最后呢,不要想得太多,说不定是好事呢! 每一件事都有两面性,塞翁失马焉知非福,因为一个学生我竟然跟鹏哥讨论...
    晓玖思维汇阅读 100评论 0 0
  • 一辈子很短,短到你根本无法细细体会生活各种滋味。 当你走出校门,还没适应社会,你就必须考虑结婚生子,如果,你可以,...
    骚动的萝卜阅读 154评论 0 0
  • 话说某个人去银行存钱的路上,一不小心弄丢了一笔钱。他郁闷至极。 谁知几个月过去了,突然某天,他在他家门外发现了一个...
    小小草2016阅读 117评论 0 0