React-native入门

搭建开发环境

  • 目标平台:IOS
  • 开发平台:macOS

安装

必需的软件

Homebrew

Homebrew, Mac系统的包管理器,用于安装NodeJS和一些其他必需的工具软件。

/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

使用Homebrew来安装node.js.

React Native目前需要NodeJS 5.0或更高版本。本文发布时Homebrew默认安装的是最新版本,一般都满足要求。

brew install node

安装完node后建议设置npm镜像以加速后面的过程(或使用科学上网工具)。注意:不要使用cnpm!cnpm安装的模块路径比较奇怪,packager不能正常识别!

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

Yarn、React Native的命令行工具(react-native-cli)

Yarn是Facebook提供的替代npm的工具,可以加速node模块的下载。React Native的命令行工具用于执行创建、初始化、更新项目、运行打包服务(packager)等任务。

npm install -g yarn react-native-cli

安装完yarn后同理也要设置镜像源:

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

如果你看到EACCES: permission denied这样的权限报错,那么请参照上文的homebrew译注,修复/usr/local目录的所有权:

sudo chown -R `whoami` /usr/local
Xcode

React Native目前需要Xcode 8.0 或更高版本。你可以通过App Store或是到Apple开发者官网上下载。这一步骤会同时安装Xcode IDE和Xcode的命令行工具。

虽然一般来说命令行工具都是默认安装了,但你最好还是启动Xcode,并在Xcode | Preferences | Locations菜单中检查一下是否装有某个版本的Command Line Tools。Xcode的命令行工具中也包含一些必须的工具,比如git等。

推荐安装的工具

Watchman

Watchman是由Facebook提供的监视文件系统变更的工具。安装此工具可以提高开发时的性能(packager可以快速捕捉文件的变化从而实现实时刷新)。

brew install watchman

Flow

Flow是一个静态的JS类型检查工具。译注:你在很多示例中看到的奇奇怪怪的冒号问号,以及方法参数中像类型一样的写法,都是属于这个flow工具的语法。这一语法并不属于ES标准,只是Facebook自家的代码规范。所以新手可以直接跳过(即不需要安装这一工具,也不建议去费力学习flow相关语法)。

brew install flow
vscode

推荐使用vscode。安装以下插件:

  • Document this

  • EditorConfig for VS Code

  • EsLint

  • Flow Language Support

  • JavaScript (ES6) code snippets

  • jsx

  • React Native Tools

  • Reactjs code snippet

  • vscode-todo

测试安装

react-native init AwesomeProject
cd AwesomeProject
react-native run-ios

修改项目

  • 使用你喜欢的编辑器打开index.ios.js并随便改上几行。
  • 在iOS Emulator中按下⌘-R就可以刷新APP并看到你的最新修改!

为已有的React Native工程添加Android支持

如果已经有了一个只有IOS版本的React Native工程,并且希望添加Android支持,需要在工程目录下运行以下命令:

  1. 打开package.json文件,在dependencies项中找到react-native,并将其后的版本号修改为最新版本
  2. $ npm install
  3. $ react-native android

动画

在React Native中,我们已经可以联合使用两个互补的系统:用于全局的布局动画LayoutAnimation,和用于创建更精细的交互控制的动画Animated.

Animated

Animated仅关注动画的输入与输出声明,在其中建立一个可配置的变化函数,然后使用简单的start/stop方法来控制动画按顺序执行。

class Playground extends React.Component {
  constructor(props: any) {
    super(props);
    this.state = {
      bounceValue: new Animated.Value(0),
    };
  }
  render(): ReactElement {
    return (
      <Animated.Image                         // 可选的基本组件类型: Image, Text, View
        source={{uri: 'http://i.imgur.com/XMKOH81.jpg'}}
        style={{
          flex: 1,
          transform: [                        // `transform`是一个有序数组(动画按顺序执行)
            {scale: this.state.bounceValue},  // 将`bounceValue`赋值给 `scale`
          ]
        }}
      />
    );
  }
  componentDidMount() {
    this.state.bounceValue.setValue(1.5);     // 设置一个较大的初始值
    Animated.spring(                          // 可选的基本动画类型: spring, decay, timing
      this.state.bounceValue,                 // 将`bounceValue`值动画化
      {
        toValue: 0.8,                         // 将其值以动画的形式改到一个较小值
        friction: 1,                          // Bouncier spring
      }
    ).start();                                // 开始执行动画
  }
}

