程序人生 A log of my life

React Native

React Native是跨平台开发的一个常见选择,其他选择有Codova等,本文介绍React Native(下面有时缩写为RN),RN的Getting Started通常有两个选择:CRNA或常规工程,下面一一介绍:

CRNA工程

npm install -g create-react-native-app
create-react-native-app rnTest

这种方法创建的工程,称之为CRNA工程,非常轻量,不需要预安装Android或iOS开发环境,所以非常适合初学,但是因为没有开发环境,所以手机上需要预安装一个工具Expo(android或iOS都可以),用来从开发机上同步代码,如果已经安装Expo,那么就可以做接下来的步骤

cd rnTest
npm start

会在终端上出现一个大大的二维码,然后用Expo去扫这个二维码即可加载了(确保手机和开发机在同一局域网下),我发现这个二维码经常扫不出,可以手工输入地址来解决。如果不愿意下载安装Expo,可以用USB连接好手机,用npm run android,RN会尝试用adb工具下载Expo到手机中,这样可以省去下载Expo的过程,但是需要开发机上有android开发工具(不需要编译,但需要adb工具)。

总之,运行起来项目之后,就可以尝试有趣的hot reload了,按手机的菜单键(没有这个键,可以摇动手机),会出现开发者菜单,里面有live reload和hot reload,前者相当于全量刷新,后者相当于保持状态下增量注入,缺省情况下hot reload是禁止的,可以打开,这样代码更新后刷新的速度比live reload要快一些,和Cordova下的hot reload一样,有时hot reload工作会不正常,需要手工reload。

当然,一旦expo安装好,以后是不需要usb连接了,比较方便。但这种CRNA工程有个缺点就是不能包括Native代码,只能使用RN自带的API和组件,因为这些组件已经包括在Expo的客户端里,所以如果你确信你的APP需要Native模块,可以直接走下面的常规工程。

常规工程

也称之为Native工程,这种工程需要本机有android或iOS开发环境,并且在开发过程中一般使用有线(USB)连接真机。

react-native init xxx
cd xxx
react-native run-android

注意:运行的命令和CRNA工程不一样,不再是npm run android。把CRNA工程转换为常规工程的方法是:

npm run eject

调试

无论CRNA还是普通工程,调试都是通过chrome,在开发者菜单中选择debug js remotely,就会在桌面上自动启动chrome的调试页了。开启chrome的开发者工具页面后,按下图的路径找到被调试的js就可以了。

在调试版本,经常会看到黄色的警告提示,如果不想看到这些警告,在js代码入口处增加:

console.disableYellowBox = true;

发布

一个最简单的RN工程,编译出的apk也大概有8M,如果想缩小体积,可以考虑删除x86的支持,大概可以缩小到4M多。

控件

控件的描述使用了一个Javascript扩展JSX,因此写起来才比较方便,避免了在JS里“嵌入”用字符串括起来的大段模板,这点和Vue不一样,Vue是把模板单独作为一段,和JS平行起来构成了VUE文件。

控件的安装流程为:

  • constructor(object props)
  • componentWillMount()
  • render()
  • componentDidMount()

控件的刷新流程为:

  • componentWillReceiveProps(object nextProps)
  • shouldComponentUpdate(object nextProps, object nextState)
  • componentWillUpdate(object nextProps, object nextState)
  • render()
  • componentDidUpdate(object prevProps, object prevState)

属性Props

class Greeting extends Component {
  render() {
    return (
      <Text>Hello {this.props.name}!</Text>
    );
  }
}

export default class LotsOfGreetings extends Component {
  render() {
    return (
      <View style=>
        <Greeting name='Rexxar' />
        <Greeting name='Jaina' />
      </View>
    );
  }
}

A parent element may alter a child element’s props at any time. The child element will generally re-render itself to reflect its new configuration parameters. A child component may decide not to re-render itself even though its configuration has changed, as determined by shouldComponentUpdate() (more on this in the Component Lifecycle API section).

状态 State

属性是不变的,状态是可变的,文档中的这个例子很清楚:

class Blink extends Component {
  constructor(props) {
    super(props);
    this.state = {isShowingText: true};

    // Toggle the state every second
    setInterval(() => {
      this.setState(previousState => {
        return { isShowingText: !previousState.isShowingText };
      });
    }, 1000);
  }

  render() {
    let display = this.state.isShowingText ? this.props.text : ' ';
    return (
      <Text>{display}</Text>
    );
  }
}

注意: 状态在构造函数中可以直接赋值,在method中可以读取this.state, 但只能通过setState来修改,每次修改会自动触发控件的刷新。如果不需要自动触发刷新,可以不通过state来管理状态,可以直接在this里创建新的变量。

样式

类似CSS,但去除了CSS中‘难用’的Cascade带来的一些问题,RN中样式的一个常见用法是把样式定义为一个数组,在代码中引用不同的样式名就可以了,达到了Web上class的效果,比如:

