React Native

React Native - 使用JavaScript和React开发移动端原生应用的框架

2017-06-16 | 阅读

React Native

官方文档

React Native是一个可以使用JavaScriptReact来开发原生应用的框架,由facebook开发,于2015年开源。

使用React Native可以让我们完全使用JavaScript来开发移动APP,其使用React ,所以可以通过声明式的组件来构建复杂的界面。

使用React Native开发的APP,不是web app,不是hybird app,而是真正的原生应用,其展示的界面与使用OC或者JAVA编写的界面完全一样。React Native通过JavaScrpit来调用基本的系统方法来构建界面以构建APP。

React Native可以热加载(Hot Reload) ,提高开发体验。热加载指RN服务器监听文件,如果文件修改,则会重新编译JS代码,而APP端只要重新刷新页面就可以看到修改。

React Native可以流畅的接入原生开发的组件和模块,以扩展更多的功能。

React Native的口号是 :

Learn once,write anywhere

React Native没有说一份代码运行在多个平台,而是说,可以用在不同平台。所以我们还是要正式不同平台之间的差距,要对三端兼容的方案和思想保持谨慎的态度。

开发环境

本文档面向对象是了解React的iOS开发人员😱

homebrew

mac上的包管理器 :

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

在Max OS X 10.11(El Capitan)版本中,homebrew在安装软件时可能会碰到/usr/local目录不可写的权限问题。可以使用下面的命令修复:

sudo chown -R `whoami` /usr/local

Node, Watchman

使用homebrew 安装:

brew install node
brew install watchman

watchman是一个facebook用来监听文件修改的工具,安装它以提高性能。

npm要设置使用淘宝的npm镜像:

npm config set registry https://registry.npm.taobao.org --global
npm config set disturl https://npm.taobao.org/dist --global

React Native CLI

rn CLI 用于初始化项目以及打包。使用npm安装:

npm install -g react-native-cli

VSCode

开发react native,一般我们使用VSCode这个编辑器,下载后,安装插件React Native Tools

hello world

通过react native cli创建一个项目 :

react-native init AwesomeProject

然后运行项目:

cd AwesomeProject
react-native run-ios

这时,我们来观察项目的结构:

__tests__目录下是测试相关文件。

androidios都是原生代码。

app.json内容如下:

{
  "name": "AwesomeProject",
  "displayName": "AwesomeProject"
}

用来控制生成的原生代码的项目名和展示名。 修改后,使用react-native eject命令来重新创建原生项目。

index.android.jsindex.ios.js : 是页面文件,这里有两份文件,一个用于iOS,一个用于android

node_modules是npm安装的依赖.

package.json是npm 的依赖控制文件,在开发RN时,当我们想要引入一些好用的第三方组件时,会在这里添加依赖。

index.ios.js

我们查看index.ios.js 文件,以了解一个简单RN页面的样例代码:

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View
} from 'react-native';

export default class Awesome extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit index.ios.js
        </Text>
        <Text style={styles.instructions}>
          Press Cmd+R to reload,{'\n'}
          Cmd+D or shake for dev menu
        </Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

AppRegistry.registerComponent('Awesome', () => Awesome);

这里我们发现,React Native项目真的与React项目的结构一样,写法一样。(本篇文档面向对象为 了解React的开发者,所以请先了解React)上述代码中,我们看到React nativeReact中不同的地方:

  1. AppRegistry , 需要通过AppRegistry.registerComponent 来注册组件,注册的组件会成为根节点,而原生代码中选择访问一个RN项目中的某个根节点。 注册后,组件成为了Controller
  2. TextView : react native中使用的所有组件都是react native提供的组件,没有web组件。
  3. StyleSheet : React native中的样式与web中的样式基本相同,但是会有一些区别:
    1. React native中的样式名是驼峰式的, 如 border-left-width ,在RN中为 borderLeftWidth
    2. 值类型不同, React Native中的很多值都是字符串类型,如color: '#333333'.
    3. 支持的样式有限,且有些会与浏览器中表现不同。

    所以,在使用React Native时,样式不要以为与css中一样,还是要看官方文档,以了解React Native样式,官方文档中,将样式分为三类: ViewStylePropTypes,TextStylePropTypes,ImageStylePropTypes

总结

这一节中,我们查看了一下react native创建的项目,大致了解react native的逻辑, 使用react native开发原生项目,与使用react开发web项目的区别,就在于使用的组件不同,和样式上稍微有些不同。

接下来介绍React Native中的特殊之处。

平台区分

