React-Native实例

前言

移动开发也走过了将近10年的历程,开发的技术也随着移动设备各个版本的迭代发生了巨大的变化,不管是iphone X和android 8.X和最初的iphone 1以及android1.X,无疑都发生了巨大的变化,哪怕一两年的节点之间,移动设备的软硬件都会产生巨大的变化。于此同时,开发技术和模式也有了长足的发展,从早前的纯native开发,到后来的webview+html的hybrid native开发模式,以及现在以React-Native、Weex等融合前端技术的DyNative模式,以及Google推出instant run之后各大公司开发的热修复框架、相应开发场景的开源框架等等,目标都是一致的,在合适的开发场景运用相应最合适的技术,为用户带来最好的使用体验,这篇文章就已我当前的项目的一小部分改造来聊聊React-Native的一些特性。

React-Native的优缺点

下图是几种开发模式的描述图:

三种模式.png

从上图描述中能得到几个关键的特性,1、性能比WebView+Html的传统APP要好。2、开发体系不完善。对于第一点,传统的webview实际上是基于webkit的微型浏览器,通过webkit的内核对页面进行渲染,由于webkit本身就是一个重量级内核,执行效率一般,React-Native较高的效率的原因是由于RN基于虚拟DOM技术,虚拟DOM技术指的是使用javascript Object模拟DOM树,计算变更,减少重绘,提升效率的一种技术实现。对于第二点,由于React-Native是基于前端的跨平台的框架,因此很难做到平衡各平台之间的特性,往往会出现各种各样的问题,例如某些特性IOS上有,Android平台不支持,或者各平台表现不一致,这都是开发者需要面对的问题。

总的来讲,按我个人的理解,React-Native最大的优点在于两点,第一,能够统一各大平台的UI显示(这点有点类似于BootStrap);第二,执行效率较传统webapp要快,能一定程度提升用户体验;第三,较于原生开发,能够支持热更新。缺点在于,React-Native并不能简化开发流程,减少开发量,React-Native提出的“Learn Once ,Write Anywhere”口号描绘的开发者基于React-Native的移动app能运行于android、ios平台能打破传统一个平台至少一个开发者的愿景是不太可能实现的,原因无他,因为即便是android或者ios自家平台的不同系统版本都无法做到完全适配,目前使用Dynative的互联网公司的配置一般是前端组、android组、ios组,前端组专门负责这些页面的绘制、逻辑和于各平台的native开发人员进行交互。再一个开发体系不完善,不同平台之间不能做到完全无差异化,以及对native特性的完全支持。

本篇文章要达到的目标

目前我从事的项目执行效果如下:

手机端
PC端

本篇文章的目标就是将手机端的主页面改造成React-Native,模拟Native端发送连接信息,改变主页连接信息的显示以及React-Native的主页点击扫描按钮调用Native端的扫码页面。

准备工作:

React-Native的配置:

环境的搭建可以参考React-Native 入门(环境搭建及运行)

但是大多数时候第一运行结果都会报错

运行失败.png

总结报错的原因,主要原因有以下几点:

本地的端口和真机或模拟器的ip未设置好。

解决办法:

adb reverse tcp:8081 tcp:8081,然后在项目运行后摇动手机进入设置页面,设置项目的ip端口,如进入不了设置页面。

需要将设置页面的声明加入到manifest

Could not get BatchedBridge, make sure your bundle is packaged correctly:

解决办法:

在app/src/main目录下新建assets文件夹,然后启动cmd,回到项目根目录执行

react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

react-native服务没有启动

解决方法:

在terminal里面执行npm start启动react-native服务

React-Native集成方案

一种方式是在原有布局中添加com.facebook.react.ReactRootView,可以参考现有Android项目引入ReactNative--九步大法

但是这种方式的话,不管是通过adb的命令还是摇晃手机都无法调出DevSettingActivity的设置界面,无法修改真机调试的ip,涉及到修改配置脚本,较为麻烦,推荐使用Activity继承ReactActivity的方式加载React-Native页面。


