React Native 簡單教程
作為一個 JavaScript 開發者,我之前從來沒想過用 JavaScript 很容易地寫原生移動應用。當然,我們已經有了如 PhoneGap 等工具,但在原生應用中封裝一個基于瀏覽器的應用還有許多需要改進的地方。
現在這一切都改變了—— 非死book 的 React 團隊發布了 React Native。它不僅可以讓我們使用 React 框架來使用原生移動組件創建應用程序,但它使一切成為了現實——這意味著我們在開發應用時不需要重新編譯——這使得它非常容易地創建移動應用!我有幸預覽了 React Native 的 beta 版本,從那開始它大規模成長了起來。
請注意,目前已經支持 iOS。因此你需要運行 OS X 上的 Xcode 來跟隨本教程。
如果你還沒有過機會學習 React, 看看我的教程 來開始用用它吧.
要重點注意學習這個并不意味著我們可以寫一次代碼就能將這段代碼用到每一個地方。嘗試那樣做會因為瘋狂的抽象級別而陷入一場災難。React Native 則讓我們可以學習一次,到處編寫。
回到 2004
如果你關注社交網絡領域的話你會記得 FaceMash,正式這個應用開創了 非死book。對于不關注這個領域的人,其實是11年前(哇塞) Mark Zuckerberg 創建了 FaceMash,它是一個你可以用來查看兩個人之中誰更加熱門的應用程序。每一個人都有一個能反映他們有多“熱門”的分數值 (盡管不知道原來使用的是什么算法,不過那部電影(社交網絡)顯示 Elo 排名算法 曾被使用過) .
它全部的榮耀都在于此 -
讓我們整個來過一遍吧 - 我們準備用 React Native 來重新創建 FaceMash。如果你覺得憑外貌來評價姑娘們不道德,你可以把圖片變成你覺得能吸引人的其它事務(狗狗,代碼塊,等等,我不做評價),隨便。
創建你的應用
如你所愿,你可以從 這兒 clone 到初始的代碼庫。這不是必須的,不過為了不讓你錯過不同階段代碼的不同分支,你可以 clone 一份下來!
休斯頓,我們已經升空
如果你沒有 clone 代碼庫,就需要設置基礎項目. React Native 可以讓我們使用 react-native-cli npm 包 CLI 快速開始一個項目。如果你還沒有安裝這個,可以快速運行命令:
npm install -g react-native-cli
然后我們就可以開始了.
在終端里導航到一個文件夾并運行命令:
react-native init FaceMash
這樣做能為我們準備好基礎到應用程序,供我們挖掘和加入更多東西.
打開它
打開 XCode 并瀏覽到你創建了應用程序的目錄里面. 你需要從這里打開 facemash.xcodeproj。
React Native 支持我們在 iOS 模擬器和實際的 iOS 設備上工作.
我將會在 iOS 模擬器上面進行開發,因為它運行更多快速的應用程序開發 - 當我們修改了JavaScript 時,可以按下 Command + R 組合建來刷新應用,或者我們也可以通過 developer 菜單(通過 Command + Control + Z 就能訪問到)啟用動態重新載入來變成超級懶人。我們設置可以在Chrome的開發者工具中調試我們的代碼。
如果你希望使用你的 iOS 設備來開發你的應用程序,就需要讓設備痛你的計算機處于同一個網絡中。React 默認會在 localhost 找到 JavaScript,所以就需要你將它指向你的計算機.
我們可以通過編輯 AppDelegate.m 文件,將 localhost 改成我們的本地 IP 來達成這個目的. 你可以通過按下 Alt 的同時點擊 wireless 菜單 在 OS X 來找到這個東西.
現在就可以運行我們的應用程序了。應用程序會在你在 XCode 中選擇的目標中打開. 當我們點擊運行,同時會產生一個在我們應用程序目錄中運行著 npm start 的終端線程. 如果你不希望通過 XCode 運行應用,確保你運行了 npm start。這將會創建一個在端口8081上的本地 web 服務器,它指向我們編譯好的 JavaScript 代碼,并且也會監視到我們保存代碼的動作以進行重新編譯。
我把應用程序運行在一個模擬的 iPhone6 之上, 屏幕是真實設備的50%那么大.
這就是了,我們有了一個空的 canvas,有好多空間活動啊!
悄悄來看一看
讓我來看看拿來渲染我們可以在上面的截屏中所看到的東西的代碼. 打開 index.ios.js.
/** * Sample React Native App * https://github.com/非死book/react-native */ 'use strict'; var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, } = React; var facemash = React.createClass({ render: function() { 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+Control+Z for dev menu </Text> </View> ); } }); var 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('facemash', () => facemash);
你可以合上你驚訝的嘴了 - 是的,這就是我們拿來渲染我們的應用程序的全部東西。看起來熟悉,對不對?
React Native vs 瀏覽器
不是 React Native 的所有東西都能滿足你在瀏覽器中使用React的用途. 不過,兩者之間的區別是如此的微不足道,所以完全沒有必要擔心它們.
-
不使用諸如 div 活著 section 之類的塊元素, 我們在React中使用的是View組件. 它會映射到原生的 iOS 組件 UIView.
</li> -
所有的文本都必須被封裝到 Text 組件里面。
</li> -
沒有樣式表 - 你的所有的樣式都是被寫成 JavaScript 對象的。
</li> -
我們沒有必要擔心瀏覽器的兼容性問題 - ES6 harmony 是在盒子之外受到支持的,flexbox也是如此。
</li> </ul>開始工作
我們準備從清理 React 組件的樣式表盒渲染函數開始。為了對 React Native 有一個理想的基本了解,我們將嘗試使用盡可能多的不同組件。
讓我們先從 TabBarIOS 組件開始. 你也許能認出 TabBar 組件來,它被用在諸如時鐘和照片這樣一些核心的iOS應用中。
var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, TabBarIOS } = React; var facemash = React.createClass({ getInitialState() { return { selectedTab: 'faceMash' } }, render: function() { return ( <TabBarIOS> <TabBarIOS.Item title="FaceMash" icon={ require('image!facemash') } selected={ this.state.selectedTab === 'faceMash' }> <View style={ styles.pageView }> <Text>Face Mash</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Messages" icon={ require('image!messages') } selected={ this.state.selectedTab === 'messages' }> <View style={ styles.pageView }> <Text>Messages</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Settings" icon={ require('image!settings') } selected={ this.state.selectedTab === 'settings' }> <View style={ styles.pageView }> <Text>Settings</Text> </View> </TabBarIOS.Item> </TabBarIOS> ); } }); var styles = StyleSheet.create({ pageView: { backgroundColor: '#fff', flex: 1 } }); // omitted code
看看這個!你會注意到當前的文本覆在了狀態條上面,我們稍后會修復這個問題。
TabBarIOS 組件對它的每一個子項都使用了 TabBarIOS.Item。我們將會有三個頁面——分別是你給人們評級的頁面,一個消息列表以及一個設置的頁面。
TabBarIOS.Item 必須有一個子項。他將會是已經被選取的頁面的內容(你可以發現我們會根據組件的狀態來選擇設置成true還是false)。
很明顯,一個 TAB 條沒有圖標不會好看。有幾個系統圖標是你可以拿來用的,不過如果你用了他們的話,TAB 的文字也會發生變化,以與系統的圖標配對. 所以我們會使用自己的圖標。為了在 React Native 中引入本地的圖片資源,你可以使用 require 后面帶上圖片的資源名稱!
我使用的圖標是可以免費拿來用的,來自于 flaticon 的 CC 3.0 許可.
使用靜態圖片
為了向 React Native 添加靜態圖片,請打開 XCode。在 Project Navigator (左手邊的第一個圖標)中, 打開 Images.xcassets 。你所有的圖片都在那兒。
這可以讓我們將所有的資源保持在同一個名稱下,這樣可以針對每一個分辨率、甚至是設備的特定圖片提供不同的圖像資源。
圖像必須遵循一個嚴格的命名約定。使用的資源名稱(比如 messages 或者是 settings) 并在后面給它帶上它應該適用來顯示的分辨率。例如,我要為 iPhone6 構建一個應用程序,我會為此使用 @2x 分辨率。
一旦為你的圖片進行了正確的命名,就可以將它拖入左手邊的 Images.xcassets 中了。
然后你就可以在 React Native 中使用 require('image!assetname') 了!
回到代碼
下一個邏輯步驟就是設置我們的主組件使得 Tab 之間的切換可用。我們可以通過設置用戶點擊它時的狀態來做到。TabBarIOS.Item 讓我們可以給它一個 onPress 屬性,可以拿來檢測用戶何時按下了一個tab。
// omitted code
var facemash = React.createClass({ getInitialState() { return { selectedTab: 'faceMash' } }, changeTab(tabName) { this.setState({ selectedTab: tabName }); }, render: function() { return ( <TabBarIOS> <TabBarIOS.Item title="FaceMash" icon={ require('image!facemash') } onPress={ () => this.changeTab('faceMash') } selected={ this.state.selectedTab === 'faceMash' }> <View style={ styles.pageView }> <Text>Face Mash</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Messages" icon={ require('image!messages') } onPress={ () => this.changeTab('messages') } selected={ this.state.selectedTab === 'messages' }> <View style={ styles.pageView }> <Text>Messages</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Settings" icon={ require('image!settings') } onPress={ () => this.changeTab('settings') } selected={ this.state.selectedTab === 'settings' }> <View style={ styles.pageView }> <Text>Settings</Text> </View> </TabBarIOS.Item> </TabBarIOS> ); } });
// omitted code</pre>
可以了!它是多么的簡單. 通過在 iOS 模擬器中按下 Command+R 來刷新應用(或者如果你是在真實設備上開發,可以通過 XCode 來對它進行重新編譯) 你就會看到現在我們可以進行按下 tab 的操作了,并且主屏幕的顯示也發生了變化!
盡管我們還沒有寫太多的代碼,但是已經見第一個步驟分支的代碼 checkout 出來了,里面也包含了我們在這個 tab 上用上了的圖標。
讓我們打分吧
讓我們來實現 FaceMash 的 tab 界面吧。我們將會從一個端點那里使用獲取來加載到數據。在步驟一的分支中,我已經在 rest/ 目錄中包含進來了一個 config.yaml 文件,那是我們將會用來使用 stubby 對端點進行模擬的。所有 endpoint/pictures 中的用戶都會被從 randomuser.me 處隨機的生成。
打開你的終端并且運行命令
stubby -d rest/config.yaml
接著我們就開始吧!
在名為 tabs/ 的目錄中創建一個新文件,命名為 FaceMash.js,在里面放一個基礎的 React 組件 -
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View } = React;
var facemashTab = React.createClass({ render: function() { return ( <View style={ styles.container }> <Text> FaceMash tab! </Text> </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } });
module.exports = facemashTab;</pre>
目前我們能從這個Tab上得到全部就是一個里面有一些文字的基礎的 View 組件。我們還可以為這個 View 弄一些基礎的樣式,這樣可以確保它具有合適的高和寬。
我們會添加一個頭部,純粹是用于展示的目的.
// omitted code
var facemashTab = React.createClass({ render: function() { return ( <View style={ styles.container }> <View style={ styles.header }> </View> <View style={ styles.content }> <Text> FaceMash tab! </Text> </View> </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, header: { height: 40, background: '#ff0000' } });
module.exports = facemashTab;</pre>
現在我們會抱怨狀態條的黑色很糟糕,不過不要擔心,因為我們可以使用 StatusBarIOS 的 API 來對其進行修改。當 changeTabfunction 被調用時,我們可以檢查看看當前的 tab 是不是 FaceMash 的 tab。如果是的話,我們將會把狀態調的狀態設置為1(白色),如果不是就設置為0(黑色).
index.ios.js
// omitted code
var { AppRegistry, StyleSheet, Text, View, TabBarIOS, StatusBarIOS } = React;
var facemash = React.createClass({ ..., changeTab(tabName) { StatusBarIOS.setStyle(tabName === 'faceMash' ? 1 : 0); this.setState({ selectedTab: tabName }); }, ... });
// omitted code</pre>
刷新你就會看到一個白色的狀態條 - 解決了!
我們現在可以訪問端點來向我們的用戶進行展示了。我們將會使用 fetch,它在 React Native 中默認是被包含了進來的。
// omitted code
var facemashTab = React.createClass({ getInitialState: function() { return { list: [], currentIndex: 0 }; }, componentWillMount: function() { fetch('http://localhost:8882/rest/mash') .then(res => res.json()) .then(res => this.setState({ list: res })); }, render: function() { return ( ... ); } });
// omitted code</pre>
請求會用返回的數據對我們的狀態進行填充。因為初始的數據時一個空的數組,所以我們可以在 render 函數中進行檢查,在他們等待的時候顯示一個加載頁面。
var { StyleSheet, Text, View, ActivityIndicatorIOS } = React;
var facemashTab = React.createClass({ ..., render: function() { var contents; if (!this.state.list.length) { contents = ( <View style={ styles.loading }> <Text style={ styles.loadingText }>Loading</Text> <ActivityIndicatorIOS /> </View> ) } else { contents = ( <View style={ styles.content }> <Text>Loaded</Text> </View> ) } return ( <View style={ styles.container }> <View style={ styles.header }> <Text style={ styles.headerText }>FaceMash</Text> </View> { contents } </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, loading: { flex: 1, backgroundColor: '#fff', justifyContent: 'center', alignItems: 'center' }, loadingText: { fontSize: 14, marginBottom: 20 }, header: { height: 50, backgroundColor: '#760004', paddingTop: 20, alignItems: 'center' }, headerText: { color: '#fff', fontSize: 20, fontWeight: 'bold' } });</pre>
現在我們將對位于 this.state.list 的數據進行訪問。我們也會在端點返回一個對象的數組時,得到位于狀態中的數組的當前索引 - 每個對象都是用戶可以進行評比的兩個人.
因為要從兩個人中選一個,兩者都有同自身相關聯的相同數據,我們將創建一個 React 組件來展示他們的數據。
// omitted code
var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Text>Person!</Text> </View> ) } });
var facemashTab = React.createClass({ getInitialState: function() { return { list: [], currentIndex: 0 }; }, componentWillMount: function() { fetch('http://localhost:8882/rest/mash') .then(res => res.json()) .then(res => this.setState({ list: res })); }, render: function() { var contents; if (!this.state.list.length) { contents = ( <View style={ styles.loading }> <Text style={ styles.loadingText }>Loading</Text> <ActivityIndicatorIOS /> </View> ) } else { var { list, currentIndex } = this.state; var record = list[currentIndex]; var people = record.users.map(person => <Person person={ person } />); contents = ( <View style={ styles.content }> { people } </View> ) } return ( <View style={ styles.container }> <View style={ styles.header }> <Text style={ styles.headerText }>FaceMash</Text> </View> { contents } </View> ); } });
var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' } });</pre>
我們現在就有了一個進行兩次數據裝入(每個人一次)的組件,合適的配置會向它進行傳遞。現在就可以將個人資料圖片和相關的用戶信息展示出來了。
展示外部的圖片
不同于我們的 tab 圖標,我們用來展示的每一個用戶的圖片都來自一個外部的源. 這不是問題,事實上展示它們要比展示靜態資源更加簡單.
我們只是向 Image 組件傳遞一個對象,而不是向它傳入一個需要的圖片. 這個對象會有一個屬性—— url,它會指向我們想要加載的圖片.
當我們將用戶信息作為一個叫做person的屬性進行傳遞時,我們可以通過 this.props.person.picture 訪問到圖片的 URL。
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> </View> ) } });
// omitted code
var styles = StyleSheet.create({ ... person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personImage: { flex: 1, height: 200 }, ... }); module.exports = facemashTab;</pre>
這里也還有一些必要的樣式 - 重新設置圖片的大小難以置信的簡單. 類似的 CSS 屬性,比如 background-size,可以在 React Native 中被應用到圖片之上, 而這里我們智慧在上面放一個 height,而圖片會據此對尺寸進行重新設置.
現在我們可以將剩下的用戶信息添加進去了。
// omitted code
var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <View style={ styles.personInfo }> <Text style={ styles.personName }> { person.firstName } { person.lastName } </Text> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> WON </Text> <Text style={ [styles.personScoreValue, styles.won] }> { person.won } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> LOST </Text> <Text style={ [styles.personScoreValue, styles.lost] }> { person.lost } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> SCORE </Text> <Text style={ styles.personScoreValue }> { person.score } </Text> </View> </View> </View> ) } });
// omitted code
var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personInfo: { borderLeftColor: 'rgba( 0, 0, 0, 0.1 )', borderLeftWidth: 1, borderRightColor: 'rgba( 0, 0, 0, 0.1 )', borderRightWidth: 1, borderBottomColor: 'rgba( 0, 0, 0, 0.1 )', borderBottomWidth: 1, padding: 10, alignItems: 'center', flexDirection: 'row' }, personImage: { flex: 1, height: 200 }, personName: { fontSize: 18, flex: 1, paddingLeft: 5 }, personScore: { flex: 0.25, alignItems: 'center' }, personScoreHeader: { color: 'rgba( 0, 0, 0, 0.3 )', fontSize: 10, fontWeight: 'bold' }, personScoreValue: { color: 'rgba( 0, 0, 0, 0.6 )', fontSize: 16 }, won: { color: '#93C26D' }, lost: { color: '#DD4B39' } }); module.exports = facemashTab;</pre>
你可以從分支二檢出到目前這兒為止的代碼。
現在我們已經讓用戶顯示了出來,可以著手加入點擊時間來讓用戶選擇出誰比較熱門了。
手指觸擊
React Native 為我們提供了 TouchableHighlight 組件. 它能讓我們的View組件正常的響應觸摸. 當它被觸摸時,被封裝視圖的透明度降低了. 這就讓我們的組件“感官上”是可以觸摸的了.
我們準備用這個東西封裝個人信息部分. 將來我們可能想要創建它來讓用戶可以在上面點擊,從而看到更多有關那個人的圖片.
// omitted code
var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <TouchableHighlight> <View style={ styles.personInfo }> <Text style={ styles.personName }> { person.firstName } { person.lastName } </Text> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> WON </Text> <Text style={ [styles.personScoreValue, styles.won] }> { person.won } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> LOST </Text> <Text style={ [styles.personScoreValue, styles.lost] }> { person.lost } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> SCORE </Text> <Text style={ styles.personScoreValue }> { person.score } </Text> </View> </View> </TouchableHighlight> </View> ) } });
// omitted code</pre>
當你重新載入我們所做的修改并且在用戶信息上點擊,會發現它起作用了 - 但看起來有點糟糕. 這是因為我們還沒有在視圖上設置一個背景顏色,其意義是讓整個組件變暗.
// omitted code
var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personInfo: { backgroundColor: '#fff', borderLeftColor: 'rgba( 0, 0, 0, 0.1 )', borderLeftWidth: 1, borderRightColor: 'rgba( 0, 0, 0, 0.1 )', borderRightWidth: 1, borderBottomColor: 'rgba( 0, 0, 0, 0.1 )', borderBottomWidth: 1, padding: 10, alignItems: 'center', flexDirection: 'row' }, ... });
// omitted code</pre>
現在當你在信息盒子上點擊時,它就能正確工作了!
TouchableHighlight 為我們提供了 TouchableWithoutFeedback 也有的一個相同的事件。TouchableWithoutFeedback 不應該被使用,因為所有被觸摸的東西都應該提供某些類型的視覺上可見的反饋。
這樣我們就可以利用 onPress - 它會在用戶已經釋放了觸摸,但是還沒有被打斷 (比如還在讓他們的手指在可觸摸的區域移動)時被調用。
我們需要向下將一個屬性傳遞到我們的 Person 組件,當其被觸摸到時。
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View, Image, ActivityIndicatorIOS, TouchableHighlight } = React;
var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <TouchableHighlight onPress={ this.props.onPress }> ... </TouchableHighlight> </View> ) } });
var facemashTab = React.createClass({ ..., onPersonPress: function() { this.setState({ currentIndex: this.state.currentIndex + 1 }); }, ..., render: function() { var contents; if (!this.state.list.length) { contents = ( <View style={ styles.loading }> <Text style={ styles.loadingText }>Loading</Text> <ActivityIndicatorIOS /> </View> ) } else { var { list, currentIndex } = this.state; var record = list[currentIndex]; var people = record.users.map(person => <Person person={ person } onPress={ this.onPersonPress } />); contents = ( <View style={ styles.content }> { people } </View> ) } return ( <View style={ styles.container }> <View style={ styles.header }> <Text style={ styles.headerText }>FaceMash</Text> </View> { contents } </View> ); } });
// omitted code</pre>
如你所見,在你的主TAB組件里面現在有了一個 onPersonPress 屬性. 然后我們就可以將這個傳到 Person 組件那兒, 而它們會在 TouchableHighlight 區域被觸摸時調用到它. 而后我們可以增加索引,視圖就會用新的人物集合來進行重新渲染.
這是對 facemash 的 tab 所做的最后修改. 如果你希望走得更遠,下面是一些好主意
-
當選取了一個人物時可以去請求一個 REST 的端點
</li> -
檢查是否已經到達了列表的盡頭,顯示一條消息
</li> -
讓用戶可以在照片上面點擊來看更多的照片
</li> </ul>你可以在分支三上面檢出 facemash 的 tab 的最終代碼。
消息
我們現在將注意力轉移到消息tab上了。這一功能有點像 iMessage - 它是有關用戶的一個可滾動列表,在其中一項上面點擊將會導航至一個針對那個用戶的聊天視圖。
幸運的是, React Native 給了我們 ListView 組件,它能讓我們擁有一個簡單的,(使用了ScrollView的)可滾動列表,而且能高效的顯示出列表(只對發生變化的行進行重新渲染,并限制了每次事件循環渲染的行只有一個)。
為了使用一個 ListView, 我們需要有一個數據源. 數據元讓我們可以擁有一個定制的函數來檢查一行是不是發生了變化 (可以想到它類似于 toshouldComponentUpdate) ,我們可以把JSON數據放到它里面去. 數據源存在于我們的狀態對象之中。
在 thetabs/folder 下創建一個名為 Messages.js 的新文件
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View, Image } = React;
var messagesTab = React.createClass({ render: function() { return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } });
module.exports = messagesTab;</pre>
你同時需要編輯 editindex.ios.jsto 來指向新創建的組件 -
// omitted code
var MessagesTab = require('./tabs/Messages');
var facemash = React.createClass({ getInitialState() { return { selectedTab: 'faceMash' } }, changeTab(tabName) { StatusBarIOS.setStyle(tabName === 'faceMash' ? 1 : 0); this.setState({ selectedTab: tabName }); }, render: function() { return ( <TabBarIOS> ... <TabBarIOS.Item title="Messages" icon={ require('image!messages') } onPress={ () => this.changeTab('messages') } selected={ this.state.selectedTab === 'messages' }> <MessagesTab /> </TabBarIOS.Item> ... </TabBarIOS> ); } });
// omitted code</pre>
根據我上面所說的,我們列表視圖需要的一個數據源。這個可以通過 viaListView.DataSource 訪問。我們將會在 ourgetInitialState 賦初值。
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View, Image, ListView } = React;
var messagesTab = React.createClass({ getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, render: function(){ return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } });
module.exports = messagesTab;</pre>
現在我們已經獲取到數據源,需要從服務器獲取一些數據,知道我們跟誰在通信。我已經包含在一個端點配置中,在 thestep-threebranch 顯得更短。
// omitted code
var messagesTab = React.createClass({ componentWillMount: function() { fetch('http://localhost:8882/rest/messages') .then(res => res.json()) .then(res => this.updateDataSource(res)); }, getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, updateDataSource: function(data){ this.setState({ dataSource: this.state.dataSource.cloneWithRows(data) }) }, render: function(){ return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } });
// omitted code</pre>
這個會獲取到我們的數據,然后更新我們的數據源。這些處理完之后,無論什么時候我們的數據源更新了,我們的視圖也會更新的。
現在我們來看看 ListViewcomponent。簡單的說, 組件需要兩個屬性 -dataSource(我們已經有了) 。andrenderRow.renderRow 是一個需要返回一個 React 元素的函數。在數據源中每一行都會調用一次,然后作為參數為每一行傳遞合適的數據。
// omitted code
var messagesTab = React.createClass({ ..., renderRow: function (){ return ( <View> <Text>Row goes here!</Text> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } });
// omitted code</pre>
我們現在每一行都可以顯示正確的數據。每個項的對象都作為第一個參數傳遞給 ourrenderRowfunction。
// omitted code
var messagesTab = React.createClass({ ..., renderRow: function (person){ return ( <View> <Text>{ person.user.firstName } { person.user.lastName }</Text> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } });
// omitted code</pre>
我們繼續我們的步驟,在這里添加其他的信息,比如圖片和最新接收到的信息。
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View, Image, ListView, PixelRatio } = React;
function prettyTime(timestamp) { var createdDate = new Date(timestamp); var distance = Math.round( ( +new Date() - timestamp ) / 60000 ); var hours = ('0' + createdDate.getHours()).slice(-2); var minutes = ('0' + createdDate.getMinutes()).slice(-2); var month = ('0' + (createdDate.getMonth() + 1)).slice(-2); var date = ('0' + createdDate.getDate()).slice(-2); var year = createdDate.getFullYear(); var string; if (distance < 1440) { string = [hours, minutes].join(':'); } else if (distance < 2879) { string = 'Yesterday'; } else { string = [date, month, year].join('/'); } return string; }
var messagesTab = React.createClass({ ..., renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, row: { flex: 1, alignItems: 'center', backgroundColor: 'white', flexDirection: 'row', padding: 10 }, textContainer: { flex: 1 }, cellImage: { height: 60, borderRadius: 30, marginRight: 10, width: 60 }, time: { position: 'absolute', top: 0, right: 0, fontSize: 12, color: '#cccccc' }, name: { flex: 1, fontSize: 16, fontWeight: 'bold', marginBottom: 2 }, lastMessage: { color: '#999999', fontSize: 12 }, cellBorder: { backgroundColor: 'rgba(0, 0, 0, 0.1)', height: 1 / PixelRatio.get(), marginLeft: 4 } });
module.exports = messagesTab;</pre>
Looking good!
你會注意到我們在樣式表中使用了一個叫做 PixelRatio 的東西. 用這個我們就可以得到能夠拿來在設備上顯示的最細的線. 一般,我們會用 1px 作為最細的邊框,但是 React Native 中沒有 px 的概念。
現在我們可以添加代碼來處理在用戶項上面的觸摸了。我們將使用 NavigatorIOS 組件 - 你會在諸如 iMessage 和 Notes 這樣的應用上看到這個東西. 它能讓我們獲得視圖之間的回退功能,頂端的導航條也會如此。
實際上我們準備創建一個新的 React 組件來裝這個導航。這是因為組件需要對一個初始的 React 組件進行渲染。
我們會將 messagesTab 組件改稱做 messageList,并創建另外一個叫做 messagesTab 的組件
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View, Image, ListView, PixelRatio, NavigatorIOS } = React; // omitted code
var messageList = React.createClass({ ..., render: function(){ return ( <View style={ > <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } });
var messagesTab = React.createClass({ render: function() { return ( <NavigatorIOS style={ styles.container } initialRoute={ { title: 'Messages', component: messageList } } /> ); } });
// omitted code</pre>
看看已經變得更專業了哦。就像我們在 facemash 的 Tab 中所做的,我們現在可以向行中添加觸摸時高亮效果( TouchableHighlight) 了。
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View, Image, ListView, PixelRatio, NavigatorIOS, TouchableHighlight } = React;
// omitted code
var messageList = React.createClass({ componentWillMount: function() { fetch('http://localhost:8882/rest/messages') .then(res => res.json()) .then(res => this.updateDataSource(res)); }, getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, updateDataSource: function(data){ this.setState({ dataSource: this.state.dataSource.cloneWithRows(data) }) }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </TouchableHighlight> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } });
// omitted code</pre>
重新加載然后,你會收到一個錯誤。這是因為我們傳了兩個子組件到 TouchableHighlight,但它只能很好的拿一個來進行顯示。不要擔心啦,我們還可以把這倆子組件封裝到另外一個 View 組件中來解決問題啊。
// omitted code
var messageList = React.createClass({ ..., renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight> <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> </TouchableHighlight> </View> ); }, ... });
// omitted code</pre>
現在,當我們在一行上面觸摸時,就會收到我們預期的效果了。等等 - 我們的底部邊框看起來怪怪的!這是因為我們使用的是 rgba 值。整個視圖的背景顏色正在發生變化,這意味著我們的邊框隨后會變得更暗。不要擔心,我們可以給它一個十六進制值的。
var styles = StyleSheet.create({ ..., cellBorder: { backgroundColor: '#F2F2F2', height: 1 / PixelRatio.get(), marginLeft: 4 } });
如上所述的代碼你可以在分支四中看到。
按下和彈出
現在我們已經讓主列表有了樣式,可以來處理用戶觸摸時導航發生的變化了。
NavigatorIOS 讓我們可以在想要改變當前的路由時,“按下”到組件的里面去。為此,我們需要子組件以及 messagList 中能有某些方式能訪問到 NavigatorIOS 實體。幸運的是,這個已經以叫做 navigator 的屬性傳入了。
讓我們向 TouchableHighlight 組件加入一個 onPress 事件吧.
// omitted code
var messageList = React.createClass({ ..., openChat: function (user){ }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }> ... </TouchableHighlight> </View> ); }, ... });
// omitted code</pre>
現在我們需要一個 React 組件來傳遞到 navigator。請在 tabs/ 文件夾中創建一個叫做 MessageView.js 的新文件。
'use strict';
var React = require('react-native'); var { StyleSheet, Text, View } = React;
var messageView = React.createClass({ render: function(){ return ( <View style={ styles.container }> <Text>Message view!</Text> </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, });
module.exports = messageView;</pre>
我們可以將這個包含到 Messages.js 文件中,并將它放到 navigator 那兒去。
// omitted code
var messageList = React.createClass({ ..., openChat: function (user){ this.props.navigator.push({ title:
${user.firstName} ${user.lastName}
, component: MessageView, passProps: { user } }); }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }> <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> </TouchableHighlight> </View> ); }, ... });// omitted code</pre>
從這兒你可以看到我們將下一個路由壓入了 navigator。在這兒我們可以設置下一個路由的標題 (用戶的姓名),渲染什么組件 (我們新創建的 MessageView) 以及傳入什么屬性。這讓我們可以訪問我們在 MessageView 組件中需要的任何東西 (我們準備傳入用戶對象)。
這對于我們聊天列表中的每一個都會起作用,不管其數量多還是少。
不過,文本會被我們新的標題條切段。解決這個問題,主要在第一個 View 組件上放一個內邊距(padding)就可以了。
我們現在已經有一個用戶屬性被傳進來了,同樣可以對其進行展示!
MessageView.js
'use strict';
var React = require('react-native');
var { StyleSheet, Text, View } = React; var messageView = React.createClass({ render: function(){ var user = this.props.user; return ( <View style={ styles.container }> <Text>Chat with { user.firstName } { user.lastName }</Text> </View> ); } });
var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', paddingTop: 64 }, });
module.exports = messageView;</pre>
你可以在step-five分支檢測出上面的代碼。
如果你想在這個tab上做更多的事情,下面是一些好主意
-
當MessageView載入時獲取那個用戶的聊天信息
</li> -
嘗試實現下拉刷新 - 提示:使用ListView上的renderHeader屬性
</li> -
在用戶的姓名右側添加一個“設置(Settings)" 按鈕
</li> </ul>總結
請自由地在設置 tab 中加入一些功能。嘗試使用其他的組件,比如 DatePickerIOS 遺跡 TextInput,來做一些通用的設置 (DOB,name,等等)。
希望這里的討論能觸及一些同你在你的 React Native 應用程序中通常會用到有所不同的組件。如有任何疑問,可以在推特 @rynclark 上聯系我.
-
-