react-native倾向于区别对待iOS和Android ,我们可以通过两种方式来区分平台。

使用 Platform

这个react native内置module 用来识别当前APP运行的平台。我们可以如下进行判断 :

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  height: (Platform.OS === 'ios') ? 200 : 100,
});

也可以使用PllatformselectAPI :

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    ...Platform.select({
      ios: {
        backgroundColor: 'red',
      },
      android: {
        backgroundColor: 'blue',
      },
    }),
  },
});

可以使用select来为组件选择不同实现 :

const Component = Platform.select({
  ios: () => require('ComponentIOS'),
  android: () => require('ComponentAndroid'),
})();

使用Version来判断Android版本 :

if (Platform.Version === 25) {
  console.log('Running on Nougat!');
}

通过文件后缀名

可以通过文件后缀名实现不同平台不同实现 ,如我们在项目中有两个文件 :

BigButton.ios.js
BigButton.android.js

当我们引入该组件时 :

const BigButton = require('./BigButton');

React Native会主动的根据平台选择正确的文件。

图片处理

React Native处理图片时,要考虑网络图片和本地图片的处理,本地图片要处理路径问题。

Image这个组件,必须要设置图片大小,因为react native认为,如果下载图片完成后,再根据图片大小去布局,会很影响用户体验。

打包图片

跟随js代码一起打包的图片,引入一张图片 :

<Image source={require('./icon.png')} />

require函数会到该js所在路径寻找文件,如果文件有两个icon.ios.pngicon.android.png ,函数能够正确地为不同的平台选择不同的文件。

在iOS中,@2x@3x图片也能够根据屏幕分辨率进行区分。

require函数寻找本地图片,是静态的,其实现是在打包时找到图片真正的路径,并将一些图片信息一起记录下来。 所以require不能动态加载图片 :

var icon = this.props.active ? 'my-icon-active' : 'my-icon-inactive';
<Image source={require('./' + icon + '.png')} />

var icon = this.props.active ? require('./my-icon-active.png') : require('./my-icon-inactive.png');
<Image source={icon} />

APP中图片

hybird app中,我们可以在react native代码中使用APP中的图片(iOS中是放在asset资源中的图片,Android中是 drawable文件夹下的图片) :

<Image source= style= />	

网络图片

// GOOD
<Image source=
       style= />

样式中的颜色

