搭建開發環境
- 目標平臺: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
媲美了。