搭建开发环境
- 目标平台: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支持,需要在工程目录下运行以下命令:
- 打开
package.json
文件,在dependencies项中找到react-native
,并将其后的版本号修改为最新版本。 $ npm install
$ 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
(顺序执行)、stagger
和delay
来组合使用。它们中的每一个都接受一个要执行的动画数组,并且自动在适当的时候调用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
来使用。比如在上面的代码片段中,leader
和follower
可以同时为valueXY
类型,这样x和y的值都会被跟踪。
输入事件
Animated.event
是Animated API中与输入有关的部分,允许手势或其它事件直接绑定到动态值上。它通过一个结构化的映射语法来完成,使得复杂事件对象中的值可以被正确的解开。第一层是一个数组,允许同时映射多个值,然后数组的每一个元素是一个嵌套的对象。在下面的例子里,你可以发现scrollX
被映射到了event.nativeEvent.contentOffset.x
(event
通常是回调函数的第一个参数),并且pan.x
和pan.y
分别映射到gestureState.dx
和gestureState.dy
(gestureState
是传递给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
媲美了。