颜色值是一个字符串,但是也支持使用rgbrgba两个函数创建,有以下常见写法:

  • '#f0f' (#rgb)
  • '#ff00ff' (#rrggbb)
  • 'rgb(255, 0, 255)'
  • 'rgba(255, 255, 255, 1.0)'
  • '#f0ff' (#rgba)
  • '#ff00ff00' (#rrggbbaa)
  • 'transparent', 同css中命名颜色。

Timer

计时函数有 :

  • setTimeout, clearTimeout : 设置一段时间后执行任务
  • setInterval, clearInterval : 设置每隔一段时间执行一次
  • setImmediate, clearImmediate : 在一次JavaScript执行完成后,发送调用给Native之前,执行回调。
  • requestAnimationFrame, cancelAnimationFrame :每帧刷新时执行一次

####InteractionManager

使用InteractionManager.runAfterInteractions ,以在动画和交互完成后,执行耗时操作。

TimerMixin

React Native中很多致命错误都是由于定时器 , 经常会出现组件已经被卸载,但计时器仍然在运行。所以React Native提供TimerMixin 。安装 :

npm i react-timer-mixin --save

由于使用es6语法,所以在使用Mixin时,我们要引入react-mixin这个库 ,然后 :

import reactMixin from 'react-mixin';
import TimerMixin from 'react-timer-mixin';
@reactMixin.decorate(TimerMixin)
class Root extends Component {
	componentDidMount: function() {
	    this.setTimeout(
	      () => { console.log('I do not leak!'); },
	      500
	    );
	  }
 }

而要支持注解语法,我们要安装decorators插件 :

npm install babel-plugin-transform-decorators-legacy --save

.babelrc中声明:

{
"presets": ["react-native"],
"plugins": ["transform-decorators-legacy"]
}

VSCode会对注解提示警告 , 我们需要项目根目录下创建 tsconfig.json 文件:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "allowJs": true
    }
}

然后重新加载页面就不会有警告了。

Debug

React Native设置为调试模式时(在RCTDefines.h文件中定义),我们可以呼出开发菜单 ,真机上进行摇晃,而模拟器上使用快捷键⌘D

automatic reloading

自动刷新设置后,每次修改js代码时,app都会自动刷新。

调试状态下错误与警告

错误是红色全屏弹框, 可以通过console.error()创建自己的错误

警告是黄色弹框,会显示在界面底部,可以进行隐藏。 通过console.warn() 来创建自己的警告。

调试模式

点击第二个选项Debug JS Remotely, 会在服务器端通过http://localhost:8081/debugger-ui 打开一个chrome页面,使用快捷键 ⌘⌥J打开调试页面:

React Native正常运行时,使用的引擎都是JavaScriptCore,而在调试模式下,JS运行环境是调试器Chrome的V8引擎,而通过WebSocket与APP进行交互。

react developer tools

安装开发工具 :

npm install -g react-devtools

然后运行开发工具

react-devtools

会打开一个页面 ,在这个页面中,我们可以查看UI的布局以及各种状态值。

Show Inspector

使用Inspector来查看页面层级与布局信息。

Perf Monitor

使用perf Monitor来查看页面性能。

将react-native 嵌入原生iOS应用中

react-native通过npm将库导入到原生应用中。首先,在我们的项目文件的上一层目录(即xcodeproj 文件的上一层目录中),添加一个文件package.json,并添加描述 :

{
  "name": "MyReactNativeApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  }
}

然后在目录下执行命令,以安装React Native

npm install --save react react-native

要在APP中支持React Native,必须引入相关依赖到原生代码中,而iOS开发中,引入依赖一般使用CocoaPods ,所以我们在CocoaPods下添加依赖 :

target 'RNTest' do
  
  pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'DevSupport', # Include this to enable In-App Devmenu if RN >= 0.43
    'RCTText',
    'RCTNetwork',
    'RCTWebSocket', # needed for debugging
    # Add any other subspecs you want to use in your project
  ]
  # Explicitly include Yoga if you are using RN >= 0.42.0
  pod "Yoga", :path => "../node_modules/react-native/ReactCommon/yoga"

end

然后执行pod install 来安装依赖。

个人看法

这里,我们可以看到,Podfile中设置依赖,从npm放置依赖的文件夹node_modules中获取React原生依赖。 这样做,很明显会让纯iOS开发者很不爽, 为什么我在开发iOS代码时,要将你npm的依赖内容放到原生代码的项目中,为什么要去使用npm 。一个莫名其妙的package.json文件放到项目中,会使仓库变得很乱,强加的npm会影响其他iOS开发者。

react-native可能是这样考虑的 ,react-native的代码使用javascript编写,第三方组件是通过npm来管理,且有一些第三方组件可能会有js代码和原生代码两个部分。react-native从自己开发的角度来考虑,肯定是希望完全通过npm来管理所有依赖,避免出现需要在npm和cocoapods同时管理依赖的情况 。

但是它的这种做法,对于大型APP肯定是无法接受的。原生APP中有大量的原生代码和很多纯粹的iOS开发者,React Native只是应该像H5一样嵌入APP这个容器中,而不是去影响APP的开发。所以我们会将React Native 构建一个私有版本,进行一些自己的需求定制,再构建一个纯CocoaPods的依赖(react native就版本也是单独的一个pod依赖,放在主干仓库中,后来为了进行优化而废弃)。而APP安装了依赖后,就拥有了加载React Native项目的能力。

原生接入

安装好依赖之后,我们就要找一个入口,来测试react native了。我们可以直接在Appdelegate中添加以下代码 :

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSString *url = @"http://localhost:8081/index.ios.bundle?platform=ios";
  
  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:[NSURL URLWithString:url]
                                                      moduleName:@"Awesome"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

这里在初始化APP时,以RCTRootView创建一个界面,并加载url指向的react native页面。

服务器端有一个index.ios.js文件,执行命令npm start启动服务器。 然后在浏览器中可以查看服务状态,使用http://localhost:8081/index.ios.js访问的是js原文件,使用http://localhost:8081/index.ios.bundle 访问的是打包好的文件,而platform是必须的参数。

iOS中RCTRootView是RN代码执行的容器,如同html运行在UIWebView中一样。初始RCTRootView时传入三个重要参数 :

  • BundleURL.bundle文件的URL,也就是入口文件打包后的路径。
  • moduleName 是模块名,是一个bundle文件中的模块名。与代码中AppRegistry.registerComponent函数注册的名称相对应。
  • Properties 是参数,会作为props传递给模块名对应的组件。

这样我们就成功将react native嵌入到我们的APP中了,运行以测试效果。

设置出错处理

必须设置。

RCTSetFatalHandler(^(NSError *error) {
	NSLog(@"React Native Error : %@",error);
});