public class XXXActivity extends ReactActivity{

@Override

protected String getMainComponentName() {

return "XXX";

}

在项目中引入React-Native中常见错误:
Cannot resolve ReactApplication或者ReactNativeHost。
解决办法:

allprojects {
    repositories {
        mavenLocal()
        jcenter()
        maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public/'//用国内aliyun、不用google
        }

        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url "$rootDir/node_modules/react-native/android"
        }
        google()
    }
}

rootDir/node_modules/react-native/android路径配置错误 ,很多教程把这个路径配置成url "rootDir/../node_modules/react-native/android",这个路径要配置成根目录和node_modules之间的正确路径,当前项目的真实路径是怎样的就该怎样配置。否则会出现加载错误的react-native的本地库,导致ReactApplication解析错误。

代码改造

完整的js代码如下。

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View,Image,Dimensions,PixelRatio,NativeModules,DeviceEventEmitter,Alert,TouchableWithoutFeedback,
} from 'react-native';
const deviceWidth = Dimensions.get('window').width;
const deviceHeight = Dimensions.get('window').height;

const px2dp = px=>PixelRatio.roundToNearestPixel(px);
const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

type Props = {};
export default class LmsaHomePage extends Component<Props> {
constructor(props) {
    super();
    this.state = {
        connect_img: require('.././image/connect_disconnected.png'),
        connect_text:'Device Disconnected',
        connect_color:'#6D6D6D',
        model_name:'Welcome',
    };

}


componentWillMount(){
    DeviceEventEmitter.addListener('LmsaConnect',this.handleConnectMessage);
    DeviceEventEmitter.addListener('LmsaModelName',this.handleModelMessage);
  }