bounceValue在构造函数中初始化为state的一部分,然后和图片的缩放比例进行绑定。在动画执行的背后,其数值会被不断的计算并用于设置缩放比例。当组件刚刚挂载的时候,缩放比例被设置到1.5。然后紧跟着在bounceValue上执行了一个弹跳动画(spring),会逐帧刷新数值,并同步更新所有依赖本数值的绑定(在这个例子里,就是图片的缩放比例)。比起调用setState然后重新渲染,这一运行过程要快得多。因为整个配置都是声明式的,我们可以实现更进一步的优化,只要序列化好配置,然后我们可以在一个高优先级的线程执行动画。

核心API

我们所需要的东西都来自于Animated模块。包括两个值类型,Value用于单个值,而ValueXY用于向量值;还包括三种动画类型,spring, decay, 还有timing, 以及三种组件类型,View Text, Image。我们可以使用Animated.createAnimatedComponent方法来对其它类型的组件创建动画。

这三种动画类型可以用来创建几乎任何你需要的动画曲线,因为它们每一个都可以被自定义:

  • spring: 基础的单次弹跳物理模型

    friction: 摩擦力,默认为7.

    tension: 张力,默认40。

  • decay: 以一个初始速度开始并且逐渐减慢停止。

    velocity: 起始速度,必填参数。

    deceleration: 速度衰减比例,默认为0.997。

  • timing: 从时间范围映射到渐变的值。

    duration: 动画持续的时间(单位是毫秒),默认为500。

    easing:一个用于定义曲线的渐变函数。阅读Easing模块可以找到许多预定义的函数。iOS默认为Easing.inOut(Easing.ease)

    delay: 在一段时间之后开始动画(单位是毫秒),默认为0。

动画可以通过调用start方法来开始。start接受一个回调函数,当动画结束的时候会调用此回调函数。如果动画是因为正常播放完成而结束的,回调函数被调用时的参数为{finished: true},但若动画是在结束之前被调用了stop而结束(可能是被一个手势或者其它的动画打断),它会收到参数{finished: false}

动画组合

多个动画可以通过parallel(同时执行)、sequence(顺序执行)、staggerdelay来组合使用。它们中的每一个都接受一个要执行的动画数组,并且自动在适当的时候调用start/stop。举个例子:

Animated.sequence([            // 首先执行decay动画,结束后同时执行spring和twirl动画
  Animated.decay(position, {   // 滑行一段距离后停止
    velocity: {x: gestureState.vx, y: gestureState.vy}, // 根据用户的手势设置速度
    deceleration: 0.997,
  }),
  Animated.parallel([          // 在decay之后并行执行:
    Animated.spring(position, {
      toValue: {x: 0, y: 0}    // 返回到起始点开始
    }),
    Animated.timing(twirl, {   // 同时开始旋转
      toValue: 360,
    }),
  ]),
]).start();                    // 执行这一整套动画序列

默认情况下,如果任何一个动画被停止或中断了,组内所有其它的动画也会被停止。Parallel有一个stopTogether属性,如果设置为false,可以禁用自动停止。

跟踪动态值

动画中所设的值还可以通过跟踪别的值得到。你只要把toValue设置成另一个动态值而不是一个普通数字就行了。比如我们可以用弹跳动画来实现聊天头像的闪动,又比如通过timing设置duration:0来实现快速的跟随。他们还可以使用插值来进行组合:

Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
  toValue: pan.x.interpolate({
    inputRange: [0, 300],
    outputRange: [1, 0],
  }),
}).start();

ValueXY是一个方便的处理2D交互的办法,譬如旋转或拖拽。它是一个简单的包含了两个Animated.Value实例的包装,然后提供了一系列辅助函数,使得ValueXY在许多时候可以替代Value来使用。比如在上面的代码片段中,leaderfollower可以同时为valueXY类型,这样x和y的值都会被跟踪。