var Style = StyleSheet.create({
  rootContainer: {
    flex: 1
  },

  displayContainer: {
    flex: 2,
    backgroundColor: '#193441'
  },

  inputContainer: {
    flex: 8,
    backgroundColor: '#3E606F'
  }
}
...
render() {
    return (
      <View style={Style.rootContainer}>
        <View style={Style.displayContainer}></View>
        <View style={Style.inputContainer}>
          {this._renderInputButtons()}
        </View>
      </View>
    )
  }

有时,针对不同平台需要不同的样式,可以通过Platform来选择,举例:

import { Platform, StyleSheet } from 'react-native'
const styles = StyleSheet.create({
  container: {
    fontFamily: 'Arial',
    ...Platform.select({
      ios: {
        color: '#333',
      },
      android: {
        color: '#ccc',
      },
    }),
  },
});

布局

主要的布局方式兼容Web的Flex布局,但比Web要简单并且好用,参考下面的文档:

  • https://medium.freecodecamp.com/an-animated-guide-to-flexbox-d280cf6afc35

除了Flex,另一个常用的布局方法是使用Dimensions,参考下面:

import { Dimensions } from 'react-native'
const { width, height } = Dimensions.get('window')

隐藏状态栏

很简单,只需要在根view中加入:

<StatusBar hidden />

应用名称、图标和版本

相对于Cordova,RN对应用名称和图标几乎没有封装,修改起来非常不便,好在都有工具可以做到,下面是做法:

  • 修改图标
    npm install -g yo generator-rn-toolbox
    yo rn-toolbox:assets --icon .\src\assets\icon.png
    

    注意修改图标需要预先安装image-magick,修改名称就比较麻烦了,简单的方法是删除android和ios目录后,重新react-native eject

  • 修改package名称
npm install react-native-rename -g
react-native-rename "myapp" -b com.mycompany.myapp

*修改版本号

只能手工修改,或者通过build.gradle文件自动化,参考这里

路由和导航

和VUE一样,路由模块不属于核心模块,不过大部分RN项目使用了某个路由模块,其中最流行的为React Navigation。它提供了最基础的StackNavigator模块,这个类似Web导航(Stack的概念),差别就是StackNavigator还提供了切换时的动画。

StackNavigator本身是一个函数,提供两个参数,一个配置参数配置路由表,一个选项参数,举例:

const RootStack = StackNavigator(
  {
    Home: {
      screen: HomeScreen,
    },
    Details: {
      screen: DetailsScreen,
    },
  },
  {
    initialRouteName: 'Home',
  }
);

这样返回的组件就可以作为根组件了,同时会在每个screen组件下注入一个属性navigation用于跳转,比如this.props.navigation.navigate('Details')或者this.props.navigation.goBack(), 跳转的时候当然是可以带参数的,可以放在后面,比如:

 this.props.navigation.navigate('Details', {
    itemId: 86,
    otherParam: 'anything you want here',
  });

在DetailsScreen组件中,可以通过this.props.navigation.state来读取传入的参数。

TabNavigator

除了StackNavigator,另一个常用的为TabNavigator,就是很多应用中首页中看到的多Tab的设计。

自带组件

按钮

比如跳转打开浏览器的按钮可以这样定义:

<TouchableHighlight
  onPress={() => Linking.openURL(
    `http://finance.yahoo.com/q?s=${this.state.symbol}`
  )}>

列表

RN内置两个列表控件,ListView和FlatList,建议使用新的FlatList,性能更好,并且将来应该会替代ListView

第三方组件

除了RN自带的组件,还有一些重要的第三方组件:

react-native-vector-icons

  • yarn add react-native-vector-icons
  • react-native link
  • 在android/app/build.gradle的dependencies下加入compile project(‘:react-native-vector-icons’)

还可以找到很多第三方组件库(类似Cordova下可以用Quasar等库),比如:

其他有趣的

总的来说RN的体系坑比较多,比Cordova环境难一些,从github上找一些RN的项目,大多在编译时都会遇到各种各样的问题,下面是一些我踩过的:

编译时 Could not delete path ‘android\app\build\generated\source\r\release\android\support’

不知道原因是什么,但是有时会出现这个问题,通常重新编译就好了,实在不行,需要cd android && gradlew clean,再重新来过

编译时com.android.ddmlib.InstallException: Failed to install all

我在一台android手机上遇到了这个问题,尝试下面几个方法:

  • react-native run-android –deviceId xxx //后面的xxx是adb devices命令返回的设备id
  • 如果是小米,关闭MIUI优化
  • 还不行,换一台手机

运行Android debug版本不覆盖手机老版本

这个还没有特别好的解决方法,只能先在手机上卸载,或者自己在构建脚本中加入adb uninstall命令。

运行时报错 set canOverrideExistingModule=true

在MainApplication.java(android/app/src/main/java/../..)下找看看,去除getPackages函数下重复的MapsPackage和import,这应该是RN的bug,也许未来的版本会解决。

运行Android debug版本一段时间后出现could not connect to development server

packager的端口断了,解决方法是再次运行adb reverse tcp:8081 tcp:8081

Debug版本打不开开发者菜单

有些手机没有菜单键,可以通过下面几个方法尝试:

  • 摇动手机
  • adb shell input keyevent 82
  • 在手机上安装可以模拟菜单键的悬浮球软件

参考

  • https://hackernoon.com/learning-react-native-where-to-start-49df64cf14a2
  • http://www.reactnativeexpress.com