  componentWillunMount(){
     DeviceEventEmitter.remove('LmsaConnect',this.handleConnectMessage);
     DeviceEventEmitter.remove('LmsaModelName',this.handleModelMessage);
  }
  handleConnectMessage=(msg)=>{
       //RN端获得native端传递的数据
       if(msg=='USB_CONNECTING'||msg=='USB_CONNECED')
       {
       /*this.refs.ref_connect_img.setNativeProps({
                                            *//*source: {require('.././image/new_version_usb.png')}*//*style:{width:200,}
                                        });*/
           this.setState({
                           connect_img: require('.././image/new_version_usb.png'),
                           connect_text:msg,
                           connect_color:'#04E861',
                       });
       }
  }
  handleModelMessage=(msg)=>{
       //RN端获得native端传递的数据
      this.setState({
                                 model_name: msg,
                                 connect_text: 'USB_CONNECTING',
                                 connect_color:'#04E861',
                             });
  }
  goToScanActivity() {
         //alert('gotoscan');
         NativeModules.homePageReactNativeModule.scanToConnect(phone);
  }
  render() {
    return (
      <View style={styles.container}>
        <View style={styles.topframe}>
        <Image source={require('.././image/home_page_top_bg_new.png')}
         style={styles.topframeBg}
         />
         <Image source={require('.././image/home_page_lmsa_log.png')}
         style={styles.topframeLogo}
         />
         <Text style={{position:'absolute',fontSize:deviceWidth*0.04,color:'#FFFFFF',marginTop:deviceHeight*0.17}}>{this.state.model_name}</Text>
         
        </View>
        <Image source={this.state.connect_img/*require('.././image/connect_disconnected.png')*/}
         style={styles.topframeConnect} ref={'ref_connect_img'}/*{(c) => this.ref_connect_img = c}*/
         />
         <Text style={{position:'absolute',fontSize:deviceWidth*0.042,color:this.state.connect_color,marginTop:deviceHeight*0.2428,marginLeft:deviceWidth*0.346}}>{this.state.connect_text}</Text>
         <Image source={require('.././image/home_page_connect_bg.png')}
         style={styles.topframeConnectBg}
         />
         <TouchableWithoutFeedback
             onPress={()=> {
                  NativeModules.homePageReactNativeModule.scanToConnect();
             }}
         >
         <Image source={require('.././image/home_page_scan_30.png')}
         style={styles.topframeConnectScan}
         />
         </TouchableWithoutFeedback>
         <Text style={{position:'absolute',fontSize:deviceWidth*0.035,color:'#3E8DDC',marginTop:deviceHeight*0.316,marginLeft:deviceWidth*0.42}}>Scan to Connect</Text>
        <View style={styles.mainframe}>
            <View style={styles.mainframe_colomn}>
                <View style={styles.mainframe_colomn_item}><Image source={require('.././image/home_page_new_version_device_info.png')} style={styles.mainframe_colomn_item_image}/>
                <Text style={{fontSize:deviceWidth*0.04,color:'#3E8DDC',marginTop:deviceHeight*0.046}}>Device Info.</Text></View>
                <View style={styles.mainframe_colomn_item}><Image source={require('.././image/home_page_new_version_cpu_info.png')} style={styles.mainframe_colomn_item_image}/>
                <Text style={{fontSize:deviceWidth*0.04,color:'#3E8DDC',marginTop:deviceHeight*0.046}}>CPU Info.</Text></View>
            </View>
            <View style={styles.mainframe_colomn}>
                <View style={styles.mainframe_colomn_item}><Image source={require('.././image/home_page_new_version_hw_detection.png')} style={styles.mainframe_colomn_item_image}/>
                <Text style={{fontSize:deviceWidth*0.04,color:'#3E8DDC',marginTop:deviceHeight*0.046}}>HW Detection</Text></View>
                <View style={styles.mainframe_colomn_item}><Image source={require('.././image/home_page_new_version_rom_clean.png')} style={styles.mainframe_colomn_item_image}/>
                <Text style={{fontSize:deviceWidth*0.04,color:'#3E8DDC',marginTop:deviceHeight*0.046}}>ROM CleanwIII</Text></View>
            </View>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
  topframe: {
    flex:4,
    
    alignItems: 'center',
  },
  mainframe: {
    flex:6,
    justifyContent: 'center',
    alignItems: 'center',   
    backgroundColor:'rgba(0,0,0,0)',
    flexDirection: 'column',
  },
  topframeBg: {
      flex:1,
        alignItems:'center',
        justifyContent:'center',
       resizeMode :'contain',
        
        
        backgroundColor:'rgba(0,0,0,0)',
  },
  topframeLogo: {
    alignItems:'center',
    justifyContent:'center',
    position:'absolute',
     marginTop:deviceHeight*0.053,
      width:deviceWidth*0.158,
      height:deviceWidth*0.158,
  },
  topframeConnect: {
     
    position:'absolute',
     marginTop:deviceHeight*0.24,
     marginLeft:deviceWidth*0.26,
      width:deviceWidth*0.07,
      height:deviceWidth*0.07,
  },
  topframeConnectBg: {
      position:'absolute',
     marginTop:deviceHeight*0.298,
     alignItems:'center',
     marginLeft:deviceWidth*0.3,
     justifyContent:'center',
      width:deviceWidth*0.405,
      height:deviceWidth*0.104,
  },
  topframeConnectScan: {
       position:'absolute',
     marginTop:deviceHeight*0.312,
     alignItems:'center',
     marginLeft:deviceWidth*0.335,
     justifyContent:'center',
      width:deviceWidth*0.063,
      height:deviceWidth*0.063,
  },
  mainframe_colomn: {
    flex:1,  
    flexDirection: 'row',
  },
  mainframe_colomn_item: {
      flex:1,
      alignItems:'center',
      
      flexDirection: 'column',
      backgroundColor:'rgba(0,0,0,0)',
      borderWidth: 0.3,
      borderTopWidth: 0,
      borderLeftWidth: 0,
      borderColor:'#7C7C7C', 
  },
  mainframe_colomn_item_image: {
      
      alignItems:'center',
     
      flexDirection: 'column',
      backgroundColor:'rgba(0,0,0,0)',
       
      marginTop:deviceHeight*0.095,
      width:deviceWidth*0.127,
      height:deviceWidth*0.127,
  },
});

代码主要做了三件事,1完成首页的js布局;2接受native发送过来连接信息改变text文字和图片;3点击扫描图标,调用native方法跳转本地扫码界面。
js的布局主要根据当前的屏幕宽度和高度对每个元素的宽高属性进行设置,保证机型适配;native页面于react-native之间的数据交互原理可以参考React-Native数据交互
最终效果图如下

效果图.gif

推荐阅读更多精彩内容