商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
作者:kmokidd
鏈接:http://zhuanlan.zhihu.com/FrontendMagazine/19996445
來源:知乎
數月前,Facebook 對外宣佈了正在開發的 React Native 框架,這個框架允許你使用 JavaScript 開發原生的 iOS 應用——就在今天,Beta 版的倉庫釋出了!
基於 PhoneGap 使用 JavaScript 和 HTML5 開發 iOS 應用已經有好幾年了,那 React Native 有什麼牛的?
React Native 真的很牛,讓大家興奮異常的主要原因有兩點:
-
可以基於 React Native使用 JavaScript 編寫應用邏輯,UI 則可以保持全是原生的。這樣的話就沒有必要就 HTML5 的 UI 做出常見的妥協;
-
React 引入了一種與衆不同的、略顯激進但具備高可用性的方案來構建用戶界面。長話短說,應用的 UI 簡單通過一個基於應用目前狀態的函數來表達。
React Native 的關鍵就是,以把 React 編程模式的能力帶到移動開發來作爲主要目標。它的目標不是跨平臺一次編寫到處執行,而是一次學習跨平臺開發。這個是一個非常大的區別。這篇教程只介紹 iOS 平臺,不過你一旦掌握了相關的概念,就可以應用到 Android 平臺,快速構建 Android 應用。
如果之前只用過 Objective-C 或者 Swift 寫應用的話,你很可能不會對使用 JavaScript 來編寫應用的願景感到興奮。儘管如此,作爲一個 Swift 開發者來說,上面提到的第二點應該可以激起你的興趣!
你通過 Swift,毫無疑問學習到了新的更多有效的編碼方法和技巧,鼓勵轉換和不變性。然而,構建 UI 的方式還是和使用 Objective-C 的方式一致。仍然以 UIKit 爲基礎,獨斷專橫。
通過像 virtual DOM 和 reconciliation 這些有趣的概念,React 將函數式編程直接帶到了 UI 層。
這篇教程將帶着你一路構建一個 UK 房產搜索應用:
如果你之前一點 JavaScript 都沒寫過,別擔心。這篇教程帶着你進行一步一步進行編碼。React 使用 CSS 屬性來定義樣式,一般比較容易讀也比較容易理解。但是如果你想了解更多的話,可以去看看 Mozilla Developer Network reference,很不錯的。
想要學習更多,繼續往下讀!
準備工作
React Native 框架託管在 GitHub 上。你可以通過兩種方式獲取到它:使用 git 克隆倉庫,或者下載一個 zip 壓縮包文件。如果你的機器上已經安裝了 React Native,在着手編碼前還有其他幾個因素需要考慮。
- React Native 藉助 Node.js,即 JavaScript 運行時來創建 JavaScript 代碼。如果你已經安裝了 Node.js,那就可以上手了。
首先,使用 Homebrew 官網提供的指引安裝 Homebrew,然後在終端執行以下命令:
brew install node
接下來,使用 homebrew 安裝 watchman,一個來自Facebook 的觀察程序:
brew install watchman
通過配置 watchman,React 實現了在代碼發生變化時,完成相關的重建的功能。就像在使用 Xcode 時,每次保存文件都會進行一次創建。
接下來使用 `npm` 安裝 React Native CLI 工具:
npm install -g react-native-cli
這使用 Node 包管理器抓取 CLI 工具,並且全局安裝;`npm` 在功能上與 CocoaPods 或者 Carthage 類似。
瀏覽到你想要創建 React Native 應用的文件夾下,使用 CLI 工具構建項目:
react-native init PropertyFinder
現在,已經創建了一個初始項目,包含了創建和運行一個 React Native 應用所需的一切。
如果仔細觀察了創建的文件夾和文件,你會發現一個 node_modules 文件夾,包含了 React Native 框架。你也會發現一個 index.ios.js 文件,這是 CLI 工具創建的一個空殼應用。最後,會出現一個 Xcode 項目文件和一個 iOS 文件夾,包含了少量的代碼用來引入 bootstrap 到你的項目中。
打開 Xcode 項目文件,然後創建並運行。模擬器將會啓動並且展示下面的問候語:
你可以能發現,有一個終端窗口彈出,輸出了下面的信息:
$ npm start
> react-native@0.1.0 start /Users/colineberhardt/Projects/react-native
> ./packager/packager.sh
===============================================================
| Running packager on port 8081.
| Keep this packager running while developing on any JS
| projects. Feel free to close this tab and run your own
| packager instance if you prefer.
|
| https://github.com/facebook/react-native
|
===============================================================
React packager ready.
這就是 React Native 包,在 node 下運行。一會兒你就會知道它是用來幹什麼的。
不要關閉終端窗口;就然它在後臺運行。如果你不小心關了,只需要停下來使用 Xcode 重新運行項目。
注意:在進入編碼工作之前,還有最後一件事 —— 在這個教程中,你需要編寫大量的 JavaScript 代碼,Xcode 並非是最好的工具!我使用 Sublime Text,一個價格合理且應用廣泛的編輯器。不過,atom,brackets 或者其他輕量的編輯器都能勝任這份工作。
React Native 你好
在開始“搜房App”之前,先來個簡單的 Hello World App 熱熱身。在這一節裏,你將會使用到一些組件。
在你鍾愛的編輯其中打開 index.ios.js,刪除當前的內容,因爲你要從頭構建你自己的應用。然後在文件頂部增加下面這樣一行::
'use strict';
這行代碼是用於開啓 Strict Mode,Strict mode的錯誤處理可以有所提高,JavaScript的一些語言缺陷也可以避免。簡而言之就是,JavaScript在這種模式下工作地更好!
注意:想要研究一下 Strict Mode 的朋友,我會推薦你閱讀 Jon Resig 的文章:“ECMAScript 5 Strict Mode, JSON, and More”
然後,加上這一行:
var React = require('react-native');
這句代碼是將 react-native 模塊加載進來,並將它賦值給變量 React 的。React Native 使用同 Node.js 相同的模塊加載方式:require,這個概念可以等同於 Swift 中的“鏈接庫”或者“導入庫”。
注意:想要了解更多關於 JavaScript 模塊的知識,我推薦閱讀 Addy Osmani 寫的這篇文章。
在 require 語句的下面,加上這一段:
var styles = React.StyleSheet.create({
text: {
color: 'black',
backgroundColor: 'white',
fontSize: 30,
margin: 80
}
});
以上代碼定義了一段應用在 “Hello World” 文本上的樣式。如果你曾接觸過Web開發,那你很可能已經發現了:React Native 使用的是 CSS 來定義應用界面的樣式。
現在我們來關注應用本身吧!依然是在相同的文件下,將以下代碼添加到樣式代碼的下面:
class PropertyFinderApp extends React.Component {
render() {
return React.createElement(React.Text, {style: styles.text}, "Hello World!");
}
}
是的,奏是 JavaScript class!
類 (class) 是在ES6中被引入的,縱然JavaScript一直在進步,但Web開發者受困於兼容瀏覽器的狀況中,不能怎麼使用JS的新特性。React Native運行在JavaScriptCore中是,也就是說,你可以使用JS的新特性啦,完全不用擔心兼容什麼的呢。
注意:如果你是一名 Web 開發者,我百分百鼓勵你要使用現代的JavaScript,然後使用像 Babel 這樣的工具生成兼容性的 JavaScript,用於支持兼容性不好的老瀏覽器。
PropertyFinderApp 繼承了 React.Component(React UI的基礎模塊)。組件包含着不可變的屬性,可變的狀態變量以及暴露給渲染用的方法。這會你做的應用比較簡單,只用一個渲染方法就可以啦。
React Native 組件並不是 UIKit 類,它們只能說是在某種程度上等同。框架只是將 React 組件樹轉化成爲原生的UI。
最後一步啦,將這一行加在文件末尾:
React.AppRegistry.registerComponent('PropertyFinder', function() { return PropertyFinderApp });
AppRegistry 定義了App的入口,並提供了根組件。
保存 PropertyFinderApp.js,回到Xcode中。確保 PropertyFinder 規劃(scheme)已經勾選了,並設置了相應的 iPhone 模擬器,然後生成並運行你的項目。幾秒之後,你應該就可以看到 “Hello World” 應用正在運行了:
還不相信這是真的?:] 那打開你的 Xcode,選擇 Debug\View Debugging\Capture View Hierarchy,你看 native view hierarchy 中都沒有 UIWebView,就只有一個原生的view!:]
你一定很好奇其中的原理吧,那就在 Xcode 中打開 AppDelegate.m,接着找到 application:didFinishLaunchingWithOptions:這個方法構建了 RCTRootView 用於加載 JavaScript 應用以及渲染最後的視圖的。
當應用開始運行的時候,RCTRootView將會從以下的URL中加載應用:
http://localhost:8081/index.ios.bundle
重新調用了你在運行這個App時打開的終端窗口,它開啓了一個 packager 和 server 來處理上面的請求。
在 Safari 中打開那個 URL;你將會看到這個 App 的 JavaScript 代碼。你也可以在 React Native 框架中找到你的 “Hello World” 代碼。
當你的App開始運行了以後,這段代碼將會被加載進來,然後 JavaScriptCore 框架將會執行它。在 Hello World 的例子裏,它將會加載 PropertyFinderApp 組件,然後構建出原生的 UIKit 視圖。關於這部分的內容,後文裏會再詳細解釋的。
你好 JSX 的世界
你當前的應用程序會使用 React.createElement 來構建應用 UI ,React會將其轉換到原生環境中。在當前情況下,你的JavaScript代碼是完全可讀的,但一個更復雜的 UI 與嵌套的元素將迅速使代碼變成一大坨。
確保應用程序仍在運行,然後回到你的文本編輯器中,編輯 PropertyFinderApp.js 。修改組件 render 方法的返回語句如下:
return <React.Text style={styles.text}>Hello World (Again)</React.Text>;
這是 JSX ,或 JavaScript 語法擴展,它直接在你的 JavaScript 代碼中混合了類似 HTML 的語法;如果你是一個 web 開發人員,應該對此不陌生。在本篇文章中你將一直使用 JSX 。
把你的改動保存到 PropertyFinderApp.js 中,並返回到模擬器。按下 Cmd + R ,你將看到你的應用程序刷新,並顯示更新的消息 “Hello World(again)”。
重新運行一個 React Native 應用程序像刷新 web 瀏覽器一樣簡單!:]
因爲你會使用相同的一系列 JavaScript 文件,您可以讓應用程序一直運行,只在更改和保存 PropertyFinderApp.js 後刷新即可
注意:如果你感到好奇,可以看看你的“包”在瀏覽器中,JSX被轉換成什麼。
這個 “Hello World” 已經夠大家玩耍了,是時候構建實際的應用程序了!
添加導航
我們的房產查找應用使用標準的棧式導航,基於 UIKit 的 navigation controller。現在正是添加的時候。
在 index.ios.js 文件中,把 PropertyFinderApp 重命名爲HelloWorld:
class HelloWorld extends React.Component {
“Hello World” 這幾個字你還需要讓它顯示一會兒,但它不再是應用的根組件了。
接下來,在 HelloWorld 這個組件下面添加如下這個類:
class PropertyFinderApp extends React.Component {
render() {
return (
<React.NavigatorIOS
style={styles.container}
initialRoute={{
title: 'Property Finder',
component: HelloWorld,
}}/>
);
}
}
構造一個 navigation controller,應用一個樣式,並把初始路由設爲 Hello World 組件。在 Web 開發中,路由就是一種定義應用導航的一種技術,即定義頁面——或者說是路由——與 URL 的對應關係。
在同一個文件中,更新樣式定義,包含如下 container 的樣式:
var styles = React.StyleSheet.create({
text: {
color: 'black',
backgroundColor: 'white',
fontSize: 30,
margin: 80
},
container: {
flex: 1
}
});
在隨後的教程中會告訴你 flex: 1 是什麼意思。
回到模擬器,Cmd+R,看看新 UI 的樣子:
創建搜索頁
在項目中添加一個新文件,命名爲 SearchPage.js,然後將其放在PropertyFinderApp.js 所在目錄下。在文件中添加下面代碼:
'use strict';
var React = require('react-native');
var {
StyleSheet,
Text,
TextInput,
View,
TouchableHighlight,
ActivityIndicatorIOS,
Image,
Component
} = React;
你會注意到,位於引入 react-native 所在位置的前面有一個嚴格模式標識,緊接着的聲明語句是新知識。
這是一種解構賦值,准許你獲取對象的多個屬性並且使用一條語句將它們賦給多個變量。結果是,後面的代碼中可以省略掉 React 前綴;比如,你可以直接引用 StyleSheet ,而不再需要 React.StyleSheet。解構同樣適用於操作數組,更多細節請戳這裏。
繼續在 SearchPage.js 文件中添加下面的樣式:
var styles = StyleSheet.create({
description: {
marginBottom: 20,
fontSize: 18,
textAlign: 'center',
color: '#656565'
},
container: {
padding: 30,
marginTop: 65,
alignItems: 'center'
}
});
同樣,以上都是標準的 CSS 屬性。和 Interface Builder 相比,這樣設置樣式缺少了可視化,但是比起在 viewDidLoad() 中逐個設置視圖屬性的做法更友好!
只需要把組件添加到樣式聲明的前面:
class SearchPage extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.description}>
Search for houses to buy!
</Text>
<Text style={styles.description}>
Search by place-name, postcode or search near your location.
</Text>
</View>
);
}
}
render 很好地展示出 JSX 以及它表示的結構。通過這個樣式,你可以輕易地描繪出組件 UI 的結構:一個容器,包含兩個 text 標籤。
最後,將下面的代碼添加到文件末尾:
module.exports = SearchPage;
這可以 export SearchPage 類,方便在其他文件中使用它。
下一步是更新應用的路由,以初始化路由。
打開 PropertyFinderApp.js,在文件頂部緊接着上一個 require 語句的位置添加下面代碼:
var SearchPage = require('./SearchPage');
在 PropertyFinderApp 類的 render 函數內部,通過更新 initialRoute 來引用最新添加的頁面,如下:
component: SearchPage
此時,如果你願意則可以移除 HelloWorld 類以及與它相關聯的樣式。你不在需要那段代碼了。
切換到模擬器,按下 Cmd+R 查看新的 UI:
現在,你已經看到了用基本的 CSS 屬性來控制外間距(margin),內間距(padding)還有顏色(color)。不過,可能你還不太瞭解要如何使用伸縮盒(flexbox),flexbox 是最近新加入 CSS 規範,用它就能很便利地佈局界面。
React Native 用 css-layout(這是一個用 JavaScript 實現flexbox標準然後編譯成 C(iOS平臺)或者Java(Android平臺)的庫)。
Facebook把這個項目單獨出來實在太正確了,這樣可以編譯成多種語言,促進更多新穎的應用的發展,比如flexbox layout to SVG。
在你的App中,容器(container)默認地是縱向佈局,也就是說在它的子元素將會豎直地排列,像這樣:
這被稱爲主軸 (main axis),它的方向可以是豎直的也可以是水平的。
每一個子元素在豎直方向上的位置是由它的margin,height和padding共同決定的。容器的 alignItems 屬性也要設置成 center,這個屬性可以控制子元素在十字軸上的位置。在這裏,它實現了居中對齊的文本。
好啦,現在我們把輸入框和按鈕加上去吧。打開 SearchPage.js,將下面的代碼插入第二個 Text 元素的後面:
<View style={styles.flowRight}>
<TextInput
style={styles.searchInput}
placeholder='Search via name or postcode'/>
<TouchableHighlight style={styles.button}
underlayColor='#99d9f4'>
<Text style={styles.buttonText}>Go</Text>
</TouchableHighlight>
</View>
<TouchableHighlight style={styles.button}
underlayColor='#99d9f4'>
<Text style={styles.buttonText}>Location</Text>
</TouchableHighlight>
現在你已經加上了兩個最高等級的視圖(top-level view),一個視圖包含了文本輸入框和一個按鈕,還有一個視圖內只有一個按鈕。在後文中你會看到,它們的樣式是什麼樣的。
接着,添加上對應的樣式:
flowRight: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'stretch'
},
buttonText: {
fontSize: 18,
color: 'white',
alignSelf: 'center'
},
button: {
height: 36,
flex: 1,
flexDirection: 'row',
backgroundColor: '#48BBEC',
borderColor: '#48BBEC',
borderWidth: 1,
borderRadius: 8,
marginBottom: 10,
alignSelf: 'stretch',
justifyContent: 'center'
},
searchInput: {
height: 36,
padding: 4,
marginRight: 5,
flex: 4,
fontSize: 18,
borderWidth: 1,
borderColor: '#48BBEC',
borderRadius: 8,
color: '#48BBEC'
}
要注意格式問題:每一個樣式都是用逗號分隔開的,所以別忘了在container 選擇器後面還要加上一個逗號。
以上的樣式將會應用在你剛剛加上的輸入框和按鈕上。
現在返回到模擬器,然後按下 Cmd+R 刷新界面:
文本區域和 ’Go’ 按鈕在同一行,不需要顯式地定義兩個組件的寬度,你只需要將它們放在同一個容器中,加上 flexDirection:'row' 樣式,再定義好它們的 flex 值。文本區域是 flex:4,按鈕則是 flex:1,這說明兩者的寬度比是4:1。
大概你也發現了,你的“按鈕”其實並不是按鈕!:] 使用了 UIKit 後,按鈕更傾向於是可以輕碰(tap)的標籤(label),所以 React Native 團隊決定直接在 JavaScript 中構建按鈕了。所以你在 App 中使用的按鈕是 TouchableHighlight,這是一個 React Native 組件,當輕碰 TouchableHighlight 時,它會變得透明從而顯示出襯底的顏色(也就是按鈕下層的組件顏色)。
搜索界面的最後一步就是加上一張圖片.你可以從這裏下載我們用的圖片素材並解壓。
在Xcode中打開Images.xcassets文件,點擊加號添加一個新的圖片集。然後將圖片素材拖進正確的“區間”:
你需要重啓應用才能讓圖片生效。
將以下代碼添加到 TouchableHighlight 組件後面,它將用於“獲取位置”按鈕:
<Image source={require('image!house')} style={styles.image}/>
現在再樣式表的最後加上圖片對應的樣式,別忘了給原樣式中最後一個加上逗號哦:
image: {
width: 217,
height: 138
}
require('image!house') 語句用於確定在你應用的asset目錄下的圖片資源,在 Xcode 中,如果你的打開了 Images.xcassets,你會看到一個“房屋”的圖標,正是上面代碼中引用到的。
返回到模擬器,Cmd+R刷新UI:
注意:如果你這會沒有看到“房屋”圖片,取而代之的是一張“找不到資源”的圖片,嘗試重啓packager(也就是在終端裏輸入 npm start 命令)。
現在你的應用看起來挺不錯的啦,不過它還少了點功能。接下來你的任務就是給它加上點狀態,讓它執行一些操作。
添加組件狀態
每個 React 組件都帶有一個key-value存儲的狀態對象,你必須在組件渲染之前設置其初始狀態。
在 SearchPage.js 中,我們對 SearchPage 類中,render方法前添加以下的代碼。
constructor(props) {
super(props);
this.state = {
searchString: 'london'
};
}
現在你的組件擁有一個狀態變量:searchString ,且初始值被設置爲 london 。
這時候你需要利用起組件中的狀態了。在render方法中,用以下的代碼替換TextInput元素中的內容:
<TextInput
style={styles.searchInput}
value={this.state.searchString}
placeholder='Search via name or postcode'/>
這一步設置了 TextInput 組件 value 屬性的值,這個值用於把狀態變量 searchString 的當前值作爲展示給用戶的文字。我們已經考慮初始值的設定了,但如果用戶編輯這裏的文字會發生什麼呢?
第一步需要建立一個方法來處理事件。在 SearchPage 類中添加以下的代碼:
onSearchTextChanged(event) {
console.log('onSearchTextChanged');
this.setState({ searchString: event.nativeEvent.text });
console.log(this.state.searchString);
}
上面的代碼從 events 中取出了 text 屬性的值,並用於更新組件的狀態。這裏面也添加了一些有用的調試代碼。
當文字改變時,需要讓這個方法被調用,調用後的文字會通過 render 函數返回到組件中。因此我們需要在標籤上添加一個 onChange 屬性,添加後的標籤如下所示:
<TextInput
style={styles.searchInput}
value={this.state.searchString}
onChange={this.onSearchTextChanged.bind(this)}
placeholder='Search via name or postcode'/>
當用戶更改文本時,會調用 onChange 上 的函數;在本例中,則是 onSearchTextChanged 。
注意:你估計會對 bind(this) 語句有疑問。在 JavaScript 中,this 這個關鍵字有點不同於大多數其他語言;在 Swift 表示 “自身”。在這種情況中,bind 可以確保在 onSearchTextChanged 方法中, this 可以作爲組件實例的引用。有關更多信息,請參見MDN this頁面。
在你再次刷新你的應用程序之前,還有一個步驟:在 return 前添加以下語句,打印一條日誌來記錄 render() 函數的調用:
console.log('SearchPage.render');
你會從這些日誌語句中學到一些很有趣的東西!:]
回到你的模擬器,然後按Cmd + R。您現在應該看到文本輸入的初始值爲 “london” ,編輯一下文本,從而在 Xcode 控制檯中產生一些日誌:
注意看上面的截圖,日誌打印的順序看起來有些奇怪:
第一次調用 render() 函數用於設置視圖。當文本變化時, onSearchTextChanged 函數被調用。之後,通過更新組件的狀態來反映輸入了新的文本,這會觸發另一次 render 。 onSearchTextChanged() 函數也會被調用,會將改變的字符串打印出來。每當應用程序更新任何 React 組件,將會觸發整個UI層的重新繪製,這會調用你所有組件的 render 方法。這是一個好主意,因爲這樣做把組件的渲染邏輯,從狀態變化影響UI這一過程中完全解耦出來。
與其他大多數 UI 框架所不同的是,你既不需要在狀態改變的時候去手動更新 UI ,或使用某種類型的綁定框架,來創建某種應用程序狀態和它的 UI 表現的關聯;例如,我的文章中講的,通過ReactiveCocoa實現MVVM模式。
在 React 中,你不再需要擔心 UI 的哪些部分可能受到狀態變化的影響;你的整個應用程序的 UI,都可以簡單地表示爲一個函數的狀態。
此時,你可能已經發現了這一概念中一個根本性的缺陷。是的,非常準確——性能!
你肯定不能在 UI 變化時,完全拋棄掉整個 UI 然後重新繪製吧
?這就是 React 高明的地方了。每當 UI 渲染出來後,render 方法會返回一顆視圖渲染樹,並與當前的 UIKit 視圖進行比較。這個稱之爲 reconciliation 的過程的輸出是一個簡單的更新列表, React 會將這個列表應用到當前視圖。這意味着,只有實際改變了的部分纔會重新繪製。
這個令人拍案叫絕的嶄新概念讓ReactJS變得獨特——virtual-DOM(文檔對象模型,一個web文檔的視圖樹)和 reconciliation 這些概念——被應用於iOS應用程序。
稍後你可以整理下思路,之後,在剛纔的應用中你仍然有一些工作要做。日誌代碼增加了代碼的繁瑣性,已經不需要了,所以刪除掉日誌代碼。
初始化搜索功能
爲了實現搜索功能,你需要處理 “Go” 按鈕的點擊事件,調用對應的 API,並提供一個視覺效果,告訴用戶正在做查詢。
在 SearchPage.js 中,在構造函數中把初始狀態更新成:
this.state = {
searchString: 'london',
isLoading: false
};
新的 isLoading 屬性將會記錄是否有請求正在處理的狀態。
在 render 開始的添加如下邏輯:
var spinner = this.state.isLoading ?
( <ActivityIndicatorIOS
hidden='true'
size='large'/> ) :
( <View/>);
這是一個三元操作符,與 if 語句類似,即根據組件 isLoading 的狀態,要麼添加一個 indicator,要麼添加一個空的 view。因爲整個組件會不停地更新,所以你自由地混合 JSX 和 JavaSript 代碼。
回到用 JSX 定義搜索界面的地方,在圖片的下面添加:
{spinner}
給渲染“Go”的 TouchableHighlight 標記添加如下的屬性:
onPress={this.onSearchPressed.bind(this)}
接下來,添加下面這兩個方法到 SearchPage 類中:
_executeQuery(query) {
console.log(query);
this.setState({ isLoading: true });
}
onSearchPressed() {
var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
this._executeQuery(query);
}
_executeQuery() 之後會進行真實的查詢,現在的話就是簡單輸出一條信息到控制檯,並且把 isLoading 設置爲對應的值,這樣 UI 就可以顯示新的狀態了。
提示:JavaScript 的類並沒有訪問修飾符,因此沒有 “私有” 的該奶奶。因此常常會發現開發者使用一個下劃線作爲方法的前綴,來說明這些方法是私有方法。
當 “Go” 按鈕被點擊時,onSearchPressed() 將會被調用,開始查詢。
最後,添加下面這個工具函數在定義 SearchPage 類的上面:
function urlForQueryAndPage(key, value, pageNumber) {
var data = {
country: 'uk',
pretty: '1',
encoding: 'json',
listing_type: 'buy',
action: 'search_listings',
page: pageNumber
};
data[key] = value;
var querystring = Object.keys(data)
.map(key => key + '=' + encodeURIComponent(data[key]))
.join('&');
return 'http://api.nestoria.co.uk/api?' + querystring;
};
- 這個函數並不依賴 SearchPage,因此被定義成了一個獨立的函數,而不是類方法。他首先通過 data 來定義查詢字符串所需要的參數,接着將 data 轉換成需要的字符串格式,name=value 對,使用 & 符號分割。語法 => 是一個箭頭函數,又一個對 JavaScript 語言的擴展,提供了這個便捷的語法來創建一個匿名函數。
回到模擬器,Cmd+R,重新加載應用,點擊 “Go” 按鈕。你可以看到 activity indicator 顯示出來,再看看 Xcode 的控制檯:
activity indicator 渲染了,並且作爲請求的 URL 出現在輸出中。把 URL 拷貝到瀏覽器中訪問看看得到的結果。你會看到大量的 JSON 對象。別擔心——你不需要理解它們,之後會使用代碼來解析之。
提示:應用使用了 Nestoria 的 API 來做房產的搜索。API 返回的 JSON 數據非常的直白。但是你也可以看看文檔瞭解更多細節,請求什麼 URL 地址,以及返回數據的格式。
下一步就是從應用中發出請求。
執行 API 請求
還是 SearchPage.js 文件中,更新構造器中的初始 state 添加一個message 變量:
this.state = {
searchString: 'london',
isLoading: false,
message: ''
};
在 render 內部,將下面的代碼添加到 UI 的底部:
<Text style={styles.description}>{this.state.message}</Text>
你需要使用這個爲用戶展示多種信息。
在 SearchPage 類內部,將以下代碼添加到 _executeQuery() 底部:
fetch(query)
.then(response => response.json())
.then(json => this._handleResponse(json.response))
.catch(error =>
this.setState({
isLoading: false,
message: 'Something bad happened ' + error
}));
這裏使用了 fetch 函數,它是 Web API 的一部分。和 XMLHttpRequest 相比,它提供了更加先進的 API。異步響應會返回一個 promise,成功的話會轉化 JSON 並且爲它提供了一個你將要添加的方法。
最後一步是將下面的函數添加到 SearchPage:
_handleResponse(response) {
this.setState({ isLoading: false , message: '' });
if (response.application_response_code.substr(0, 1) === '1') {
console.log('Properties found: ' + response.listings.length);
} else {
this.setState({ message: 'Location not recognized; please try again.'});
}
}
如果查詢成功,這個方法會清除掉正在加載標識並且記錄下查詢到屬性的個數。
注意:Nestoria 有很多種返回碼具備潛在的用途。比如,202 和 200 會返回最佳位置列表。當你創建完一個應用,爲什麼不處理一下這些,可以爲用戶呈現一個可選列表。
保存項目,然後在模擬器中按下 Cmd+R,嘗試搜索 ‘london’;你會在日誌信息中看到 20 properties were found。然後隨便嘗試搜索一個不存在的位置,比如 ‘narnia’,你會得到下面的問候語。
是時候看一下這20個房屋所對應的真實的地方,比如倫敦!
結果顯示
創建一個新的文件,命名爲 SearchResults.js,然後加上下面這段代碼:
'use strict';
var React = require('react-native');
var {
StyleSheet,
Image,
View,
TouchableHighlight,
ListView,
Text,
Component
} = React;
你肯定注意到啦,這裏用到了 require 語句將 react-native 模塊引入其中,還有一個重構賦值語句。
接着就是加入搜索結果的組件:
class SearchResults extends Component {
constructor(props) {
super(props);
var dataSource = new ListView.DataSource(
{rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
this.state = {
dataSource: dataSource.cloneWithRows(this.props.listings)
};
}
renderRow(rowData, sectionID, rowID) {
return (
<TouchableHighlight
underlayColor='#dddddd'>
<View>
<Text>{rowData.title}</Text>
</View>
</TouchableHighlight>
);
}
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}/>
);
}
}
上述的代碼裏用到了一個特定的組件 – ListView – 它能將數據一行行地呈現出來,並放置在一個可滾動的容器內,和 UITableView 很相似。通過 ListView.DataSource 將 ListView 的數據引入,還有一個函數來顯示每一行UI。
在構建數據源的同時,你還需要一個函數用來比較每兩行之間是否重複。 爲了確認列表數據的變化,在 reconciliation 過程中ListView 就會使用到這個函數。在這個實例中,由 Nestoria API 返回的房屋數據都有一個guid 屬性,它就是用來測試數據變化的。
現在將模塊導出的代碼添加至文件末尾:
module.exports = SearchResults;
將下面這段代碼加到 SearchPage.js 較前的位置,不過要在 require 語句的後面哦:
var SearchResults = require('./SearchResults');
這樣我們就能在 SearchPage 類中使用剛剛加上的 SearchResults 類。
還要把 _handleResponse 方法中的 console.log 語句改成下面這樣:
this.props.navigator.push({
title: 'Results',
component: SearchResults,
passProps: {listings: response.listings}
});
SearchResults 組件通過上面的代碼傳入列表裏。在這裏用的是 push方法確保搜索結果全部推進導航棧中,這樣你就可以通過 ‘Back’ 按鈕返回到根頁面。
回到模擬器,按下 Cmd+R 刷新頁面,然後試試看我們的搜索。估計你會得到類似下面這樣的結果:
耶!你的搜索實現了呢,不過這搜索結果頁面的顏值也太低了,不要擔心,接下來給它化化妝。
可點擊樣式
這些 React Native 的原生代碼現在應該理解起來輕車熟路了,所以本教程將會加快速度。
在 SearchResults.js 中,destructuring 聲明後面添加以下語句來定義樣式:
var styles = StyleSheet.create({
thumb: {
width: 80,
height: 80,
marginRight: 10
},
textContainer: {
flex: 1
},
separator: {
height: 1,
backgroundColor: '#dddddd'
},
price: {
fontSize: 25,
fontWeight: 'bold',
color: '#48BBEC'
},
title: {
fontSize: 20,
color: '#656565'
},
rowContainer: {
flexDirection: 'row',
padding: 10
}
});
這些定義了每一行的樣式。
接下來修改 renderRow() 如下:
renderRow(rowData, sectionID, rowID) {
var price = rowData.price_formatted.split(' ')[0];
return (
<TouchableHighlight onPress={() => this.rowPressed(rowData.guid)}
underlayColor='#dddddd'>
<View>
<View style={styles.rowContainer}>
<Image style={styles.thumb} source={{ uri: rowData.img_url }} />
<View style={styles.textContainer}>
<Text style={styles.price}>£{price}</Text>
<Text style={styles.title}
numberOfLines={1}>{rowData.title}</Text>
</View>
</View>
<View style={styles.separator}/>
</View>
</TouchableHighlight>
);
}
這個操作修改了返回的價格,將已經格式了化的”300000 GBP”中的GBP後綴刪除。然後它通過你已經很熟悉的技術來渲染每一行的 UI 。這一次,通過一個 URL 來提供縮略圖的數據, React Native 負責在主線程之外解碼這些數據。
同時要注意 TouchableHighlight 組件中 onPress屬性後使用的箭頭函數;它用於捕獲每一行的 guid。
最後一步,給類添加一個方法來處理按下操作:
rowPressed(propertyGuid) {
var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
}
該方法通過用戶觸發的屬性來定位。目前該方法沒有做任何事,你可以稍後處理。現在,是時候欣賞你的大作了。
回到模擬器,按下 Cmd + R 查看結果:
看起來好多了——儘管你會懷疑是否任何人都能承受住在倫敦的代價!
是時候嚮應用程序添加最後一個視圖了。
房產詳情視圖
添加一個新的文件 PropertyView.js 到項目中,在文件的頂部添加如下代碼:
'use strict';
var React = require('react-native');
var {
StyleSheet,
Image,
View,
Text,
Component
} = React;
信手拈來了吧!
接着添加如下樣式:
var styles = StyleSheet.create({
container: {
marginTop: 65
},
heading: {
backgroundColor: '#F8F8F8',
},
separator: {
height: 1,
backgroundColor: '#DDDDDD'
},
image: {
width: 400,
height: 300
},
price: {
fontSize: 25,
fontWeight: 'bold',
margin: 5,
color: '#48BBEC'
},
title: {
fontSize: 20,
margin: 5,
color: '#656565'
},
description: {
fontSize: 18,
margin: 5,
color: '#656565'
}
});
然後加上組件本身:
class PropertyView extends Component {
render() {
var property = this.props.property;
var stats = property.bedroom_number + ' bed ' + property.property_type;
if (property.bathroom_number) {
stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
? 'bathrooms' : 'bathroom');
}
var price = property.price_formatted.split(' ')[0];
return (
<View style={styles.container}>
<Image style={styles.image}
source={{uri: property.img_url}} />
<View style={styles.heading}>
<Text style={styles.price}>£{price}</Text>
<Text style={styles.title}>{property.title}</Text>
<View style={styles.separator}/>
</View>
<Text style={styles.description}>{stats}</Text>
<Text style={styles.description}>{property.summary}</Text>
</View>
);
}
}
render() 前面部分對數據進行了處理,與通常的情況一樣,API 返回的數據良莠不齊,往往有些字段是缺失的。這段代碼通過一些簡單的邏輯,讓數據更加地規整一些。
render 剩餘的部分就非常直接了。它就是一個簡單的這個狀態不可變狀態的函數。
最後在文件的末尾加上如下的 export:
module.exports = PropertyView;
返回到 SearchResults.js 文件,在頂部,require React 的下面,添加一個新的 require 語句。
var PropertyView = require('./PropertyView');
接下來更新 rowPassed(),添加跳轉到新加入的 PropertyView:
rowPressed(propertyGuid) {
var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
this.props.navigator.push({
title: "Property",
component: PropertyView,
passProps: {property: property}
});
}
你知道的:回到模擬器,Cmd + R,一路通過搜索點擊一行到房產詳情界面:
物廉價美——看上去很不錯哦!
應用即將完成,最後一步是允許用戶搜索附近的房產。
地理位置搜索
在 Xcode 中,打開 Info.plist 添加一個新的 key,在編輯器內部單擊鼠標右鍵並且選擇 Add Row。使用NSLocationWhenInUseUsageDescription 作爲 key 名並且使用下面的值:
PropertyFinder would like to use your location to find nearby properties
下面是當你添加了新的 key 後,所得到的屬性列表:
你將把這個關鍵的細節提示呈現給用戶,方便他們請求訪問當前位置。
打開 SearchPage.js,找到用於渲染 Location 按鈕的TouchableHighlight,然後爲其添加下面的屬性值:
onPress={this.onLocationPressed.bind(this)}
當你用手指輕點這個按鈕,會調用 onLocationPressed —— 接下來會定義這個方法。
將下面的代碼添加到 SearchPage 類中:
onLocationPressed() {
navigator.geolocation.getCurrentPosition(
location => {
var search = location.coords.latitude + ',' + location.coords.longitude;
this.setState({ searchString: search });
var query = urlForQueryAndPage('centre_point', search, 1);
this._executeQuery(query);
},
error => {
this.setState({
message: 'There was a problem with obtaining your location: ' + error
});
});
}
通過 navigator.geolocation 檢索當前位置;這是一個 Web API 所定義的接口,所以對於每個在瀏覽器中使用 location 服務的用戶來說這個接口都應該是一致的。React Native 框架藉助原生的 iOS location 服務提供了自身的 API 實現。
如果當前位置很容易獲取到,你將調用第一個箭頭函數;這會向Nestoria 發送一個 query。如果出現錯誤則會得到一個基本的出錯信息。
因爲你已經改變了屬性列表,你需要重新啓動這個應用以看到更改。抱歉,這次不可以 Cmd+R。請中斷 Xcode 中的應用,然後創建和運行項目。
在使用基於位置的搜索前,你需要指定一個被 Nestoria 數據庫覆蓋的位置。在模擬器菜單中,選擇 Debug\Location\Custom Location … 然後輸入 55.02 維度和 -1.42 經度,這個座標是英格蘭北部的一個景色優美的海邊小鎮,我經常在那給家裏打電話。
警示:我們可以正常地使用位置搜索功能,不過可能有部分同學不能使用(在訪問時返回 access denied 錯誤)—— 我們尚不確定其原因,可能是 React Native 的問題?如果誰遇到了同樣的問題並且已經結果,煩請告訴我們。這裏沒有倫敦那樣值得炫耀 —— 不過更加經濟!:]
下一步行動?
完成了第一個 React Native 應用呢,恭喜你!你可以下載本教程的完整代碼,親自來試試看。
如果已經接觸過 Web 開發了,你會發現使用 JavaScript 和 React 來定義與原生 UI 相連接的接口和導航是多麼地容易。而如果你曾經開發過原生 App,我相信在使用 React Native 的過程裏你會感受到它種種好處:快速的應用迭代,JavaScript 的引入以及清晰地使用 CSS 定義樣式。
也許下次做 App 的時候,你可以試試這個框架?或者說,你依然堅持使用 Swift 或者 Objective-C?無論之後你的選擇是怎麼樣的,我都希望讀完這篇文章的你有所收穫,還能把這些收穫融入到你的項目當中是最好的啦