输入事件

Animated.event是Animated API中与输入有关的部分,允许手势或其它事件直接绑定到动态值上。它通过一个结构化的映射语法来完成,使得复杂事件对象中的值可以被正确的解开。第一层是一个数组,允许同时映射多个值,然后数组的每一个元素是一个嵌套的对象。在下面的例子里,你可以发现scrollX被映射到了event.nativeEvent.contentOffset.x(event通常是回调函数的第一个参数),并且pan.xpan.y分别映射到gestureState.dxgestureState.dygestureState是传递给PanResponder回调函数的第二个参数)。

onScroll={Animated.event(
  [{nativeEvent: {contentOffset: {x: scrollX}}}]   // scrollX = e.nativeEvent.contentOffset.x
)}
onPanResponderMove={Animated.event([
  null,                                          // 忽略原生事件
  {dx: pan.x, dy: pan.y}                         // 从gestureState中解析出dx和dy的值
]);
响应当前的动画值

你可能会注意到这里没有一个明显的方法来在动画的过程中读取当前的值——这是出于优化的角度考虑,有些值只有在原生代码运行阶段中才知道。如果你需要在JavaScript中响应当前的值,有两种可能的办法:

  • spring.stopAnimation(callback)会停止动画并且把最终的值作为参数传递给回调函数callback——这在处理手势动画的时候非常有用。
  • spring.addListener(callback) 会在动画的执行过程中持续异步调用callback回调函数,提供一个最近的值作为参数。这在用于触发状态切换的时候非常有用,譬如当用户拖拽一个东西靠近的时候弹出一个新的气泡选项。不过这个状态切换可能并不会十分灵敏,因为它不像许多连续手势操作(如旋转)那样在60fps下运行。

LayoutAnimation

LayoutAnimation允许你在全局范围内创建更新动画,这些动画会在下一次渲染或布局周期运行。它常用来更新flexbox布局,因为它可以无需测量或者计算特定属性就能直接产生动画。尤其是当布局变化可能影响到父节点(譬如“查看更多”展开动画既增加父节点的尺寸又会将位于本行之下的所有行向下推动)时,如果不使用LayoutAnimation,可能就需要显式声明组件的坐标,才能使得所有受影响的组件能够同步运行动画。

注意尽管LayoutAnimation非常强大且有用,但它对动画本身的控制没有Animated或者其它动画库那样方便,所以如果你使用LayoutAnimation无法实现一个效果,那可能还是要考虑其他的方案。

requestAnimationFrame

requestAnimationFrame是一个对浏览器标准API的兼容实现,你可能已经熟悉它了。它接受一个函数作为唯一的参数,并且在下一次重绘之前调用此函数。一些基于JavaScript的动画库高度依赖于这一API。通常你不必直接调用它——那些动画库会替你管理好帧的更新。

关于setNativeProps

setNativeProps方法可以使我们直接修改基于原生视图的组件的属性,而不需要使用setState来重新渲染整个组件树。如果我们发现动画丢帧,可以尝试使用setNativeProps或者shouldComponentUpdate来优化它们。有时候可能还需要将部分计算工作放在动画完成之后进行,这时候可以使用interactionManager

定时器

定时器是一个应用中非常重要的部分。React Native实现了和浏览器一致的定时器Timer

定时器

  • setTimeout, clearTimeout
  • setInterval, clearInterval
  • setImmediate, clearImmediate
  • requestAnimationFrame, cancelAnimationFrame

requestAnimationFrame(fn)setTimeout(fn, 0)不同,前者会在每帧刷新之后执行一次,而后者则会尽可能快的执行(在iPhone5S上有可能每秒1000次以上)。

setImmediate则会在当前JavaScript执行块结束的时候执行,就在将要发送批量响应数据到原生之前。注意如果你在setImmediate的回调函数中又执行了setImmediate,它会紧接着立刻执行,而不会在调用之前等待原生代码。

Promise的实现就使用了setImmediate来执行异步调用。

InteractionManager

原生应用感觉如此流畅的一个重要原因就是在互动和动画过程中避免繁重的操作。在React Native中,我们目前受到限制,因为我们只有一个JavaScript执行线程。不过我们可以使用InteractionManager来确保在执行繁重工作之前所有的交互和动画都已处理完毕。应用可以通过以下代码来安排一个任务,使其在交互之后执行:

InteractionManager.runAfterInteractions(() => {
  // ...需要长时间同步执行
})

我们来把它和之前的几个任务安排方法对比一下:

  • requestAnimationFrame(): 用来执行在一段时间内控制视图动画的代码
  • setImmediate/setTimeout/setInterval(): 在稍后执行代码。注意这有可能会延迟当前正在进行的动画。
  • runAfterInteractions(): 在稍后执行代码,不会延迟当前进行的动画。

触摸处理系统会把一个或多个进行中的触摸操作认定为'交互',并且会将runAfterInteractions()的回调函数延迟执行,直到所有的触摸操作都结束或取消了。

InteractionManager还允许应用注册动画,在动画开始时创建一个交互“句柄”,然后在结束的时候清除它。

var handle = InteractionManager.createInteractionHandle();
// 执行动画... (`runAfterInteractions`中的任务现在开始排队等候)
// 在动画完成之后
InteractionManager.clearInteractionHandle(handle);
// 在所有句柄都清除之后,现在开始依序执行队列中的任务

TimerMixin

我们发现很多React Native应用发生致命错误(闪退)是与计时器有关。具体来说,是在某个组件被卸载(unmount)之后,计时器却仍然被激活。为了解决这个问题,我们引入了TimerMixin。如果你在组件中引入TimerMixin,就可以把你原本的setTimeout(fn, 500)改为this.setTimeout(fn, 500)(只需要在前面加上this.),然后当你的组件卸载时,所有的计时器事件也会被正确的清除。

这个库并没有跟着React Native一起发布。你需要在项目文件夹下输入npm i react-timer-mixin --save来单独安装它。

var TimerMixin = require('react-timer-mixin');

var Component = React.createClass({
  mixins: [TimerMixin],
  componentDidMount: function() {
    this.setTimeout(
      () => { console.log('这样我就不会导致内存泄露!'); },
      500
    );
  }
});

注意:Mixin属于ES5语法,对于ES6来说,无法直接使用Mixin。如果项目使用的是ES6代码编写,同时又使用定时器,那么你只需要铭记在unmount组件时清除(clearTimeout/clearInterval)所有用到的定时器,那么也可以实现和TimerMixin同样的效果。

import React,{
  Component
} from 'react';

export default class Hello extends Component {
  componentDidMount() {
    this.timer = setTimeout(
      () => { console.log('把一个定时器的引用挂在this上'); },
      500
    );
  }
  componentWillUnmount() {
    // 如果存在this.timer,则使用clearTimeout清空。
    // 如果你使用多个timer,那么用多个变量,或者用个数组来保存引用,然后逐个clear
    this.timer && clearTimeout(this.timer);  //true&&表达式   执行表达式
  }
};

直接操作

有时候我们需要直接改动组件并触发局部的刷新,但不使用state或是props。在React Native中,setNativeProps就是等价于直接操作DOM节点的方法。

在(不得不)频繁刷新而又遇到瓶颈时,我们会使用setNativeProps;

直接操作组件并不是应该经常使用的工具。一般来说只是用来创建连续的动画,同时避免渲染组件结构和同步太多视图变化所带来的大量开销。setNativeProps是一个“简单粗暴”的方法,它直接在底层(DOM、UIView等)而不是React组件中记录state,这样会使代码逻辑难以理清。所以,我们建议先尝试用setState和shouldComponentUpdate方法来解决问题。

setNativeProps与TouchableOpacity

TouchableOpacity这个组件就在内部使用了setNativeProps方法来更新其子组件的透明度:

setOpacityTo: function(value) {
  // Redacted: animation related code
  this.refs[CHILD_REF].setNativeProps({
    opacity: value
  });
},

由此我们可以写出下面这样的代码:子组件可以响应点击事件,更改自己的透明度。而子组件自身并不需要处理这件事情,也不需要在实现中做任何修改。

<TouchableOpacity onPress={this._handlePress}>
  <View style={styles.button}>
    <Text>Press me!</Text>
  </View>
</TouchableOpacity>

如果不使用setNativeProps这个方法来实现这一需求,那么一种可能的办法是把透明值保存到state中,然后在onPress事件触发时更新这个值:

getInitialState() {
  return { myButtonOpacity: 1, }
},

render() {
  return (
    <TouchableOpacity onPress={() => this.setState({myButtonOpacity: 0.5})}
                      onPressOut={() => this.setState({myButtonOpacity: 1})}>
      <View style={[styles.button, {opacity: this.state.myButtonOpacity}]}>
        <Text>Press me!</Text>
      </View>
    </TouchableOpacity>
  )
}

比起之前的例子,这一做法会消耗大量的计算 —— 每一次透明值变更的时候React都要重新渲染组件结构,即便视图的其他属性和子组件并没有变化。一般来说这一开销也不足为虑,但当执行连续的动画以及响应用户手势的时候,只有正确地优化组件才能提高动画的流畅度。

如果你看过NativeMethodsMixin.js中的setNativeProps方法的实现,你就会发现它实际是对RCTUIManager.updateView的封装 —— 而这正是重渲染所触发的函数调用,具体可以参看ReactNativeBaseComponent.js中的receiveComponent.

复合组件与setNativeProps

复合组件并不是单纯的由一个原生视图构成,所以我们不能直接使用setNativeProps。我们可以尝试这样去理解:如果是通过React.createClass方法自定义了一个组件,直接给它设置样式prop是不会生效的,你得把样式props层层向下传递给子组件,直到子组件是一个能够直接定义样式的原生组件。同理,我们也需要把setNativeProps传递给由原生组件封装的子组件。

将setNativeProps传递给子组件

具体的做法就是在我们的自定义组件中再封装一个setNativeProps方法,它的内容为对合适的子组件调用真正的setNativeProps方法,并传递要设置的参数。

var MyButton = React.createClass({
  setNativeProps(nativeProps) {
    this._root.setNativeProps(nativeProps);
  },

  render() {
    return (
      <View ref={component => this._root = component} {...this.props}>
        <Text>{this.props.label}</Text>
      </View>
    )
  },
});

现在可以在TouchableOpacity中嵌入MyButton了!

我们在向下传递props时使用了{...this.props}语法(这一用法的说明请参考对象的扩展运算符)。这是因为TouchableOpacity本身其实也是个复合组件, 它除了要求在子组件上执行setNativeProps 以外,还要求子组件对触摸事件进行处理。因此,它会传递多个props,其中包含了onmoveshouldsetresponder 函数,这个函数需要回调给TouchableOpacity组件,以完成触摸事件的处理。与之相对的是TouchableHighlight组件,它本身是由原生视图构成,因而只需要我们实现setNativeProps

setNativeProps与shouldComponentUpdate

通过使用shouldComponentUpdate方法,可以避免重新渲染那些实际没有发生变化的子组件所带来的额外开销,此时使用setState的性能已经可以与setNativeProps媲美了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,825评论 25 707
  • 作为一名Android开发,学习React Native其实是一个很陡峭的过程,本文主要记录自己从接触React ...
    stefanli阅读 2,622评论 2 15
  • 概述 随着app端越来越复杂,迭代越来越快,很多app采用原生+html5的方式来实现,然后不知道什么时候,它就有...
    街角仰望阅读 1,211评论 3 8
  • 背景 随着各种技术的发展,目前市场有各种形式的App,当前主要如下几类: . Web App(SUI,MUI,io...
    这个博客有点懒阅读 282评论 0 1
  • 她叫张梦琪,是个单亲孩子,是被爸爸捡回来的,以前,爸爸的生意很好,家境还不错。可是自从那次意外,一切都变了。 ...
    LOVE小凯阅读 185评论 0 1